from .ctor import CustomConstructor, RegularConstructor
from .iface import ImpasseException, MissingAnnotationException, NoSuchInstanceException, unset
from .match import AllInstancesOf, wrap
from .source import Builder, Class, ContextManager, Factory, Instance, Proxy
from .start import starter
from .util import bfs
from collections import defaultdict
from contextlib import ExitStack
from foyndation import singleton
from inspect import isfunction
import logging

log = logging.getLogger(__name__)
typeitself = type

@singleton
class NullPlan:

    args = ()

    def make(self):
        pass

def types(*deptypes, **kwargs):
    'Declare that the decorated function or method expects args of the given types. Use square brackets to request all instances of a type as a list. Use `this` kwarg to declare the type of result returned by a factory.'
    def g(f):
        f.di_deptypes = tuple(wrap(t) for t in deptypes)
        if 'this' in kwargs:
            f.di_owntype = kwargs['this']
        return f
    return g

class State:

    class Context:

        def __init__(self):
            self.instances = {}
            self.stack = ExitStack()

        def reset(self):
            self.instances.clear()
            stack, self.stack = self.stack, ExitStack()
            stack.close()

    @property
    def context(self):
        return self.contexts[-1]

    def __init__(self):
        self.contexts = [self.Context()]

    def enter(self):
        self.contexts.append(self.Context())

    def exit(self, exc_info):
        return self.contexts.pop().stack.__exit__(*exc_info)

    def getinstance(self, source):
        for context in reversed(self.contexts):
            if (i := context.instances.get(source, unset)) is not unset:
                return i
        raise NoSuchInstanceException(source.type)

class DI:

    log = log # Tests may override.
    depthunit = '>'

    @property
    def state(self):
        return self.treestate if self.parent is None else self.parent.state

    def __init__(self, parent = None):
        self.typetosources = defaultdict(list)
        if parent is None:
            self.treestate = State()
        self.parent = parent

    def addsource(self, source):
        for type in source.types:
            self.typetosources[type].append(source)

    def removesource(self, source): # TODO: Untested.
        for type in source.types:
            self.typetosources[type].remove(source)

    def addclass(self, clazz):
        constructors = [*RegularConstructor.find(clazz), *CustomConstructor.find(clazz)]
        if not constructors:
            raise MissingAnnotationException("Missing types annotation: %s" % clazz)
        constructor, = constructors
        self.addsource(Class(clazz, constructor, self))
        self._addbuilders(clazz)
        if getattr(clazz, 'start', None) is not None:
            self.addclass(starter(clazz))

    def _addbuilders(self, cls):
        for name in dir(cls):
            m = getattr(cls, name)
            if hasattr(m, 'di_deptypes') and hasattr(m, 'di_owntype'):
                assert '__init__' != name # TODO LATER: Check upfront.
                self.addsource(Builder(cls, m, self))

    def addinstance(self, instance, type = None):
        clazz = instance.__class__ if type is None else type
        self.addsource(Instance(instance, clazz))
        if not isinstance(instance, typeitself):
            self._addbuilders(clazz)

    def addfactory(self, factory):
        self.addsource(Factory(factory, self))

    def addcmfactory(self, f):
        self.addsource(cmsource := Factory(f, self))
        self.addsource(ContextManager(cmsource, self))

    def _addmethods(self, obj):
        if hasattr(obj, 'di_owntype') or isfunction(obj):
            yield self.addfactory
        elif hasattr(obj, '__class__'):
            clazz = obj.__class__
            if clazz == type: # It's a non-fancy class.
                yield self.addclass
            elif isinstance(obj, type): # It's a fancy class.
                yield self.addinstance
                try:
                    obj.__init__.di_deptypes
                    yield self.addclass
                except AttributeError:
                    pass # Not admissible as class.
            else: # It's an instance.
                yield self.addinstance
        else: # It's an old-style class.
            yield self.addclass

    def add(self, *objs):
        'Register the given classes, factories or instances.'
        for obj in objs:
            for m in self._addmethods(obj):
                m(obj)

    def all(self, type):
        'Return all objects of the given type, instantiating them and collaborators if necessary.'
        return self._resolve(AllInstancesOf(type))

    def __call__(self, clazz):
        'Return unique object of the given type, instantiating it and its collaborators if necessary.'
        return self._resolve(wrap(clazz))

    def __getitem__(self, clazz):
        return wrap(clazz).di_get(self, unset).resolve()

    def _resolve(self, match):
        rootarg = match.di_get(self, unset)
        plans = {}
        @bfs([rootarg])
        def proc(info, a):
            for s in a.sources:
                if s not in plans:
                    p = s.plan(self.depthunit * info.depth, a.trigger)
                    if p is None:
                        p = NullPlan
                    plans[s] = p
                    for b in p.args:
                        yield b
        while plans:
            sources = [s for s in plans if not any(r in plans for a in plans[s].args for r in a.sources)]
            if not sources:
                raise ImpasseException
            for s in sources:
                plans.pop(s).make()
        return rootarg.resolve()

    def join(self, type):
        'Add a source to the parent that requests the given type from this container.'
        self.parent.addsource(Proxy(self, type))

    def __enter__(self):
        self.state.enter()
        return self

    def __exit__(self, *exc_info):
        return self.state.exit(exc_info)

    def discardall(self):
        '''Discard all instances created in the current context.
        Each instance created not via a context manager will have its `dispose` method called if there is one.
        Normally this procedure is automatic on context exit, this method is provided for the root context which is never exited.'''
        self.state.context.reset()
