"""Multiple-producer-multiple-consumer signal-dispatching.

``dispatcher`` is the core of Louie, providing the primary API and the
core logic for the system.

Internal attributes:

- ``WEAKREF_TYPES``: Tuple of types/classes which represent weak
  references to receivers, and thus must be dereferenced on retrieval
  to retrieve the callable object
        
- ``connections``::

    { senderkey (id) : { signal : [receivers...] } }
    
- ``senders``: Used for cleaning up sender references on sender
  deletion::

    { senderkey (id) : weakref(sender) }
    
- ``senders_back``: Used for cleaning up receiver references on receiver
  deletion::

    { receiverkey (id) : [senderkey (id)...] }
"""

import os
import weakref

try:
    set
except NameError:
    from sets import Set as set, ImmutableSet as frozenset

from louie import error
from louie import robustapply
from louie import saferef
from louie.sender import Any, Anonymous
from louie.signal import All
try:
    dict.iteritems
except AttributeError:
    #python 3
    def itervalues(d):
        return iter(d.values())
    def iteritems(d):
        return iter(d.items())

else:
    #python 2
    def itervalues(d):
        return d.itervalues()
    def iteritems(d):
        return d.iteritems()


# Support for statistics.
if __debug__:
    connects = 0
    disconnects = 0
    sends = 0

    def print_stats():
        print('\n'
               'Louie connects: {}\n'
               'Louie disconnects: {}\n'
               'Louie sends: {}\n'
               '\n'.format(connects, disconnects, sends))

    if 'PYDISPATCH_STATS' in os.environ:
        import atexit
        atexit.register(print_stats)



WEAKREF_TYPES = (weakref.ReferenceType, saferef.BoundMethodWeakref)


connections = {}
senders = {}
senders_back = {}
plugins = []

def reset():
    """Reset the state of Louie.

    Useful during unit testing.  Should be avoided otherwise.
    """
    global connections, senders, senders_back, plugins
    connections = {}
    senders = {}
    senders_back = {}
    plugins = []


def connect(receiver, signal=All, sender=Any, weak=True):
    """Connect ``receiver`` to ``sender`` for ``signal``.

    - ``receiver``: A callable Python object which is to receive
      messages/signals/events.  Receivers must be hashable objects.

      If weak is ``True``, then receiver must be weak-referencable (more
      precisely ``saferef.safe_ref()`` must be able to create a
      reference to the receiver).
    
      Receivers are fairly flexible in their specification, as the
      machinery in the ``robustapply`` module takes care of most of the
      details regarding figuring out appropriate subsets of the sent
      arguments to apply to a given receiver.

      Note: If ``receiver`` is itself a weak reference (a callable), it
      will be de-referenced by the system's machinery, so *generally*
      weak references are not suitable as receivers, though some use
      might be found for the facility whereby a higher-level library
      passes in pre-weakrefed receiver references.

    - ``signal``: The signal to which the receiver should respond.
    
      If ``All``, receiver will receive all signals from the indicated
      sender (which might also be ``All``, but is not necessarily
      ``All``).
        
      Otherwise must be a hashable Python object other than ``None``
      (``DispatcherError`` raised on ``None``).
        
    - ``sender``: The sender to which the receiver should respond.
    
      If ``Any``, receiver will receive the indicated signals from any
      sender.
        
      If ``Anonymous``, receiver will only receive indicated signals
      from ``send``/``send_exact`` which do not specify a sender, or
      specify ``Anonymous`` explicitly as the sender.

      Otherwise can be any python object.
        
    - ``weak``: Whether to use weak references to the receiver.
      
      By default, the module will attempt to use weak references to
      the receiver objects.  If this parameter is ``False``, then strong
      references will be used.

    Returns ``None``, may raise ``DispatcherTypeError``.
    """
    if signal is None:
        raise error.DispatcherTypeError(
            'Signal cannot be None (receiver={0!r} sender={1!r})'
                .format(receiver, sender))
    if weak:
        receiver = saferef.safe_ref(receiver, on_delete=_remove_receiver)
    senderkey = id(sender)
    if senderkey in connections:
        signals = connections[senderkey]
    else:
        connections[senderkey] = signals = {}
    # Keep track of senders for cleanup.
    # Is Anonymous something we want to clean up?
    if sender not in (None, Anonymous, Any):
        def remove(object, senderkey=senderkey):
            _remove_sender(senderkey=senderkey)
        # Skip objects that can not be weakly referenced, which means
        # they won't be automatically cleaned up, but that's too bad.
        try:
            weak_sender = weakref.ref(sender, remove)
            senders[senderkey] = weak_sender
        except Exception:
            pass
    receiver_id = id(receiver)
    # get current set, remove any current references to
    # this receiver in the set, including back-references
    if signal in signals:
        receivers = signals[signal]
        _remove_old_back_refs(senderkey, signal, receiver, receivers)
    else:
        receivers = signals[signal] = []
    try:
        current = senders_back.get(receiver_id)
        if current is None:
            senders_back[receiver_id] = current = []
        if senderkey not in current:
            current.append(senderkey)
    except Exception:
        pass
    receivers.append(receiver)
    # Update stats.
    if __debug__:
        global connects
        connects += 1


