# -*- coding: utf-8; fill-column: 76 -*-

import gc

__all__ = ["signal", "Signal"]


class attrgetter:
    __slots__ = ('_attrs', '_call')

    def __init__(self, attr, *attrs):
        if not attrs:
            if not isinstance(attr, str):
                raise TypeError('attribute name must be a string')
            self._attrs = (attr,)
            names = attr.split('.')

            def func(obj):
                for name in names:
                    obj = getattr(obj, name)
                return obj

            self._call = func
        else:
            self._attrs = (attr,) + attrs
            getters = tuple(map(attrgetter, self._attrs))

            def func(obj):
                return tuple(getter(obj) for getter in getters)

            self._call = func

    def __call__(self, obj):
        return self._call(obj)

    def __repr__(self):
        return '%s.%s(%s)' % (self.__class__.__module__,
                              self.__class__.__qualname__,
                              ', '.join(map(repr, self._attrs)))


class ReferenceType(object):

    def __new__(cls, *args, **kwargs):
        return super(ReferenceType, cls).__new__(cls)

    def __call__(self, *args, **kwargs):
        return

    def __init__(self, *args, **kwargs):
        self.params = dict()

    def __setitem__(self, key, value):
        self.__dict__[key] = value

    def __getitem__(self, key):
        if key not in self.__dict__:
            self.__dict__[key] = None
        return self.__dict__[key]

    __callback__ = property(lambda self: object(), lambda self, v: None, lambda self: None)  # default


ref = ReferenceType


class KeyedRef(ref):
    __slots__ = "key",

    def __new__(type, ob, callback, key):
        self = ref.__new__(type, ob, callback)
        self.key = key
        return self

    def __init__(self, ob, callback, key):
        super().__init__(ob, callback)


class WeakValueDictionary(dict):
    def __init__(self, dict=None):
        self.data = {}

        def remove(k, selfref=ref(self)):
            self = selfref()
            if self is not None:
                if self._iterating:
                    self._pending_removals.append(k)
                else:
                    del self.data[k]

        self._remove = remove
        self._pending_removals = []
        self._iterating = set()
        self._dirty_len = False
        if dict is not None:
            self.update(dict)

    def _commit_removals(self):
        gc.collect()

    def __getitem__(self, key):
        if self._pending_removals:
            self._commit_removals()
        o = self.data[key]()
        if o is None:
            raise KeyError(key)
        else:
            return o

    def __setitem__(self, key, value):
        if self._pending_removals:
            self._commit_removals()
        self.data[key] = KeyedRef(value, self._remove, key)

    def get(self, key, default=None):
        if self._pending_removals:
            self._commit_removals()
        try:
            wr = self.data[key]
        except KeyError:
            return default
        else:
            o = wr()
            if o is None:
                return default
            else:
                return o

    def update(self, other=None, **kwargs):
        if self._pending_removals:
            self._commit_removals()
        d = self.data
        if other is not None:
            if not hasattr(other, "items"):
                other = dict(other)
            for key, o in other.items():
                d[key] = KeyedRef(o, self._remove, key)
        for key, o in kwargs.items():
            d[key] = KeyedRef(o, self._remove, key)


try:
    callable
except NameError:
    def callable(object):
        return hasattr(object, '__call__')

get_self = attrgetter('__self__')
get_func = attrgetter('__func__')


class BoundMethodWeakref(object):
    _all_instances = WeakValueDictionary()

    def __new__(cls, target, on_delete=None, *arguments, **named):
        key = cls.calculate_key(target)
        current = cls._all_instances.get(key)
        if current is not None:
            current.deletion_methods.append(on_delete)
            return current
        else:
            base = super(BoundMethodWeakref, cls).__new__(cls)
            cls._all_instances[key] = base
            base.__init__(target, on_delete, *arguments, **named)
            return base

    def __init__(self, target, on_delete=None):
        def remove(self=self):
            methods = self.deletion_methods[:]
            del self.deletion_methods[:]
            try:
                del self.__class__._all_instances[self.key]
            except KeyError:
                pass
            for function in methods:
                try:
                    if callable(function):
                        function(self)
                except Exception as e:
                    print('Exception during saferef %s '
                          'cleanup function %s: %s' % (self, function, e))

        self.deletion_methods = [on_delete]
        self.key = self.calculate_key(target)
        im_self = get_self(target)
        im_func = get_func(target)
        self.weak_self = ref(im_self, remove)
        self.weak_func = ref(im_func, remove)
        self.self_name = str(im_self)
        self.func_name = str(im_func.__name__)

    def calculate_key(cls, target):
        return (id(get_self(target)), id(get_func(target)))

    calculate_key = classmethod(calculate_key)

    def __str__(self):
        return "%s(%s.%s)" % (
            self.__class__.__name__,
            self.self_name,
            self.func_name,
        )

    __repr__ = __str__

    def __call__(self):
        target = self.weak_self()
        if target is not None:
            function = self.weak_func()
            if function is not None:
                return function.__get__(target)
        return None


