import inspect
import logging
from logging import Logger
from types import ModuleType
from typing import List, Type, Union, Optional, Callable, Set

from ._DependencyBuilder import DependencyBuilder
from ._DependencyContext import DependencyContext
from ._DependencyContextImpl import DependencyContextImpl
from ._DependencyMap import DependencyMap
from ._utils import (
    load_module_then_execute,
    get_class_parent_types,
    get_method_by_name,
    inherit_from,
)


class DependencyLoader(object):

    def __init__(self, logger: Optional[Logger] = None):
        self.dependency_map = DependencyMap()
        self.logger = logger or logging.getLogger()

    def load_modules(
            self,
            modules: List[ModuleType],
            recursive: Optional[bool] = True,
            class_filter: Optional[Callable[[Type, ], bool]] = None,
    ):
        for module in modules:
            load_module_then_execute(
                load_module=module,
                callback=lambda m: self._load_dependencies_from_module(
                    load_module=m,
                    class_filter=class_filter
                ),
                logger=self.logger,
                recursive=recursive
            )

    def build_context(self) -> DependencyContext:
        return DependencyContextImpl(self.dependency_map.clone(), self.logger)

    def get_dependencies_by_parent(self, parent_type: Type) -> Set[Type]:
        return self.dependency_map.classes_by_parent.get(parent_type, [])

    def add_dependency_class(self, dep_class: Type):
        if not inspect.isabstract(dep_class):
            self.logger.debug(f"---> Loaded class dependency {dep_class.__name__}")
            if self._is_builder(dep_class):
                build_method = inspect.signature(self._get_builder_method(dep_class))
                build_return = build_method.return_annotation
                self.dependency_map.builders[build_return] = dep_class
                for child_type in get_class_parent_types(build_return):
                    self.dependency_map.builders[child_type] = dep_class
            else:
                self._add_class_hierarchy_to_dependency(dep_class)

            for base_type in get_class_parent_types(dep_class):
                if base_type not in self.dependency_map.classes_by_parent:
                    self.dependency_map.classes_by_parent[base_type] = set()
                self.dependency_map.classes_by_parent[base_type].add(dep_class)

    def _add_class_hierarchy_to_dependency(self, cls: Type):
        if cls not in self.dependency_map.classes:
            self.dependency_map.classes[cls] = cls
            for parent in get_class_parent_types(cls):
                self.dependency_map.classes[parent] = cls

    def _load_dependencies_from_module(
            self,
            load_module: ModuleType,
            class_filter: Optional[Callable[[Type, ], bool]] = None,
    ):
        for member_name, member_obj in inspect.getmembers(load_module):
            if inspect.isclass(member_obj) and not inspect.isabstract(member_obj):
                if class_filter is not None and class_filter(member_obj):
                    self.add_dependency_class(member_obj)
                elif class_filter is None:
                    self.add_dependency_class(member_obj)

    @staticmethod
    def _get_builder_method(element: Union[Type, object]):
        return get_method_by_name(element, 'build')

    @staticmethod
    def _is_builder(dep_class: Union[Type, object]):
        return inherit_from(dep_class, DependencyBuilder)