def disconnect(receiver, signal=All, sender=Any, weak=True):
    """Disconnect ``receiver`` from ``sender`` for ``signal``.

    - ``receiver``: The registered receiver to disconnect.
    
    - ``signal``: The registered signal to disconnect.
    
    - ``sender``: The registered sender to disconnect.
    
    - ``weak``: The weakref state to disconnect.

    ``disconnect`` reverses the process of ``connect``, the semantics for
    the individual elements are logically equivalent to a tuple of
    ``(receiver, signal, sender, weak)`` used as a key to be deleted
    from the internal routing tables.  (The actual process is slightly
    more complex but the semantics are basically the same).

    Note: Using ``disconnect`` is not required to cleanup routing when
    an object is deleted; the framework will remove routes for deleted
    objects automatically.  It's only necessary to disconnect if you
    want to stop routing to a live object.
        
    Returns ``None``, may raise ``DispatcherTypeError`` or
    ``DispatcherKeyError``.
    """
    if signal is None:
        raise error.DispatcherTypeError(
            'Signal cannot be None (receiver={0!r} sender={1!r})'
                .format(receiver, sender))
    if weak:
        receiver = saferef.safe_ref(receiver)
    senderkey = id(sender)
    try:
        signals = connections[senderkey]
        receivers = signals[signal]
    except KeyError:
        raise error.DispatcherKeyError(
            'No receivers found for signal {0!r} from sender {1!r}'
                .format(signal, sender))
    try:
        # also removes from receivers
        _remove_old_back_refs(senderkey, signal, receiver, receivers)
    except ValueError:
        raise error.DispatcherKeyError(
            'No connection to receiver {0!r} '
            'for signal {1!r} from sender {2!r}'
                .format(receiver, signal, sender))
    _cleanup_connections(senderkey, signal)
    # Update stats.
    if __debug__:
        global disconnects
        disconnects += 1


def get_receivers(sender=Any, signal=All):
    """Get list of receivers from global tables.

    This function allows you to retrieve the raw list of receivers
    from the connections table for the given sender and signal pair.

    Note: There is no guarantee that this is the actual list stored in
    the connections table, so the value should be treated as a simple
    iterable/truth value rather than, for instance a list to which you
    might append new records.

    Normally you would use ``live_receivers(get_receivers(...))`` to
    retrieve the actual receiver objects as an iterable object.
    """
    try:
        return connections[id(sender)][signal]
    except KeyError:
        return []


def live_receivers(receivers):
    """Filter sequence of receivers to get resolved, live receivers.

    This is a generator which will iterate over the passed sequence,
    checking for weak references and resolving them, then returning
    all live receivers.
    """
    for receiver in receivers:
        if isinstance(receiver, WEAKREF_TYPES):
            # Dereference the weak reference.
            receiver = receiver()
        if receiver is not None:
            # Check installed plugins to make sure this receiver is
            # live.
            live = True
            for plugin in plugins:
                if not plugin.is_live(receiver):
                    live = False
                    break
            if live:
                yield receiver
            

