require_relative 'event_bus/registrations'

class EventBus

  class << self

    #
    # Announce an event to any waiting listeners.
    #
    # The +event_name+ is added to the +payload+ hash (with the key +:event_name+)
    # before being passed on to listeners.
    #
    # @param event_name [String, Symbol] the name of your event
    # @param payload [Hash] the information you want to pass to the listeners
    # @return [EventBus] the EventBus, ready to be called again.
    #
    def publish(event_name, payload = {})
      case event_name
      when Symbol, String
        registrations.announce(event_name, payload)
        self
      else
        raise ArgumentError.new('The event name must be a string or a symbol')
      end
    end

    alias :announce :publish
    alias :broadcast :publish

    #
    # Subscribe to a set of events.
    #
    # If +blk+ is supplied, it will be called with any event whose name
    # matches +pattern+.
    #
    # If no block is given, and if +pattern+ is a String or a Regexp,
    # a method will be called on +listener+ whenever an event matching
    # +pattern+ occurs. In this case, if +method_name+ is supplied the
    # EventBus will look for, and call, a method of that name on +listener+;
    # otherwise if +method_name+ is not given, the EventBus will attempt to
    # call a method whose name matches the event's name.
    #
    # Finally, if no block is given and +pattern+ is not a String or a Regexp,
    # then +pattern+ is taken to be a listener object and the EventBus will
    # attempt to call a method on it whose name matches the event's name.
    #
    # Either +listener+ or +blk+ must be provided, both never both.
    #
    # When a matching event occurs, either the block is called or the +method_name+
    # method on the +listener+ object is called.
    #
    # @param pattern [String, Regexp] listen for any events whose name matches this pattern
    # @param listener the object to be notified when a matching event occurs
    # @param method_name [Symbol] the method to be called on +listener+ when a matching event occurs
    # @return [EventBus] the EventBus, ready to be called again.
    #
    def subscribe(pattern, listener = nil, method_name = nil, &blk)
      case pattern
      when Regexp, String, Symbol
        subscribe_pattern(pattern, listener, method_name, &blk)
      else
        raise ArgumentError.new('You cannot give two listeners') if listener || method_name
        raise ArgumentError.new('You cannot give both a listener and a block') if block_given?
        subscribe_obj(pattern)
      end
      self
    end

    alias :listen_for :subscribe

    #
    # Register a global error handler
    #
    # The supplied block will be called once for each error that is raised by
    # any listener, for any event.
    #
    # The block will be provided with two parameters, the listener that errored,
    # and the payload of the event.
    #
    # @param blk the block to be called when any unhandled error occurs in a listener
    # @return [EventBus] the EventBus, ready to be called again
    def on_error(&blk)
      registrations.on_error &blk
      self
    end

    #
    # Delete all current listener registrations
    #
    # @return the EventBus, ready to be called again.
    #
    def clear
      registrations.clear
      self
    end

    #
    # Adds a subscriber that only listens for the duration of the block. Mostly
    # useful for testing event dispatching.
    #
    # @param pattern [String, Regexp] listen for any events whose name matches this pattern
    # @param listener the object to be notified when a matching event occurs
    # @param method_name [Symbol] the method to be called on +listener+ when a matching event occurs
    # @return [EventBus] the EventBus, ready to be called again.
    def with_temporary_subscriber(pattern, listener = nil, method_name = nil)
      subscribe(pattern, listener, method_name)
      temporary_subscriber = registrations.last_subscriber

      yield

      self
    ensure
      registrations.remove_subscriber(temporary_subscriber)
    end

    private

    def subscribe_pattern(pattern, listener, method_name, &blk)
      if listener
        raise ArgumentError.new('You cannot give both a listener and a block') if block_given?
        raise ArgumentError.new('You must supply a method name') unless method_name
        registrations.add_method(pattern, listener, method_name)
      else
        raise ArgumentError.new('You must provide a listener or a block') unless block_given?
        registrations.add_block(pattern, &blk)
      end
    end

    def subscribe_obj(listener)
      registrations.add_block(/.*/) do |payload|
        method = payload[:event_name].to_sym
        listener.send(method, payload) if listener.respond_to?(method)
      end
    end

    def registrations
      Registrations.instance
    end

  end

end

