#  This file is part of Pynguin.
#
#  SPDX-FileCopyrightText: 2019–2022 Pynguin Contributors
#
#  SPDX-License-Identifier: LGPL-3.0-or-later
#
"""
Provide wrappers around constructors, methods, function, fields and enums.
Think of these like the reflection classes in Java.
"""
from __future__ import annotations

import abc
import enum
import typing
from typing import Callable, List, cast

if typing.TYPE_CHECKING:
    from pynguin.typeinference.strategy import InferredSignature


class GenericAccessibleObject(metaclass=abc.ABCMeta):
    """Abstract base class for something that can be accessed."""

    def __init__(self, owner: type | None):
        self._owner = owner

    @abc.abstractmethod
    def generated_type(self) -> type | None:
        """Provides the type that is generated by this accessible object.

        Returns:
            The generated type  # noqa: DAR202
        """

    @property
    def owner(self) -> type | None:
        """The type which owns this accessible object.

        Returns:
            The owner of this accessible object
        """
        return self._owner

    # pylint: disable=no-self-use
    def is_enum(self) -> bool:
        """Is this a enum?

        Returns:
            Whether or not this is an enum
        """
        return False

    # pylint: disable=no-self-use
    def is_method(self) -> bool:
        """Is this a method?

        Returns:
            Whether or not this is a method
        """
        return False

    # pylint: disable=no-self-use
    def is_constructor(self) -> bool:
        """Is this a constructor?

        Returns:
            Whether or not this is a constructor
        """
        return False

    # pylint: disable=no-self-use
    def is_function(self) -> bool:
        """Is this a function?

        Returns:
            Whether or not this is a function
        """
        return False

    # pylint: disable=no-self-use
    def is_field(self) -> bool:
        """Is this a field?

        Returns:
            Whether or not this is a field
        """
        return False

    # pylint: disable=no-self-use
    def is_static(self) -> bool:
        """Is this static?

        Returns:
            Whether or not this is static
        """
        return False

    # pylint: disable=no-self-use
    def get_num_parameters(self) -> int:
        """Number of parameters.

        Returns:
            The number of parameters
        """
        return 0

    @abc.abstractmethod
    def get_dependencies(self) -> set[type]:
        """A set of types that are required to use this accessible.

        Returns:
            A set of types  # noqa: DAR202
        """


class GenericEnum(GenericAccessibleObject):
    """Models an enum."""

    def __init__(self, owner: type[enum.Enum]):
        super().__init__(owner)
        self._names = list(map(lambda e: e.name, cast(List[enum.Enum], list(owner))))

    def generated_type(self) -> type | None:
        return self._owner

    @property
    def names(self) -> list[str]:
        """All names that this enum has.

        Returns:
            All possible values of this enum."""
        return self._names

    def is_enum(self) -> bool:
        return True

    def get_dependencies(self) -> set[type]:
        return set()  # pylint:disable=no-self-use

    def __eq__(self, other):
        if self is other:
            return True
        if not isinstance(other, GenericEnum):
            return False
        return self._owner == other._owner

    def __hash__(self):
        return hash(self._owner)

    def __repr__(self):
        return f"{self.__class__.__name__}({self.owner})"


class GenericCallableAccessibleObject(
    GenericAccessibleObject, metaclass=abc.ABCMeta
):  # pylint: disable=W0223
    """Abstract base class for something that can be called."""

    def __init__(
        self,
        owner: type | None,
        callable_: Callable,
        inferred_signature: InferredSignature,
    ) -> None:
        super().__init__(owner)
        self._callable = callable_
        self._inferred_signature = inferred_signature

    def generated_type(self) -> type | None:
        return self._inferred_signature.return_type

    @property
    def inferred_signature(self) -> InferredSignature:
        """Provides access to the inferred type signature information.

        Returns:
            The inferred type signature
        """
        return self._inferred_signature

    @property
    def callable(self) -> Callable:
        """Provides the callable.

        Returns:
            The callable
        """
        return self._callable

    def get_num_parameters(self) -> int:
        return len(self.inferred_signature.parameters)

    def get_dependencies(self) -> set[type]:
        return {
            value
            for value in self.inferred_signature.parameters.values()
            if value is not None
        }


class GenericConstructor(GenericCallableAccessibleObject):
    """A constructor."""

    def __init__(self, owner: type, inferred_signature: InferredSignature) -> None:
        # super().__init__(owner, owner.__init__, inferred_signature)  # type: ignore
        super().__init__(owner, getattr(owner, "__init__"), inferred_signature)
        assert owner

    def generated_type(self) -> type | None:
        return self.owner

    def is_constructor(self) -> bool:
        return True

    def __eq__(self, other):
        if self is other:
            return True
        if not isinstance(other, GenericConstructor):
            return False
        return self._owner == other._owner

    def __hash__(self):
        return hash(self._owner)

    def __repr__(self):
        return f"{self.__class__.__name__}({self.owner}, {self.inferred_signature})"