def get_all_receivers(sender=Any, signal=All):
    """Get list of all receivers from global tables.

    This gets all receivers which should receive the given signal from
    sender, each receiver should be produced only once by the
    resulting generator.
    """
    yielded = set()
    for receivers in (
        # Get receivers that receive *this* signal from *this* sender.
        get_receivers(sender, signal),
        # Add receivers that receive *all* signals from *this* sender.
        get_receivers(sender, All),
        # Add receivers that receive *this* signal from *any* sender.
        get_receivers(Any, signal),
        # Add receivers that receive *all* signals from *any* sender.
        get_receivers(Any, All),
        ):
        # Make a copy of each list so it's immutable within the context
        # of this function, even if a receiver calls disconnect() or any
        # other function that changes a list of receivers.
        for receiver in list(receivers):
            if receiver: # filter out dead instance-method weakrefs
                try:
                    if not receiver in yielded:
                        yielded.add(receiver)
                        yield receiver
                except TypeError:
                    # dead weakrefs raise TypeError on hash...
                    pass


def send(signal=All, sender=Anonymous, *arguments, **named):
    """Send ``signal`` from ``sender`` to all connected receivers.
    
    - ``signal``: (Hashable) signal value; see ``connect`` for details.

    - ``sender``: The sender of the signal.
    
      If ``Any``, only receivers registered for ``Any`` will receive the
      message.

      If ``Anonymous``, only receivers registered to receive messages
      from ``Anonymous`` or ``Any`` will receive the message.

      Otherwise can be any Python object (normally one registered with
      a connect if you actually want something to occur).

    - ``arguments``: Positional arguments which will be passed to *all*
      receivers. Note that this may raise ``TypeError`` if the receivers
      do not allow the particular arguments.  Note also that arguments
      are applied before named arguments, so they should be used with
      care.

    - ``named``: Named arguments which will be filtered according to the
      parameters of the receivers to only provide those acceptable to
      the receiver.

    Return a list of tuple pairs ``[(receiver, response), ...]``

    If any receiver raises an error, the error propagates back through
    send, terminating the dispatch loop, so it is quite possible to
    not have all receivers called if a raises an error.
    """
    # Call each receiver with whatever arguments it can accept.
    # Return a list of tuple pairs [(receiver, response), ... ].
    responses = []
    for receiver in live_receivers(get_all_receivers(sender, signal)):
        # Wrap receiver using installed plugins.
        original = receiver
        for plugin in plugins:
            receiver = plugin.wrap_receiver(receiver)
        response = robustapply.robust_apply(
            receiver, original,
            signal=signal,
            sender=sender,
            *arguments,
            **named
            )
        responses.append((receiver, response))
    # Update stats.
    if __debug__:
        global sends
        sends += 1
    return responses


def send_minimal(signal=All, sender=Anonymous, *arguments, **named):
    """Like ``send``, but does not attach ``signal`` and ``sender``
    arguments to the call to the receiver."""
    # Call each receiver with whatever arguments it can accept.
    # Return a list of tuple pairs [(receiver, response), ... ].
    responses = []
    for receiver in live_receivers(get_all_receivers(sender, signal)):
        # Wrap receiver using installed plugins.
        original = receiver
        for plugin in plugins:
            receiver = plugin.wrap_receiver(receiver)
        response = robustapply.robust_apply(
            receiver, original,
            *arguments,
            **named
            )
        responses.append((receiver, response))
    # Update stats.
    if __debug__:
        global sends
        sends += 1
    return responses


def send_exact(signal=All, sender=Anonymous, *arguments, **named):
    """Send ``signal`` only to receivers registered for exact message.

    ``send_exact`` allows for avoiding ``Any``/``Anonymous`` registered
    handlers, sending only to those receivers explicitly registered
    for a particular signal on a particular sender.
    """
    responses = []
    for receiver in live_receivers(get_receivers(sender, signal)):
        # Wrap receiver using installed plugins.
        original = receiver
        for plugin in plugins:
            receiver = plugin.wrap_receiver(receiver)
        response = robustapply.robust_apply(
            receiver, original,
            signal=signal,
            sender=sender,
            *arguments,
            **named
            )
        responses.append((receiver, response))
    return responses
    