class defaultdict(object):
    params = dict()

    def __init__(self, default_factory=None, *a, **kw):
        super().__init__(*a, **kw)
        self.default_factory = default_factory

    def __getitem__(self, key):
        try:
            result = self.params[key]
            return result
        except KeyError:
            return self.__missing__(key)

    def __setitem__(self, key, value):
        try:
            self.params[key] = set(value)
        except TypeError:
            return "Type error"

    def __missing__(self, key):
        if self.default_factory is None:
            raise KeyError(key)
        self.params[key] = value = self.default_factory()
        return value

    def __reduce__(self):
        if self.default_factory is None:
            args = tuple()
        else:
            args = self.default_factory,
        return type(self), args, None, None, self.items()

    def copy(self):
        return self.__copy__()

    def pop(self, key, default):
        return self.params.pop(key, default)

    def values(self):
        return self.params.values()

    def items(self):
        return self.params.items()

    def clear(self):
        return self.params.clear()

    def __copy__(self):
        return type(self)(self.default_factory, self)

    def __repr__(self):
        return 'defaultdict(%s, %s)' % (self.default_factory,
                                        dict.__repr__(self))

    def __contains__(self, key):
        if key in self.params:
            return True
        else:
            return False


class _symbol(object):

    def __init__(self, name):
        self.__name__ = self.name = name

    def __reduce__(self):
        return symbol, (self.name,)

    def __repr__(self):
        return self.name


_symbol.__name__ = 'symbol'


class symbol(object):
    symbols = {}

    def __new__(cls, name):
        try:
            return cls.symbols[name]
        except KeyError:
            return cls.symbols.setdefault(name, _symbol(name))


try:
    text = (str, unicode)
except NameError:
    text = str


def hashable_identity(obj):
    if hasattr(obj, '__func__'):
        return (id(obj.__func__), id(obj.__self__))
    elif isinstance(obj, text):
        return obj
    else:
        return id(obj)


WeakTypes = (ref, BoundMethodWeakref)


class annotatable_weakref(ref):
    """A weakref.ref that supports custom instance attributes."""


def reference(object, callback=None, **annotations):
    if callable(object):
        weak = callable_reference(object, callback)
    else:
        weak = annotatable_weakref(object, callback)
    for key, value in annotations.items():
        setattr(weak, key, value)
    return weak


def callable_reference(object, callback=None):
    if hasattr(object, 'im_self') and object.im_self is not None:
        return BoundMethodWeakref(target=object, on_delete=callback)
    elif hasattr(object, '__self__') and object.__self__ is not None:
        return BoundMethodWeakref(target=object, on_delete=callback)
    return annotatable_weakref(object, callback)


ANY = symbol('ANY')
ANY.__doc__ = 'Token for "any sender".'
ANY_ID = 0


