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 SolvingError.
        """
        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 non None `call` method,
        if one is not already present.
        That is, after this is run, self.call should not be None.

        This can also be used for recording type annotations or parameter names.

        This method may return the same instance or another DependantBase altogether.
        """
        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]
