# Moovida - Home multimedia server
# Copyright (C) 2006-2009 Fluendo, S.A. (www.fluendo.com).
# All rights reserved.
#
# This file is available under one of two license agreements.
#
# This file is licensed under the GPL version 3.
# See "LICENSE.GPL" in the root of this distribution including a special
# exception to use Moovida with Fluendo's plugins.
#
# The GPL part of Moovida is also available under a commercial licensing
# agreement from Fluendo.
# See "LICENSE.Moovida" in the root directory of this distribution package
# for details on that license.

__maintainer__ = 'Philippe Normand <philippe@fluendo.com>'
__maintainer2__ = 'Florian Boucault <florian@fluendo.com>'


from elisa.core import log
from elisa.core.components.message import Message
from elisa.core.utils import threadsafe_list, defer
from twisted.internet import reactor, task

class Bus(log.Loggable):
    """
    Python objects can register callbacks with the bus and be called when
    L{elisa.core.components.massage.Message}s are sent by other objects.

    Here is a simple example::

        bus = Bus()

        def my_cb(message, sender):
            print 'Got message %r from %r' % (message, sender)

        bus.register(my_cb)

        bus.send_message(Message())


    Messages dispatching empties the message queue and call the registered
    callbacks. Messages filtering is also supported, just pass a Message
    type class to bus.register(), like this::

        bus.register(my_cb, Message)

    You can filter on multiple Message types by supplying a list to
    bus.register()::

        bus.register(my_cb, (FooMessage, DataMessage))

    @ivar callbacks: registered callbacks
    @type callbacks: dict, keys are callable objects and values are
                     Message types lists
    """

    def __init__(self):
        """Initialize the Message queue and the callbacks dictionary.
        """
        log.Loggable.__init__(self)
        self.debug("Creating")

        self._queue = threadsafe_list.ThreadsafeList()

        self._running = False

        # callbacks dictionary mapping callables to Message type lists
        self._callbacks = {}

    def deferred_dispatch(self):
        """
        Dequeue and dispatch each queued message. This method is
        called when the bus starts, to dispatch all the messages that
        were sent before startup.

        This method can also be used to force dispatch on a bus you
        don't want to start, for instance for functional test purpose.

        @returns: a deferred fired when all messages have been dispatched
        @rtype:   L{twisted.internet.defer.Deferred}
        """
        def iterate():
            while self._queue:
                message, sender = self._queue.pop()
                yield self._dispatch(message, sender)

        return task.coiterate(iterate())

    def start(self):
        """
        Start message dispatching: once started, messages sent over the bus
        are guaranteed to be dispatched.

        @returns: a deferred fired when all queued messages have been
                  dispatched
        @rtype:   L{elisa.core.utils.defer.Deferred}
        """
        self.info("Starting")
        self._running = True
        return self.deferred_dispatch()

    def stop(self):
        """
        Stop message dispatching: messages sent over the bus will not be
        dispatched automatically anymore, they will be locally queued.
        """
        self.info("Stopping")
        self._running = False

    def send_message(self, message, sender=None):
        """Send a message over the bus. The message is automatically
        dispatched if the L{Bus} is running. Otherwise the message is
        locally queued until the L{Bus} starts.

        @param message: the message to send
        @type message:  L{elisa.core.components.message.Message}
        @param sender:  the sender object. None by default. Will be passed to
                        receiver callbacks.
        @type sender:   object
        @returns:       a deferred fired when the message have been
                        dispatched or queued
        @rtype:         L{elisa.core.utils.defer.Deferred}
        """
        assert isinstance(message, Message), message
        self.debug("Sending message %r", message)

        if self._running:
            dfr = self._dispatch(message, sender)
        else:
            dfr = defer.maybeDeferred(self._queue.append, (message, sender))
        return dfr

    def register(self, callback, *message_types):
        """
        Register a new callback with the bus. The given callback will be
        called when a message of one of the given types is dispatched on the
        bus.

        @param callback:      the callback to register
        @type callback:       callable
        @param message_types: Message types to filter on
        @type message_types:  type or list of types
        """
        if not message_types:
            message_types = (Message,)

        self.debug("Registering callback %r for %r message types",
                   callback, message_types)

        self._callbacks[callback] = message_types

    def unregister(self, callback):
        """Unregister a callback from the bus.

        @param callback: the callback to register
        @type callback:  callable
        """
        if callback in self._callbacks:
            del self._callbacks[callback]

    def _dispatch(self, message, sender):
        """Dispatch one message to registered callbacks.

        @param message: the message to dispatch
        @type message:  L{elisa.core.components.message.Message}
        @param sender:  the sender object. None by default. Will be passed to
                        receiver callbacks.
        @type sender:   object
        @returns:       a deferred fired when the message have been
                        dispatched
        @rtype:         L{twisted.internet.defer.Deferred}
        """

        def got_failure(failure, message, cb_name):
            self.warning("Exception happened during dispatch of %r to %s: %s",
                         message, cb_name, failure.getTraceback())

        def iterate():
            dispatched = False
            for callback, mfilter in self._callbacks.copy().iteritems():
                if isinstance(message, mfilter):
                    try:
                        cb_name = "%s.%s" % (callback.im_class.__name__,
                                             callback.__name__)
                    except AttributeError:
                        cb_name = callback.__name__
                    self.debug("Dispatching message %r to %r", message,
                               cb_name)
                    dispatched = True
                    dfr = defer.maybeDeferred(callback, message, sender)
                    dfr.addErrback(got_failure, message, cb_name)
                    yield dfr

            if not dispatched:
                self.debug("Undispatched message: %r", message)

        return task.coiterate(iterate())

def bus_listener(bus, *message_types):
    """ Utility decorator to simply register a function or method on
    the message bus.
    """
    def decorator(func):
        bus.register(func, *message_types)

    return decorator
