import weakref

from elisa.core.log import Loggable

class NotBoundError(Exception):
    """
    Raised when you try to unbind something that is not bound.
    """
    pass

class NotBindable(Exception):
    """
    Raised when someone tries to bind to a private attribute (beginning with an
    underscore)
    """
    pass

def get_sub_obj(obj, attribute):
    path = attribute.split('.')
    while len(path) > 1:
        e = path.pop(0)
        obj = getattr(obj, e)
    return obj, path[0]

class Bindable(Loggable):
    """A Bindable instance can have its attributes replicated into ones of
    other objects. All modifications to the attributes are directly reflected.
    """

    def __init__(self):
        self._bindings = {}
        super(Bindable, self).__init__()

    def bind(self, attribute, destination_object, destination_attribute):
        """
        Bind a local L{attribute} to L{destination_attribute} of
        L{destination_object}.

        @param attribute: local attribute to bind
        @type attribute:  str

        @raises NotBindable: when you try to bind a private attribute
                            (beginning with an underscore)
        """


        if attribute.startswith('_'):
            raise NotBindable("You can't bind to %s as it is an private"
                              " attribute." % attribute)

        self.debug("binding %s to %s of %s" %
                    (attribute, destination_attribute, destination_object))

        weak_destination = weakref.ref(destination_object)
        binding = (weak_destination, destination_attribute)

        self._bindings.setdefault(attribute, []).append(binding)

        # set the destination attribute to the current value of attribute
        try:
            obj, attr = get_sub_obj(destination_object, destination_attribute)
        except AttributeError, e:
            self.debug("could not get sub attribute: %s" % e)
            return

        if hasattr(self, attribute):
            value = getattr(self, attribute)
            setattr(obj, attr, value)
        else:
            if hasattr(obj, attr):
                delattr(obj, attr)

    def unbind(self, attribute, destination_object, destination_attribute):
        """
        Remove the binding of L{attribute} to L{destination_attribute} of
        L{destination_object}. 

        If you want to unbind the whole destination_object you should use the
        L{unbind_object} method instead.

        @param attribute: local attribute to unbind
        @type attribute:  str

        @raises NotBoundError: when you try to unbind something that is not
                               bound
        """
        try:
            bindings = self._bindings[attribute]
        except KeyError:
            raise NotBoundError()

        self.debug("unbinding %s to %s of %s" %
                    (attribute, destination_attribute, destination_object))

        for index, (weak_dest, dest_attribute) in enumerate(bindings):
            if (weak_dest(), dest_attribute) == \
                  (destination_object, destination_attribute):
                break
        else:
            raise NotBoundError()

        del bindings[index]

    def unbind_object(self, destination_object):
        """
        Remove all the bindings you have for a certain L{destination_object}.
        """
        self.debug("unbinding object %s" % destination_object)

        for attribute, bindings in self._bindings.iteritems():
            refs = [b[0]() for b in bindings]
            deleted = 0
            for index, (weak_destination, dest_attribute) in enumerate(list(bindings)):
                if weak_destination() == destination_object:
                    del bindings[index - deleted]
                    deleted += 1

    def __setattr__(self, attribute, new_value):
        super(Bindable, self).__setattr__(attribute, new_value)

        if attribute.startswith('_'):
            return

        try:
            bindings = self._bindings[attribute]
        except KeyError:
            # attribute not bound
            return

        for binding in bindings:
            weak_destination, destination_attribute = binding
            destination_object = weak_destination()
            obj, attr = get_sub_obj(destination_object, destination_attribute)
            setattr(obj, attr, new_value)

    def __delattr__(self, attribute):
        super(Bindable, self).__delattr__(attribute)

        try:
            bindings = self._bindings[attribute]
        except KeyError:
            # attribute not bound
            return

        for binding in bindings:
            weak_destination, destination_attribute = binding
            destination_object = weak_destination()
            
            obj, attr = get_sub_obj(destination_object, destination_attribute)
            delattr(obj, attr)
