# frozen_string_literal: true

module Vedeu

  module Events

    # Contains all the logic of an event. Handles debouncing and
    # throttling.
    #
    # Vedeu provides an event mechanism to facilitate the
    # functionality of your application. The events are either Vedeu
    # defined, ie. system events or user defined, ie. user events.
    # User events allow you to orchestrate behaviour within your
    # application, ie. the user presses a specific key,
    # you trigger an event to make something happen. Eg. pressing 'p'
    # instructs the player to play.
    #
    # Events described here assume that you have bound the events
    # within your classes:
    #
    #   class SomeClassInYourApplication
    #     Vedeu.bind(:event_name) do |arg1, arg2|
    #       # Things that should happen when the event is triggered;
    #       # these can be method calls or the triggering of another
    #       # event or events.
    #     end
    #
    #     Vedeu.bind(:event_name) do
    #       # Not all events you define will have arguments; like
    #       # methods.
    #       :do_stuff
    #     end
    #
    # @api private
    #
    class Event

      include Vedeu::Repositories::Model

      class << self

        # Register an event by name with optional delay (throttling)
        # which when triggered will execute the code contained within
        # the passed block.
        #
        # @macro param_name
        # @param options [Hash<Symbol => void>] The options to
        #   register the event with.
        # @option options :delay [Fixnum|Float] Limits the execution
        #   of the triggered event to only execute when first
        #   triggered, with subsequent triggering being ignored until
        #   the delay has expired.
        # @option options :debounce [Fixnum|Float] Limits the
        #   execution of the triggered event to only execute once the
        #   debounce has expired. Subsequent triggers before debounce
        #   expiry are ignored.
        # @macro param_block
        #
        # @example
        #   Vedeu.bind :my_event do |some, args|
        #     # ... some code here ...
        #
        #     Vedeu.trigger(:my_other_event)
        #   end
        #
        #   T = Triggered, X = Executed, i = Ignored.
        #
        #   0.0.....0.2.....0.4.....0.6.....0.8.....1.0.....1.2.....1.4.....1.6.
        #   .T...T...T...T...T...T...T...T...T...T...T...T...T...T...T...T...T..
        #   .X...i...i...i...i...X...i...i...i...i...X...i...i...i...i...i...i..
        #
        #   Vedeu.bind(:my_delayed_event, { delay: 0.5 })
        #     # ... some code here ...
        #   end
        #
        #   T = Triggered, X = Executed, i = Ignored.
        #
        #   0.0.....0.2.....0.4.....0.6.....0.8.....1.0.....1.2.....1.4.....1.6.
        #   .T...T...T...T...T...T...T...T...T...T...T...T...T...T...T...T...T..
        #   .i...i...i...i...i...i...i...X...i...i...i...i...i...i...X...i...i..
        #
        #   Vedeu.bind(:my_debounced_event, { debounce: 0.7 })
        #     # ... some code here ...
        #   end
        #
        # @return [Boolean]
        def bind(name, options = {}, &block)
          Vedeu.log(type: :event, message: "Binding: '#{name.inspect}'")

          new(name, block, options).bind
        end
        alias event bind
        alias register bind

        # {include:file:docs/dsl/by_method/bound.md}
        # @macro param_name
        # @return [Boolean]
        def bound?(name)
          Vedeu.events.registered?(name) ||
            Vedeu::Events::Aliases.registered?(name)
        end

        # {include:file:docs/dsl/by_method/unbind.md}
        # @macro param_name
        # @return [Boolean]
        def unbind(name)
          return false unless Vedeu.bound?(name)

          Vedeu.log(type:    :event,
                    message: "Unbinding: '#{name.inspect}'")

          Vedeu.events.remove(name)

          true
        end

        extend Forwardable

        def_delegators Vedeu::Events::Trigger,
                       :trigger

      end # Eigenclass

      # Returns a new instance of Vedeu::Events::Event.
      #
      # @param (see Vedeu::Events::Event.bind)
      # @return [Vedeu::Events::Event]
      def initialize(name, closure, options = {})
        @name         = name
        @options      = options
        @closure      = closure
        @deadline     = 0
        @executed_at  = 0
        @now          = 0
        @repository   = Vedeu.events
      end

      # @see Vedeu::Events::Event.bind
      def bind
        new_collection = if repository.registered?(name)
                           repository.find(name).add(self)

                         else
                           Vedeu::Events::Events.new([self], nil, name)

                         end

        repository.store(new_collection)

        true
      end

      # Triggers the event based on debouncing and throttling
      # conditions.
      #
      # @param args [Array]
      # @return [void]
      def trigger(*args)
        return execute(*args) unless debouncing? || throttling?

        return execute(*args) if debouncing? && debounce_expired?

        return execute(*args) if throttling? && throttle_expired?
      end

      protected

      # @!attribute [r] closure
      # @return [String]
      attr_reader :closure

      # @!attribute [r] name
      # @macro return_name
      attr_reader :name

      private

      # Execute the code stored in the event closure.
      #
      # @param args [void]
      # @return [void]
      def execute(*args)
        @deadline    = 0    # reset deadline
        @executed_at = @now # set execution time to now
        @now         = 0    # reset now

        closure.call(*args)
      end

      # Returns a boolean indicating whether throttling is required
      # for this event. Setting the delay option to any value greater
      # than 0 will enable throttling.
      #
      # @return [Boolean]
      def throttling?
        @now = Vedeu.clock_time

        options[:delay] > 0
      end

      # Returns a boolean indicating whether the throttle has expired.
      #
      # @return [Boolean]
      def throttle_expired?
        return true if (@now - @executed_at) > delay

        Vedeu.log(type: :event, message: "Throttling: '#{name.inspect}'")

        false
      end

      # Returns a boolean indicating whether debouncing is required
      # for this event. Setting the debounce option to any value
      # greater than 0 will enable debouncing.
      # Sets the deadline for when this event can be executed to a
      # point in the future determined by the amount of debounce time
      # left.
      #
      # @return [Boolean]
      def debouncing?
        @now = Vedeu.clock_time

        @deadline = @now + debounce unless deadline?

        options[:debounce] > 0
      end

      # Returns a boolean indicating whether the debounce has expired.
      #
      # @return [Boolean]
      def debounce_expired?
        return true if (@executed_at = @now) > @deadline

        Vedeu.log(type: :event, message: "Debouncing: '#{name.inspect}'")

        false
      end

      # Returns a boolean indicating whether this event has a
      # deadline.
      #
      # @return [Boolean]
      def deadline?
        @deadline > 0
      end

      # Return the amount of time in seconds to debounce the event by.
      #
      # @return [Fixnum|Float]
      def debounce
        options[:debounce] || defaults[:debounce]
      end

      # Return the amount of time in seconds to throttle the event by.
      #
      # @return [Fixnum|Float]
      def delay
        options[:delay] || defaults[:delay]
      end

      # Combines the options provided at instantiation with the
      # default values.
      #
      # @return [Hash<Symbol => void>]
      def options
        defaults.merge!(@options)
      end

      # @macro defaults_method
      def defaults
        {
          delay:      0.0,
          debounce:   0.0,
        }
      end

    end # Event

  end # Events

  # @api public
  # @!method bind
  #   @see Vedeu::Events::Event.bind
  # @api public
  # @!method bound?
  #   @see Vedeu::Events::Event.bound?
  # @api public
  # @!method unbind
  #   @see Vedeu::Events::Event.unbind
  def_delegators Vedeu::Events::Event,
                 :bind,
                 :bound?,
                 :unbind

  # :nocov:

  # See {file:docs/events/system.md#\_log_}
  Vedeu.bind(:_log_) { |msg| Vedeu.log(type: :debug, message: msg) }

  # :nocov:

end # Vedeu
