import functools
import inspect
from copy import copy
from typing import Dict, Type, Union, Any, TypeVar, Callable, Set, List

from .interfaces import SpecialDepDefinition
from .exceptions import UnresolvableType, DuplicateDefinition
from .definitions import normalise, Singleton, Construction
from .util.reflection import RETURN_ANNOTATION

UNRESOLVABLE_TYPES = [str, int, float, bool]

X = TypeVar("X")

DepDefinition = Any


class Container:
    _registered_types: Dict[Type, SpecialDepDefinition]
    _explicitly_registered_types: Set[Type]

    def __init__(self):
        self._registered_types = {}
        self._explicitly_registered_types = set()

    def define(self, dep: Union[Type[X], Type], resolver: DepDefinition) -> None:
        if dep in self._explicitly_registered_types:
            raise DuplicateDefinition()
        self._registered_types[dep] = normalise(resolver, self)
        self._explicitly_registered_types.add(dep)

    def resolve(self, dep_type: Type[X], suppress_error=False) -> X:
        try:
            if dep_type in UNRESOLVABLE_TYPES:
                raise UnresolvableType(f"Cannot construct type {dep_type}")
            registered_type = self._registered_types.get(dep_type, dep_type)
            return self._build(registered_type)
        except UnresolvableType as inner_error:
            if not suppress_error:
                raise UnresolvableType(
                    f"Cannot construct type {dep_type.__name__}"
                ) from inner_error
            return None  # type: ignore

    def partial(
        self, func: Callable[..., X], keys_to_skip=None, shared: List[Type] = None
    ) -> Callable[..., X]:
        if shared:
            return self._partial_with_shared_singletons(func, shared)
        spec = inspect.getfullargspec(func)
        bindable_deps = self._infer_dependencies(
            spec, suppress_error=True, keys_to_skip=keys_to_skip or []
        )
        bound_func = functools.partial(func, **bindable_deps)

        if inspect.iscoroutinefunction(bound_func.func):

            async def _async_wrapper(*args, **kwargs):
                return await bound_func(*args, **kwargs)  # type: ignore

            return _async_wrapper

        # This function exists so that binding can be used in places
        # that rely on `inspect.is_function` to return True.
        # The output from functools.partial will evaluate to False so we
        # need to wrap the call.
        def _compatibility_wrapper(*args, **kwargs):
            return bound_func(*args, **kwargs)

        return _compatibility_wrapper

    def clone(self):
        new_container = Container()
        new_container._registered_types = copy(self._registered_types)

        # Even though the new container has the type definitions we want
        # them all to be overridable so the clone can have updated
        # definitions
        new_container._explicitly_registered_types = set()

        return new_container

    def __getitem__(self, dep: Type[X]) -> X:
        return self.resolve(dep)

    def __setitem__(self, dep: Type, resolver: DepDefinition):
        self.define(dep, resolver)

    def _build(self, dep_type: Any) -> Any:
        if isinstance(dep_type, SpecialDepDefinition):
            return dep_type.get_instance(self._build)
        return self._reflection_build(dep_type)

    def _reflection_build(self, dep_type: Type[X]) -> X:
        spec = inspect.getfullargspec(dep_type.__init__)
        sub_deps = self._infer_dependencies(spec)
        return dep_type(**sub_deps)  # type: ignore

    def _infer_dependencies(
        self, spec: inspect.FullArgSpec, suppress_error=False, keys_to_skip=None
    ):
        keys_to_skip = keys_to_skip or []
        sub_deps = {
            key: self.resolve(sub_dep_type, suppress_error=suppress_error)
            for (key, sub_dep_type) in spec.annotations.items()
            if key != RETURN_ANNOTATION
            and sub_dep_type != Any
            and key not in keys_to_skip
        }
        filtered_deps = {key: dep for (key, dep) in sub_deps.items() if dep is not None}
        return filtered_deps

    def _partial_with_shared_singletons(
        self, func: Callable[..., X], shared: List[Type]
    ):
        def _function(*args, **kwargs):
            temp_container = self.clone()
            # For each of the shared dependencies resolve before invocation
            # and replace with a singleton
            for type_def in shared:
                temp_container[type_def] = Singleton(
                    Construction(lambda: self.resolve(type_def))
                )
            temp_bound_func = temp_container.partial(func, shared=[])
            return temp_bound_func(*args, **kwargs)

        return _function
