CHAIN_DEBUG = false

class ChainListener
  attr_reader :object, :callback

  def initialize(event_chain, object, callback)
    @event_chain = event_chain
    @object = object
    @callback = callback

    if RUBY_PLATFORM == 'opal' && CHAIN_DEBUG
      `window.chain_listeners = window.chain_listeners || 0;`
      `window.chain_listeners += 1;`
      `console.log('chain listeners: ', window.chain_listeners)`
    end
  end

  def remove
    # raise "event chain already removed" if @removed
    if @removed
      puts "event chain already removed"
      return
    end

    @removed = true
    @event_chain.remove_object(self)

    # We need to clear these to free memory
    @event_chain = nil
    @object = nil
    @callback = nil

    if RUBY_PLATFORM == 'opal' && CHAIN_DEBUG
      `window.chain_listeners -= 1;`
      `console.log('del chain listeners: ', window.chain_listeners)`
    end
  end
end

class EventChain
  def initialize(main_object)
    @event_chain = {}
    @main_object = main_object
    @event_counts = {}
  end

  # Register an event listener that chains from object to self
  def setup_listener(event, chain_listener)
    return chain_listener.object.on(event, @main_object) do |filter, *args|
      if (callback = chain_listener.callback)
        callback.call(event, filter, *args)
      else
        # Trigger on this value, when it happens on the parent

        # Only pass the filter from non-reactive to reactive?  This
        # lets us scope the calls on a proxied object.
        # Filters limit which listeners are triggered, passing them to
        # the ReactiveValue's from non-reactive lets us filter at the
        # reactive level as well.
        filter = nil unless !chain_listener.object.reactive? && @main_object.reactive?

        @main_object.trigger!(event, filter, *args)
      end
    end
  end

  # We can chain our events to any other object that includes
  # Events
  def add_object(object, &block)
    chain_listener = ChainListener.new(self, object, block)

    listeners = {}

    @main_object.listeners.keys.each do |event|
      # Create a listener for each event
      listeners[event] = setup_listener(event, chain_listener)
    end

    @event_chain[chain_listener] = listeners

    return chain_listener
  end


  def remove_object(chain_listener)
    @event_chain[chain_listener].each_pair do |event,listener|
      # Unbind each listener
      # TODO: The if shouldn't be needed, but sometimes we get nil for some reason?
      listener.remove if listener
    end

    @event_chain.delete(chain_listener)
  end

  def add_event(event)
    unless @event_counts[event]
      @event_chain.each_pair do |chain_listener,listeners|
        # Only add if we haven't already chained this event
        unless listeners[event]
          listeners[event] = setup_listener(event, chain_listener)
        end
      end
    end

    @event_counts[event] ||= 0
    @event_counts[event] += 1
  end

  # Removes the event from all events in all objects
  def remove_event(event)
    if @event_counts[event]
      count = @event_counts[event] -= 1

      if count == 0
        @event_chain.each_pair do |chain_listener,listeners|
          listeners[event].remove# if listeners[event]
          listeners.delete(event)
        end

        # Also remove the event count
        @event_counts.delete(event)
      end
    end
  end
end