# -*- coding: utf-8 -*-
# Moovida - Home multimedia server
# Copyright (C) 2006-2009 Fluendo Embedded S.L. (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__ = 'Florian Boucault <florian@fluendo.com>'
__maintainer2__ = 'Philippe Normand <philippe@fluendo.com>'

import weakref

class WrongArgument(Exception):
    """ Exception raised by Signal when the developer tries to emit
    wrong types values with the Signal.

    @ivar arg:           the value of the wrongly typed object
    @type arg:           object
    @ivar expected_type: the expcected type for the argument, given at
                         Signal creation
    @type expected_args: type
    @ivar position:      position of the argument in the arguments list, if it's
                         not a keyword argument
    @type position:      int or None
    """

    def __init__(self, arg, expected_type, position):
        Exception.__init__(self)
        self.arg = arg
        self.expected_type = expected_type
        self.position = position

    def __str__(self):
        """ Textual representation of the Exception

        @rtype: string
        """
        if self.position is not None:
            position = "at position %s" % self.position
        else:
            position = "as keyword argument"
        msg = "Expected an %r %s, got %r (%r) instead."
        msg = msg % (self.expected_type.__name__, position,
                     self.arg, type(self.arg))
        return msg

class Signal(object):
    """
    A Signal is a 1-to-many communication system.
    It provides a way for two objects to communicate without knowing
    about each other. They only have to know about the signal object. Receivers
    objects connect to the signal and emitters can trigger the emission of the
    signal so that it is sent to all the receivers. Signal emission is
    synchronous, that is, the receivers are immediatly called upon emission,
    one after the other.

    WARNING:
        - a Signal is *not* thread-safe
        - the receivers are called in the emitting thread
        - receivers calling is not ordered

    TODO:
        - might be useful to send a reference of the sender
        - maybe add static extra data at connect time
        - think about implementing return values support
        - think about weak references for receivers
            http://docs.python.org/lib/module-weakref.html
        - think about integrating it with Twisted and making signals
            deferred in threads

    """


    def __init__(self, name, *args_types, **kw_types):
        """
        Initialize a signal with the signature of receivers that can connect
        to it.

        @param name:       name of the signal. Not unique, but developer
                           friendly
        @type name:        string
        @param args_types: arguments types of the receivers that can connect
                           to the signal
        @type args_types:  tuple of types
        @param kw_types:   arguments types of the receivers that can connect
                           to the signal
        @type kw_types:    dictionary of types
        """
        self._name = name
        self._args_types = args_types
        self._kw_types = kw_types
        self._receivers = []
        self._arguments = weakref.WeakKeyDictionary()

    def __repr__(self):
        """ Textual representation of the Signal

        @returns: information about the signal and its receivers
        @rtype:   string
        """
        r = '<Signal %s with %d receivers>' % (self._name, len(self._receivers))
        return r

    def connect(self, receiver, *args, **kw):
        """
        Connect a receiver to the signal. It will be called when the signal
        is emitted.

        @param receiver:      object to be called when the signal is emitted
        @type receiver:       callable

        @param args:        parameters to send to the receiver when calling
        @param kw:            keywords to send to the receiver when calling

        @raise TypeError: if the receiver is not callable
        """
        if callable(receiver):
            if receiver not in [receiver() for receiver in self._receivers]:
                self._receivers.append( weakref.ref(receiver) )
                self._arguments[receiver] = (args, kw)
        else:
            raise TypeError("Receiver is not a callable")

    def disconnect(self, receiver):
        """
        Disconnect a receiver from the signal.

        @param receiver:      object to be disconnected
        @type receiver:       callable

        @raise IndexError: raised if the receiver is not in the list of
                           receivers
        """
        index = [ current_receiver() for current_receiver in
                                        self._receivers].index(receiver)
        self._receivers.pop(index)
        self._arguments.pop(receiver)

    def emit(self, *args, **kw):
        """
        Call all the connected receivers. It avoids calling the
        neutralized ones. Since receivers calling is not ordered, one should be
        careful with the potential side effects in the receivers.

        @param args:              arguments passed to the receivers
        @type args:               tuple
        @param kw:                arguments passed to the receivers
        @type kw:                 dictionary

        @raise WrongArgument: if one of the arguments is of the wrong type
        """
        self._check_args_types(args, kw)

        # filter out the dropped ones
        filter(lambda receiver: receiver() != None, self._receivers)

        for receiver in self._receivers:
            real_receiver = receiver()
            if real_receiver is None:
                # should happen rarely but can happen: the receiver was
                # removed AFTER we filtered...
                continue

            # try to get arguments and keyword
            this_args, this_kw = self._arguments.get(real_receiver, (None, None) )

            # create our copy of the data
            send_args = args
            send_kw = {}

            if this_args is not None and len(args) > 0:
                # if there are argument then merge it
                send_args += this_args

            if this_kw is not None:
                # if there are parameters, merge them
                send_kw.update(this_kw)

            # needs to be done AFTER maybe merges because we want the same
            # ones to get overwritten!
            send_kw.update(kw)

            # FIXME: other receivers do not get proceeded if one fails
            real_receiver(*send_args, **send_kw)

    def _check_args_types(self, args, kw):
        """ Check the types of arguments and keywords comply with
        types specifications given at Signal creation.

        @raise WrongArgument: if one of the arguments is of the wrong type
        """
        position = 0
        for arg in args:
            expected_type = self._args_types[position]
            if not isinstance(arg, expected_type):
                raise WrongArgument(arg, expected_type, position)
            position += 1

        for key, value in self._kw_types.iteritems():
            if key in kw and not isinstance(kw[key], value):
                raise WrongArgument(kw[key], value, None)

#if __name__ == "__main__":
#    def test(number, text, foo=0):
#        print "test %d %s foo=%s" % (number, text, foo)
#
#    signal1 = Signal('test-signal', int, object, foo=int)
#    signal1.connect(test)
#    signal1.emit(42, "boudiou",foo='bar')