def send_robust(signal=All, sender=Anonymous, *arguments, **named):
    """Send ``signal`` from ``sender`` to all connected receivers catching
    errors

    - ``signal``: (Hashable) signal value, see connect for details

    - ``sender``: The sender of the signal.
    
      If ``Any``, only receivers registered for ``Any`` will receive the
      message.

      If ``Anonymous``, only receivers registered to receive messages
      from ``Anonymous`` or ``Any`` will receive the message.

      Otherwise can be any Python object (normally one registered with
      a connect if you actually want something to occur).

    - ``arguments``: Positional arguments which will be passed to *all*
      receivers. Note that this may raise ``TypeError`` if the receivers
      do not allow the particular arguments.  Note also that arguments
      are applied before named arguments, so they should be used with
      care.

    - ``named``: Named arguments which will be filtered according to the
      parameters of the receivers to only provide those acceptable to
      the receiver.

    Return a list of tuple pairs ``[(receiver, response), ... ]``

    If any receiver raises an error (specifically, any subclass of
    ``Exception``), the error instance is returned as the result for
    that receiver.
    """
    # Call each receiver with whatever arguments it can accept.
    # Return a list of tuple pairs [(receiver, response), ... ].
    responses = []
    for receiver in live_receivers(get_all_receivers(sender, signal)):
        original = receiver
        for plugin in plugins:
            receiver = plugin.wrap_receiver(receiver)
        try:
            response = robustapply.robust_apply(
                receiver, original,
                signal=signal,
                sender=sender,
                *arguments,
                **named
                )
        except Exception as err:
            responses.append((receiver, err))
        else:
            responses.append((receiver, response))
    return responses


def _remove_receiver(receiver):
    """Remove ``receiver`` from connections."""
    if not senders_back:
        # During module cleanup the mapping will be replaced with None.
        return False
    backKey = id(receiver)
    for senderkey in senders_back.get(backKey, ()):
        try:
            signals = connections[senderkey].keys()
        except KeyError:
            pass
        else:
            for signal in list(signals):
                try:
                    receivers = connections[senderkey][signal]
                except KeyError:
                    pass
                else:
                    try:
                        receivers.remove(receiver)
                    except Exception:
                        pass
                _cleanup_connections(senderkey, signal)
    try:
        del senders_back[backKey]
    except KeyError:
        pass

            
def _cleanup_connections(senderkey, signal):
    """Delete empty signals for ``senderkey``. Delete ``senderkey`` if
    empty."""
    try:
        receivers = connections[senderkey][signal]
    except Exception:
        pass
    else:
        if not receivers:
            # No more connected receivers. Therefore, remove the signal.
            try:
                signals = connections[senderkey]
            except KeyError:
                pass
            else:
                del signals[signal]
                if not signals:
                    # No more signal connections. Therefore, remove the sender.
                    _remove_sender(senderkey)


def _remove_sender(senderkey):
    """Remove ``senderkey`` from connections."""
    _remove_back_refs(senderkey)
    try:
        del connections[senderkey]
    except KeyError:
        pass
    # Senderkey will only be in senders dictionary if sender 
    # could be weakly referenced.
    try:
        del senders[senderkey]
    except Exception:
        pass


def _remove_back_refs(senderkey):
    """Remove all back-references to this ``senderkey``."""
    try:
        signals = connections[senderkey]
    except KeyError:
        signals = None
    else:
        for signal, receivers in iteritems(signals):
            for receiver in receivers:
                _kill_back_ref(receiver, senderkey)


def _remove_old_back_refs(senderkey, signal, receiver, receivers):
    """Kill old ``senders_back`` references from ``receiver``.

    This guards against multiple registration of the same receiver for
    a given signal and sender leaking memory as old back reference
    records build up.

    Also removes old receiver instance from receivers.
    """
    try:
        index = receivers.index(receiver)
        # need to scan back references here and remove senderkey
    except ValueError:
        return False
    else:
        old_receiver = receivers[index]
        del receivers[index]
        found = 0
        signals = connections.get(signal)
        if signals is not None:
            for sig, recs in iteritems(connections.get(signal, {})):
                if sig != signal:
                    for rec in recs:
                        if rec is old_receiver:
                            found = 1
                            break
        if not found:
            _kill_back_ref(old_receiver, senderkey)
            return True
        return False
        
        
def _kill_back_ref(receiver, senderkey):
    """Do actual removal of back reference from ``receiver`` to
    ``senderkey``."""
    receiverkey = id(receiver)
    senders = senders_back.get(receiverkey, ())
    while senderkey in senders:
        try:
            senders.remove(senderkey)
        except Exception:
            break
    if not senders:
        try:
            del senders_back[receiverkey]
        except KeyError:
            pass
    return True

    
