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


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 = []

    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):
        """
        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

        @exception TypeError: raised if the receiver is not callable
        """
        if callable(receiver):
            if receiver not in self._receivers:
                self._receivers.append(receiver)
        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

        @exception Exception: raised if the receiver is not connected
        """
        try:
            self._receivers.remove(receiver)
        except:
            raise Exception("%s not connected to the signal" % 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

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

        for receiver in self._receivers:
            receiver(*args, **kw)

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

        @exception WrongArgument: Raised 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')
