# Elisa - Home multimedia server
# Copyright (C) 2006-2008 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 Elisa with Fluendo's plugins.
#
# The GPL part of Elisa is also available under a commercial licensing
# agreement from Fluendo.
# See "LICENSE.Elisa" 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>'


import threading

from elisa.core import log
from elisa.base_components.message import Message
from elisa.core.utils import threadsafe_list
from twisted.internet import reactor

class Bus(log.Loggable):
    """
    Python objects can register callbacks with the bus and be called when
    L{elisa.base_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 = {}

        # threading lock for operations on self._callbacks
        self._callbacks_lock = threading.Lock()

    def _dispatch_queued_msgs(self):
        # dispatch pending messages
        while self._queue:
            message, sender = self._queue.pop()
            reactor.callFromThread(self._dispatch, message, sender)

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

        Queued messages are dispatched inside a separate thread.
        """
        self.info("Starting")
        self._running = True
        self._dispatch_queued_msgs()

    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 (inside a thread) if the L{Bus} is
        running. Otherwise the message is locally queued until the
        L{Bus} starts.

        MT safe.

        @param message: the message to send
        @type message:  L{elisa.base_components.message.Message}
        @param sender:  the sender object. None by default. Will be passed to
                        receiver callbacks.
        @type sender:   object
        """
        assert isinstance(message, Message), message
        self.debug("Sending message %r", message)

        if self._running:
            reactor.callFromThread(self._dispatch, message, sender)
        else:
            self._queue.append((message, sender))
    
    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.

        MT safe.

        @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)

        try:
            self._callbacks_lock.acquire()
            self._callbacks[callback] = message_types
        finally:
            self._callbacks_lock.release()

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

        MT safe.

        @param callback: the callback to register
        @type callback:  callable
        """
        try:
            self._callbacks_lock.acquire()
            if callback in self._callbacks:
                del self._callbacks[callback]
        finally:
            self._callbacks_lock.release()

    def _dispatch(self, message, sender):
        """Dispatch messages to registered callbacks and empty the
        message queue.
        """
        dispatched = False
        try:
            self._callbacks_lock.acquire()
            for callback, mfilter in self._callbacks.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)
                    callback(message, sender)
                    dispatched = True
        finally:
            self._callbacks_lock.release()

        if not dispatched:
            self.debug("Undispatched message: %r", message)
        return dispatched
    
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
