"""This module defines util classes
which allow to execute operations
for each element of a list of objects of the same class.
"""

import abc
import collections.abc
import functools


class SubclassingMeta(abc.ABCMeta):
    """Metaclass which creates a subclass for each instance.

    As decorators only work
    if the class object contains the declarations,
    we need to create a subclass for each different type
    if we want to dynamically use them.
    """

    SUBCLASS_IDENTIFIER = "__subclassed__"

    def __call__(cls, *args, **kwargs):
        if hasattr(cls, SubclassingMeta.SUBCLASS_IDENTIFIER):
            return super().__call__(*args, **kwargs)

        subclass = type(
            cls.__name__,
            (cls,),
            {
                SubclassingMeta.SUBCLASS_IDENTIFIER: SubclassingMeta.SUBCLASS_IDENTIFIER
            },
        )
        return subclass(*args, **kwargs)


class BatchList(collections.abc.MutableSequence, metaclass=SubclassingMeta):
    """BatchList provides the execution of methods and field access
    for each element of a list of instances of the same class
    in a similar way to one of these instances it would.
    """

    __attributes_declared = False

    class BatchMember:
        def __init__(self, outer, name):
            """
            Args:
                outer (BatchList): Outer class instance
            """
            self.outer = outer
            self.name = name

    class BatchField(BatchMember):
        def __get__(self, owner_instance, owner_class):
            return BatchList(
                [
                    instance.__getattribute__(self.name)
                    for instance in self.outer
                ]
            )

        def __set__(self, owner_instance, value):
            for instance in self.outer:
                instance.__setattr__(self.name, value)

        def __delete__(self, instance):
            for instance in self.outer:
                instance.__delattr__(self.name)

    class BatchMethod(BatchMember):
        def __call__(self, *args, **kwargs):
            return BatchList(
                [
                    instance.__getattribute__(self.name)(*args, **kwargs)
                    for instance in self.outer
                ]
            )

    def __init__(self, collection: list):
        """
        Args:
            collection (List): List of target instances
        """
        self.__collection = collection.copy()
        self.__initialized = False
        self.__type = None
        self.entered = False
        self.__attributes_declared = True
        self.__init_members__()

    def __call__(self, *args, **kwargs):
        if self.__initialized:
            raise TypeError("'%s' object is not callable" % self.__type)
        return BatchList([])

    def __getattr__(self, name):
        if self.__initialized:
            return AttributeError(
                "'%s' object has no attribute '%s'" % (self.__type, name)
            )
        return BatchList([])

    def __setattr__(self, name, value):
        if (
            not self.__attributes_declared
            or self.__initialized
            or not isinstance(getattr(self, name), BatchList)
        ):
            super().__setattr__(name, value)

    def __init_members__(self):
        if self.__collection and not self.__initialized:
            # Note: We can't simply use the class,
            #       as the attributes exists only after the instantiation
            self.__initialized = True
            instance = self.__collection[0]
            self.__type = type(instance)
            self.__init_attributes__(instance)
            self.__init_methods__(instance)

    def __declare_decorator__(self, name, decorator):
        setattr(type(self), name, decorator)

    def __init_attributes__(self, target_instance):
        for name in self.__get_public_attributes(target_instance):
            self.__declare_decorator__(name, BatchList.BatchField(self, name))

    @staticmethod
    def __get_public_attributes(target_instance):
        attributes = (
            vars(target_instance)
            if hasattr(target_instance, "__dict__")
            else []
        )
        return (name for name in attributes if not name.startswith("_"))

    @staticmethod
    @functools.lru_cache()
    def __get_public_members(target_type):
        members = {
            name: member
            for type_members in map(vars, reversed(target_type.mro()))
            for name, member in type_members.items()
        }
        return {
            name: member
            for name, member in members.items()
            if not name.startswith("_")
        }

    def __init_methods__(self, target_instance):
        public_members = self.__get_public_members(type(target_instance))
        for name, value in public_members.items():
            if callable(value):
                self.__declare_decorator__(
                    name, BatchList.BatchMethod(self, name)
                )
            else:
                # should be an decorator
                self.__declare_decorator__(
                    name, BatchList.BatchField(self, name)
                )

    def __enter__(self):
        self.entered = True
        return BatchList([instance.__enter__() for instance in self])

    def __exit__(self, *args):
        for instance in self:
            instance.__exit__(*args)

    def __iadd__(self, other):
        if self.entered:
            for i in other:
                i.__enter__()
        self.__collection.__iadd__(other)
        self.__init_members__()
        return self

    def append(self, item):
        if self.entered:
            item.__enter__()
        self.__collection.append(item)
        self.__init_members__()

    def insert(self, index, item):
        if self.entered:
            item.__enter__()
        self.__collection.insert(index, item)
        self.__init_members__()

    def extend(self, iterable):
        for item in iterable:
            self.append(item)

    def __add__(self, other):
        return BatchList(self.__collection.__add__(other))

    def reverse(self):
        self.__collection.reverse()

    def clear(self):
        if self.entered:
            for i in self.__collection:
                i.__exit__(None, None, None)
        self.__collection.clear()

    def copy(self):
        return BatchList(self.__collection.copy())

    def pop(self, *args):
        result = self.__collection.pop(*args)

        if self.entered:
            result.__exit__(None, None, None)

        return result

    def remove(self, value):
        if self.entered:
            value.__exit__(None, None, None)
        return self.__collection.remove(value)

    def __isub__(self, other):
        for i in other:
            self.remove(i)
        return self

    def __sub__(self, other):
        copied = self.copy()
        copied -= other
        return copied

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

    def __delitem__(self, key):
        return self.pop(key)

    def __setitem__(self, key, value):
        self.pop(key)
        self.insert(key, value)

    def __getitem__(self, key):
        return self.__collection[key]

    def count(self, *args, **kwargs):
        return self.__collection.count(*args, **kwargs)

    def index(self, *args, **kwargs):
        return self.__collection.index(*args, **kwargs)

    def __iter__(self):
        return iter(self.__collection)

    def __contains__(self, item):
        return item in self.__collection

    def __reversed__(self):
        return reversed(self.__collection)


if __name__ == "__main__":

    class FooBar:
        def __init__(self, a, b, c):
            self.mhm = a
            self.b = b
            self.c = c

        def ok(self):
            return self.b

        @property
        def prop(self):
            return self.c

    # print attributes
    # print(vars(FooBar()))
    # print properties and methods
    # print(vars(FooBar).keys())
    blist = BatchList([FooBar("foo", "bar", "yay")])
    blist += [FooBar("foobar", "barfoo", "yay foobar")]
    print("mhm", blist.mhm)
    print("prop", blist.prop)
    # print('ok', blist.ok)
    print("ok call", blist.ok())
