"""
Simple event dispatch system. It works like this::

  class X:
    def __init__(self, name):
      self.name = name

    def on_foo(self, event):
      print("Fire foo on {}".format(name))

  player = dict(id='player')
  a = X('a')
  b = X('b')
  dispatcher = EventDispatcher()
  dispatcher.register_event_type('foo')

  # 'a.on_foo' gets called for all 'foo' events
  dispatcher.add_subscriber(a, 'foo', None)
  # 'b.on_foo' gets called only for events with `entity is player`
  dispatcher.add_subscriber(b, 'foo', player)

  dispatcher.fire('foo', None, None)  # prints 'a'
  dispatcher.fire('foo', player, None)  # prints 'a' and 'b'

.. note::

  Having used this in a game, I think I'm going to try something totally
  different next time. This class will probably be removed in the next release.
"""
from enum import Enum


class Event:
    """
    .. py:attribute:: name

      Event name as registered in :py:meth:`EventDispatcher.register_event_type`

    .. py:attribute:: entity

      Entity associated with this event. May be ``None``.

    .. py:attribute:: data

      Arbitrary data passed to :py:meth:`EventDispatcher.fire`.
    """

    def __init__(self, name, entity, data):
        self.name = name
        self.entity = entity
        self.data = data
        self._is_halted = False

    def stop_propagation(self):
        """
        Prevent any more handlers for this event from firing
        """
        self._is_halted = True


class EventDispatcher:
    """
    Collects and calls object methods in response to events.

    For an event ``foo``, your subscriber should have a method ``on_foo()``
    and take one argument, an :py:class:`Event` instance.
    """

    def __init__(self):
        self.handlers = {}
        self._is_halted = False

    def register_event_type(self, name):
        """
        :param str|Enum name: Name of the event. May be a string-valued enum.

        Allow events with the given name to be subscribed to and fired.

        The dispatcher requires you to register events before using them to avoid
        typo-related errors.
        """
        if isinstance(name, Enum):
            name = name.value
        self.handlers[name] = []

    def add_subscriber(self, obj, name, entity):
        """
        :param object obj: Object to be called when the event fires
        :param str|Enum name: Name of the event. May be a string-valued enum.
        :param entity: If ``None``, receive all events regardless of their entity.
                       Otherwise, only receive events whose entity ``is`` this
                       object.

        Store ``(obj, entity)`` as a subscriber for the event *name*. When
        :py:meth:`fire` is called with a pattern that matches ``(obj, entity)``,
        call ``obj.on_[name](Event(name, entity, data))``.

        An event is said to "match" a subscription if the subscription has the same
        event name, and either the subscriber's entity is ``None``, or the
        subscriber's entity ``is`` the event's entity.

        You may subscribe more than once to receive the event multiple times.
        """
        if isinstance(name, Enum):
            name = name.value
        self.handlers[name].append((obj, entity))

    def remove_subscriber(self, obj, name, entity):
        """
        :param object obj: Object that is subscribed
        :param str|Enum name: Name of the event. May be a string-valued enum.
        :param entity: Entity originally subscribed to

        If you subscribed more than once with the same object/entity pair, you will
        need to unsubscribe more than once as well.
        """
        if isinstance(name, Enum):
            name = name.value
        self.handlers[name].remove((obj, entity))

    def fire(self, name, entity, data):
        """
        :param object obj: Object that is subscribed
        :param str|Enum name: Name of the event. May be a string-valued enum.
        :param entity: Entity, or ``None``.
        :param data: Arbitrary data to add to the :py:class:`Event`.

        Call all event handlers for the given *name* + *entity*, and pass *data*.
        """
        if isinstance(name, Enum):
            name = name.value
        method_name = "on_" + name.lower()
        event = Event(name, entity, data)
        for (obj, required_entity) in self.handlers[name]:
            if required_entity is None or entity is required_entity:
                method = getattr(obj, method_name)
                method(event)
            if event._is_halted or self._is_halted:
                break
        self._is_halted = False

    def stop_propagation(self):
        """
        Prevent any more handlers for the active event from firing
        """
        self._is_halted = True
