"""Dependency Injection logic for metsrw.

Here a global singleton feature broker is instantiated. By providing features
(i.e., dependencies) to the feature broker one can alter, for example, the
PREMIS dependencies used by metsrw's ``FSEntry`` class when reading and writing
PREMIS XML. Here we provide default classes for handling PREMIS using classes
defined in plugins.premisrw::

    >>> feature_broker = FeatureBroker()
    >>> feature_broker.provide('premis_object_class', premisrw.PREMISObject)
    >>> feature_broker.provide('premis_event_class', premisrw.PREMISEvent)
    >>> feature_broker.provide('premis_agent_class', premisrw.PREMISAgent)

However, the PREMIS dependencies can be altered by providing other features
with the same names, e.g.,::

    >>> feature_broker.provide('premis_object_class', MyOtherPREMISObjectClass)

See http://code.activestate.com/recipes/413268/
"""

from .plugins import premisrw


class FeatureBroker(object):
    """Feature broker allows for the provisioning of features. These features
    are dependencies that can be injected. Usage::

        >>> feature_broker = FeatureBroker()
        >>> feature_broker.provide('premis_object_class', premisrw.PREMISObject)
    """

    def __init__(self, allow_replace=True):
        self.providers = {}
        self.allow_replace = allow_replace

    def provide(self, feature_name, provider, *args, **kwargs):
        """Provide a feature named ``feature_name`` using the provider object
        ``provider`` and any arguments (``args``, ``kwargs``) needed by the
        provider if it is callable.
        """
        if not self.allow_replace:
            assert feature_name not in self.providers, (
                'Duplicate feature: {!r}'.format(feature_name))
        if callable(provider) and not isinstance(provider, type):
            self.providers[feature_name] = lambda: provider(*args, **kwargs)
        else:
            self.providers[feature_name] = lambda: provider

    def clear(self):
        self.providers.clear()

    def __len__(self):
        return len(self.providers)

    def __getitem__(self, feature_name):
        try:
            provider = self.providers[feature_name]
        except KeyError:
            raise KeyError('Unknown feature named {!r}'.format(feature_name))
        return provider()


def set_feature_broker_to_default_state(fb):
    fb.clear()
    fb.provide('premis_object_class', premisrw.PREMISObject)
    fb.provide('premis_event_class', premisrw.PREMISEvent)
    fb.provide('premis_agent_class', premisrw.PREMISAgent)


feature_broker = FeatureBroker()  # global singleton feature broker
set_feature_broker_to_default_state(feature_broker)


class Dependency(object):
    """Non-overriding descriptor for declaring required dependencies in metsrw
    classes. In the following example usage the ``FSEntry`` class is declaring
    a dependency on a feature named 'premis_object_class' which is a class and
    which has methods ``fromtree`` and ``serialize``::

        >>> from .di import is_class, has_methods, Dependency
        >>> class FSEntry(object):
        ...     premis_object_class = Dependency(
        ...         'premis_object_class',
        ...         has_methods('serialize'),
        ...         has_class_methods('fromtree'),
        ...         is_class)

    """

    def __init__(self, dependency_name, *assertions):
        self.dependency_name = dependency_name
        self.assertions = assertions

    def __get__(self, instance, owner):
        obj = feature_broker[self.dependency_name]
        for assertion in self.assertions:
            assert assertion(obj), (
                'The value {!r} of {!r} does not match the specified'
                ' criteria'.format(obj, self.dependency_name))
        return obj


# ==============================================================================
#   Assertions for declaring dependencies using Dependency
# ==============================================================================

def has_class_methods(*class_method_names):
    """Return a test function that, when given a class, returns ``True`` if that
    class has all of the class methods in ``class_method_names``. If an object
    is passed to the test function, check for the class methods on its
    class.
    """
    def test(cls):
        if not isinstance(cls, type):
            cls = type(cls)
        for class_method_name in class_method_names:
            try:
                class_method = getattr(cls, class_method_name)
                if class_method.__self__ is not cls:
                    return False
            except AttributeError:
                return False
        return True
    return test


def has_methods(*method_names):
    """Return a test function that, when given an object (class or an
    instance), returns ``True`` if that object has all of the (regular) methods
    in ``method_names``. Note: this is testing for regular methods only and the
    test function will correctly return ``False`` if an instance has one of the
    specified methods as a classmethod or a staticmethod. However, it will
    incorrectly return ``True`` (false positives) for classmethods and
    staticmethods on a *class*.
    """
    def test(obj):
        for method_name in method_names:
            try:
                method = getattr(obj, method_name)
            except AttributeError:
                return False
            else:
                if not callable(method):
                    return False
                if not isinstance(obj, type):
                    try:
                        # An instance method is a method type with a __self__
                        # attribute that references the instance.
                        if method.__self__ is not obj:
                            return False
                    except AttributeError:
                        return False
        return True
    return test


is_class = lambda x: isinstance(x, type)

@@ -5953,24 +5953,28 @@
  test%0A%0A%0A
+def 
 is_class
  = lambd
@@ -5969,20 +5969,25 @@
 lass
- = lambda x:
+(obj):%0A    return
  isi
@@ -5994,17 +5994,19 @@
 nstance(
-x
+obj
 , type)%0A

fix E731 do not assign a lambda expression, use a def