class GenericMethod(GenericCallableAccessibleObject):
    """A method."""

    def __init__(
        self,
        owner: type,
        method: Callable,
        inferred_signature: InferredSignature,
        method_name: str | None = None,
    ) -> None:
        super().__init__(owner, method, inferred_signature)
        assert owner
        self._method_name = method_name

    @property
    def method_name(self):
        """Returns the name of a generic method.

        Returns:
            The name of a generic method.
        """
        return self._method_name

    def is_method(self) -> bool:
        return True

    def get_dependencies(self) -> set[type]:
        assert self.owner, "Method must have an owner"
        dependencies = super().get_dependencies()
        dependencies.add(self.owner)
        return dependencies

    def __eq__(self, other):
        if self is other:
            return True
        if not isinstance(other, GenericMethod):
            return False
        return self._callable == other._callable

    def __hash__(self):
        return hash(self._callable)

    def __repr__(self):
        return (
            f"{self.__class__.__name__}({self.owner},"
            + f" {self._callable.__name__}, {self.inferred_signature})"
        )


class GenericFunction(GenericCallableAccessibleObject):
    """A function, which does not belong to any class."""

    def __init__(
        self,
        function: Callable,
        inferred_signature: InferredSignature,
        function_name: str | None = None,
    ) -> None:
        self._function_name = function_name
        super().__init__(None, function, inferred_signature)

    def is_function(self) -> bool:
        return True

    @property
    def function_name(self) -> str | None:
        """Returns the name of a generic function.

        Returns:
            The name of a generic function.
        """
        return self._function_name

    def __eq__(self, other):
        if self is other:
            return True
        if not isinstance(other, GenericFunction):
            return False
        return self._callable == other._callable

    def __hash__(self):
        return hash(self._callable)

    def __repr__(self):
        return (
            f"{self.__class__.__name__}({self._callable.__name__}, "
            + f"{self.inferred_signature})"
        )


class GenericAbstractField(GenericAccessibleObject, metaclass=abc.ABCMeta):
    """Abstract superclass for fields."""

    def __init__(self, owner: type | None, field: str, field_type: type | None) -> None:
        super().__init__(owner)
        self._field = field
        self._field_type = field_type

    def is_field(self) -> bool:
        return True

    def generated_type(self) -> type | None:
        return self._field_type

    @property
    def field(self) -> str:
        """Provides the name of the field.

        Returns:
            The name of the field
        """
        return self._field


class GenericField(GenericAbstractField):
    """A field of an object."""

    def __init__(self, owner: type, field: str, field_type: type | None):
        super().__init__(owner, field, field_type)
        assert owner, "Field must have an owner"

    def get_dependencies(self) -> set[type]:
        assert self._owner, "Field must have an owner"
        return {self._owner}

    def __eq__(self, other):
        if self is other:
            return True
        if not isinstance(other, GenericField):
            return False
        return self._owner == other._owner and self._field == other._field

    def __hash__(self):
        return 31 + 17 * hash(self._owner) + 17 * hash(self._field)

    def __repr__(self):
        return (
            f"{self.__class__.__name__}({self.owner}, {self._field},"
            + f" {self._field_type})"
        )


class GenericStaticField(GenericAbstractField):
    """Static field of a class."""

    def __init__(self, owner: type, field: str, field_type: type | None):
        super().__init__(owner, field, field_type)
        assert owner, "Field must have an owner"

    def is_static(self) -> bool:
        return True

    def get_dependencies(self) -> set[type]:
        return set()

    def __eq__(self, other):
        if self is other:
            return True
        if not isinstance(other, GenericStaticField):
            return False
        return self._owner == other._owner and self._field == other._field

    def __hash__(self):
        return 31 + 17 * hash(self._owner) + 17 * hash(self._field)

    def __repr__(self):
        return (
            f"{self.__class__.__name__}({self.owner}, {self._field},"
            + f" {self._field_type})"
        )


class GenericStaticModuleField(GenericAbstractField):
    """Static fields defined in a module."""

    # TODO(fk) combine with regular static field?

    def __init__(self, module: str, field: str, field_type: type | None):
        super().__init__(None, field, field_type)
        self._module = module

    def is_static(self) -> bool:
        return True

    def get_dependencies(self) -> set[type]:
        return set()

    @property
    def module(self) -> str:
        """Provides the name of the module where the field is defined.

        Returns:
            The name of the module where the field is defined.
        """
        return self._module

    def __eq__(self, other):
        if self is other:
            return True
        if not isinstance(other, GenericStaticModuleField):
            return False
        return self._module == other._module and self._field == other._field

    def __hash__(self):
        return 31 + 17 * hash(self._module) + 17 * hash(self._field)

    def __repr__(self):
        return (
            f"{self.__class__.__name__}({self._module}, {self._field},"
            + f" {self._field_type})"
        )