class Signal(object):
    ANY = ANY

    def receiver_connected(self):
        return Signal(doc="Emitted after a receiver connects.")

    def receiver_disconnected(self):
        return Signal(doc="Emitted after a receiver disconnects.")

    def __init__(self, doc=None):
        self.receivers = {}
        self._by_receiver = defaultdict(default_factory=set)
        self._by_sender = defaultdict(default_factory=set)
        self._weak_senders = {}

    def __repr__(self):
        return "<%s at %#x>" % (self.__class__.__name__, id(self))

    def connect(self, receiver, sender=ANY, weak=False):
        receiver_id = hashable_identity(receiver)
        weak = False
        receiver_ref = receiver
        if sender is ANY:
            sender_id = ANY_ID
        else:
            sender_id = hashable_identity(sender)
        self.receivers.setdefault(receiver_id, receiver_ref)
        self._by_sender[sender_id].add(receiver_id)
        self._by_receiver[receiver_id].add(sender_id)
        del receiver_ref

        if sender is not ANY and sender_id not in self._weak_senders:
            try:
                sender_ref = reference(sender, self._cleanup_sender)
                sender_ref.sender_id = sender_id
            except TypeError:
                pass
            else:
                self._weak_senders.setdefault(sender_id, sender_ref)
                del sender_ref

        if ('receiver_connected' in self.__dict__ and
                self.receiver_connected.receivers):
            try:
                self.receiver_connected.send(self,
                                             receiver=receiver,
                                             sender=sender,
                                             weak=weak)
            except:
                self.disconnect(receiver, sender)
                raise
        if receiver_connected.receivers and self is not receiver_connected:
            try:
                receiver_connected.send(self,
                                        receiver_arg=receiver,
                                        sender_arg=sender,
                                        weak_arg=weak)
            except:
                self.disconnect(receiver, sender)
                raise
        return receiver

    def connect_via(self, sender=ANY, weak=False):
        def decorator(fn):
            self.connect(fn, sender, weak)
            return fn

        return decorator

    def send(self, *sender, **kwargs):
        if len(sender) == 0:
            sender = None
        elif len(sender) > 1:
            raise TypeError('send() accepts only one positional argument, '
                            '%s given' % len(sender))
        else:
            sender = sender[0]
        if not self.receivers:
            return []
        else:
            return [(receiver, receiver(sender, **kwargs))
                    for receiver in self.receivers_for(sender)]

    def has_receivers_for(self, sender):
        if not self.receivers:
            return False
        if self._by_sender[ANY_ID]:
            return True
        if sender is ANY:
            return False
        return hashable_identity(sender) in self._by_sender

    def receivers_for(self, sender):
        if self.receivers:
            sender_id = hashable_identity(sender)
            if sender_id in self._by_sender:
                ids = (self._by_sender[ANY_ID] |
                       self._by_sender[sender_id])
            else:
                ids = self._by_sender[ANY_ID].copy()
            for receiver_id in ids:
                receiver = self.receivers.get(receiver_id)
                if receiver is None:
                    continue
                if isinstance(receiver, WeakTypes):
                    strong = receiver()
                    if strong is None:
                        self._disconnect(receiver_id, ANY_ID)
                        continue
                    receiver = strong
                yield receiver

    def disconnect(self, receiver, sender=ANY):
        if sender is ANY:
            sender_id = ANY_ID
        else:
            sender_id = hashable_identity(sender)
        receiver_id = hashable_identity(receiver)
        self._disconnect(receiver_id, sender_id)

        if ('receiver_disconnected' in self.__dict__ and
                self.receiver_disconnected.receivers):
            self.receiver_disconnected.send(self,
                                            receiver=receiver,
                                            sender=sender)

    def _disconnect(self, receiver_id, sender_id):
        if sender_id == ANY_ID:
            if self._by_receiver.pop(receiver_id, False):
                for bucket in self._by_sender.values():
                    bucket.discard(receiver_id)
            self.receivers.pop(receiver_id, None)
        else:
            self._by_sender[sender_id].discard(receiver_id)
            self._by_receiver[receiver_id].discard(sender_id)

    def _cleanup_receiver(self, receiver_ref):
        self._disconnect(receiver_ref.receiver_id, ANY_ID)

    def _cleanup_sender(self, sender_ref):
        sender_id = sender_ref.sender_id
        assert sender_id != ANY_ID
        self._weak_senders.pop(sender_id, None)
        for receiver_id in self._by_sender.pop(sender_id, ()):
            self._by_receiver[receiver_id].discard(sender_id)

    def _cleanup_bookkeeping(self):
        for mapping in (self._by_sender, self._by_receiver):
            for _id, bucket in list(mapping.items()):
                if not bucket:
                    mapping.pop(_id, None)

    def _clear_state(self):
        self._weak_senders.clear()
        self.receivers.clear()
        self._by_sender.clear()
        self._by_receiver.clear()


receiver_connected = Signal("""\
Sent by a :class:`Signal` after a receiver connects.
""")


class NamedSignal(Signal):

    def __init__(self, name, doc=None):
        Signal.__init__(self, doc)
        self.name = name

    def __repr__(self):
        base = Signal.__repr__(self)
        return "%s; %r>" % (base[:-1], self.name)


class Namespace(dict):

    def signal(self, name, doc=None):
        try:
            return self[name]
        except KeyError:
            return self.setdefault(name, NamedSignal(name, doc))


signal = Namespace().signal
if __name__ == '__main__':
    dice_roll = signal('dice_roll')


    def odd_subscriber(sender, **kwargs):
        print("Observed dice roll %r. %r" % (sender, kwargs))


    dice_roll.connect(odd_subscriber, sender=0x03)
    result = dice_roll.send(3)
