from __future__ import annotations

import abc
import inspect
from dataclasses import dataclass
from typing import Any, Generic, List, Optional, TypeVar

from di.types.providers import DependencyProviderType
from di.types.scopes import Scope

DependencyType = TypeVar("DependencyType")


T = TypeVar("T")


class DependantBase(Generic[DependencyType], abc.ABC):
    """A dependant is an object that can provide the container with:
    - A hash, to compare itself against other dependants
    - A scope
    - A callable that can be used to assemble itself
    - The dependants that correspond to the keyword arguments of that callable
    """

    call: Optional[DependencyProviderType[DependencyType]]
    scope: Scope
    share: bool

    @abc.abstractmethod
    def __hash__(self) -> int:
        """Dependencies need to be hashable for quick O(1) lookups.

        Generally, hash(self.call) will suffice so that dependencies are identified by their callable.
        """
        pass  # pragma: no cover

    @abc.abstractmethod
    def __eq__(self, o: object) -> bool:
        """Used in conjunction with __hash__ for mapping lookups of dependencies.

        If this returns `True`, the two dependencies are considered the same and
        `di` will pick one to subsitute for the other.

        Note that using the same dependenyc in two different scopes is prohibited,
        so if this returns `True` and `self.scope != o.scope` `di` will raise a DependencyRegistryError.
        """
        pass  # pragma: no cover

    @abc.abstractmethod
    def get_dependencies(
        self,
    ) -> List[DependencyParameter[DependantBase[Any]]]:
        """Collect all of the sub dependencies for this dependant"""
        pass  # pragma: no cover

    @abc.abstractmethod
    def register_parameter(
        self: DependantBase[T], param: inspect.Parameter
    ) -> DependantBase[T]:
        """Called by the parent so that us / this / the child can register
        the parameter it is attached to.

        It is *required* that this method register a noe None `call` method,
        if one is not already present.

        This can also be used for recording type annotations or parameter names.
        """
        pass  # pragma: no cover

    def __repr__(self) -> str:
        share = "" if self.share is False else ", share=True"
        return f"{self.__class__.__name__}(call={self.call}, scope={self.scope}{share})"


# this could be a NamedTuple
# but https://github.com/python/mypy/issues/685
# we need the generic type
# so we can use it for DependantBase and Task
@dataclass
class DependencyParameter(Generic[DependencyType]):
    dependency: DependencyType
    parameter: Optional[inspect.Parameter]
