"""
Utility module for checking generic types provided by the `typing` library.
"""

__all__ = [
    'is_union',
    'get_origin',
    'get_origin_v2',
    'is_typed_dict_type_qualifier',
    'get_args',
    'get_keys_for_typed_dict',
    'is_typed_dict',
    'is_generic',
    'is_annotated',
    'eval_forward_ref',
    'eval_forward_ref_if_needed',
]

import functools
import sys
import typing

# noinspection PyUnresolvedReferences,PyProtectedMember
from typing import Union, _AnnotatedAlias  # type: ignore

from .._type_def import (
    FREF,
    PyForwardRef,
    PyNotRequired,
    PyReadOnly,
    PyRequired,
)
from ..constants import PY310_OR_ABOVE, PY313_OR_ABOVE
from ._string_conv import repl_or_with_union

# noinspection PyTypedDict
_TYPED_DICT_TYPE_QUALIFIERS = frozenset(
    {PyRequired, PyNotRequired, PyReadOnly}
)


def get_keys_for_typed_dict(cls):
    """
    Given a :class:`TypedDict` sub-class, returns a pair of
    (required_keys, optional_keys)
    """
    return cls.__required_keys__, cls.__optional_keys__


def _is_annotated(cls):
    return isinstance(cls, _AnnotatedAlias)


# Ref:
#   https://typing.readthedocs.io/en/latest/spec/typeddict.html#required-and-notrequired
#   https://typing.readthedocs.io/en/latest/spec/glossary.html#term-type-qualifier
def is_typed_dict_type_qualifier(cls) -> bool:
    return cls in _TYPED_DICT_TYPE_QUALIFIERS


# Ref:
#   https://github.com/python/typing/blob/master/typing_extensions/src_py3/typing_extensions.py#L2111
if PY310_OR_ABOVE:  # pragma: no cover
    from types import GenericAlias, UnionType
    _get_args = typing.get_args

    # noinspection PyUnresolvedReferences,PyProtectedMember
    _BASE_GENERIC_TYPES = (
        typing._GenericAlias,  # type: ignore
        typing._SpecialForm,
        GenericAlias,
        UnionType,
    )

    _UNION_TYPES = frozenset({
        UnionType,
        Union,
    })

    _TYPING_LOCALS = None

    def _process_forward_annotation(base_type):
        return PyForwardRef(base_type, is_argument=False)

    def is_union(cls) -> bool:
        return cls in _UNION_TYPES

    def get_origin_v2(cls):
        if type(cls) is UnionType:
            return UnionType

        return getattr(cls, '__origin__', cls)

    def _get_origin(cls, raise_=False):
        if isinstance(cls, UnionType):
            return Union

        try:
            return cls.__origin__
        except AttributeError:
            if raise_:
                raise
            return cls

else:  # pragma: no cover
    from typing_extensions import get_args as _get_args

    # noinspection PyProtectedMember,PyUnresolvedReferences
    _BASE_GENERIC_TYPES = (
        typing._GenericAlias,  # type: ignore
        typing._SpecialForm,
    )

    # PEP 585 is introduced in Python 3.9
    # PEP 604 (Allows writing union types as `X | Y`) is introduced
    #   in Python 3.10
    _TYPING_LOCALS = {'Union': Union}

    def _process_forward_annotation(base_type):
        return PyForwardRef(
            repl_or_with_union(base_type), is_argument=False)

    def is_union(cls) -> bool:
        return cls is Union

    def get_origin_v2(cls):
        return getattr(cls, '__origin__', cls)

    def _get_origin(cls, raise_=False):
        try:
            return cls.__origin__
        except AttributeError:
            if raise_:
                raise
            return cls


try:
    # noinspection PyProtectedMember,PyUnresolvedReferences
    from typing_extensions import _TYPEDDICT_TYPES, is_typeddict

except ImportError:
    from typing import is_typeddict as is_typed_dict

else:
    def is_typed_dict(cls: type) -> bool:
        """
        Checks if `cls` is a sub-class of ``TypedDict``
        """
        return isinstance(cls, _TYPEDDICT_TYPES)


def is_generic(cls):
    """
    Detects any kind of generic, for example `List` or `List[int]`. This
    includes "special" types like Union, Any ,and Tuple - anything that's
    subscriptable, basically.

    https://stackoverflow.com/a/52664522/10237506
    """
    return isinstance(cls, _BASE_GENERIC_TYPES)


get_args = _get_args
get_args.__doc__ = """
Get type arguments with all substitutions performed.

For unions, basic simplifications used by Union constructor are performed.
Examples::
    get_args(Dict[str, int]) == (str, int)
    get_args(int) == ()
    get_args(Union[int, Union[T, int], str][int]) == (int, str)
    get_args(Union[int, Tuple[T, int]][str]) == (int, Tuple[str, int])
    get_args(Callable[[], T][int]) == ([], int)\
"""

# TODO refactor to use `typing.get_origin` when time permits.
get_origin = _get_origin
get_origin.__doc__ = """
Get the un-subscripted value of a type. If we're unable to retrieve this
value, return type `cls` if `raise_` is false.

This supports generic types, Callable, Tuple, Union, Literal, Final and
ClassVar. Return None for unsupported types.

Examples::

    get_origin(Literal[42]) is Literal
    get_origin(int) is int
    get_origin(ClassVar[int]) is ClassVar
    get_origin(Generic) is Generic
    get_origin(Generic[T]) is Generic
    get_origin(Union[T, int]) is Union
    get_origin(List[Tuple[T, T]][int]) == list

:raise AttributeError: When the `raise_` flag is enabled, and we are
  unable to retrieve the un-subscripted value.\
"""

is_annotated = _is_annotated
is_annotated.__doc__ = """Detects a :class:`typing.Annotated` class."""


if PY313_OR_ABOVE:
    # noinspection PyProtectedMember,PyUnresolvedReferences
    _eval_type = functools.partial(typing._eval_type, type_params=())
else:
    # noinspection PyProtectedMember,PyUnresolvedReferences
    _eval_type = typing._eval_type


def eval_forward_ref(base_type: FREF,
                     cls: type):
    """
    Evaluate a forward reference using the class globals, and return the
    underlying type reference.
    """

    if isinstance(base_type, str):
        base_type = _process_forward_annotation(base_type)

    # Evaluate the ForwardRef here
    base_globals = sys.modules[cls.__module__].__dict__

    return _eval_type(base_type, base_globals, _TYPING_LOCALS)


_ForwardRefTypes = frozenset(FREF.__constraints__)


def eval_forward_ref_if_needed(base_type: Union[type, FREF],
                               base_cls: type):
    """
    If needed, evaluate a forward reference using the class globals, and
    return the underlying type reference.
    """

    if type(base_type) in _ForwardRefTypes:
        # Evaluate the forward reference here.
        base_type = eval_forward_ref(base_type, base_cls)

    return base_type
