#!/usr/bin/env python3
# --------------------( LICENSE                           )--------------------
# Copyright (c) 2014-2021 Beartype authors.
# See "LICENSE" for further details.

'''
**Beartype type-checking error cause sleuth** (i.e., object recursively
fabricating the human-readable string describing the failure of the pith
associated with this object to satisfy this PEP-compliant type hint also
associated with this object) classes.

This private submodule is *not* intended for importation by downstream callers.
'''

# ....................{ IMPORTS                           }....................
from beartype.roar._roarexc import _BeartypeCallHintPepRaiseException
from beartype._cave._cavefast import NoneType
from beartype._cave._cavemap import NoneTypeOr
from beartype._util.hint.pep.proposal.utilhintpep484 import (
    get_hint_pep484_newtype_class,
    is_hint_pep484_newtype,
)
from beartype._util.hint.pep.proposal.utilhintpep544 import (
    get_hint_pep544_io_protocol_from_generic,
    is_hint_pep544_io_generic,
)
from beartype._util.hint.pep.proposal.utilhintpep593 import (
    get_hint_pep593_metahint,
    is_hint_pep593,
    is_hint_pep593_beartype,
)
from beartype._util.hint.pep.utilhintpepget import (
    get_hint_pep_args,
    get_hint_pep_generic_bases_unerased,
    get_hint_pep_sign,
)
from beartype._util.hint.pep.utilhintpeptest import (
    is_hint_pep,
    is_hint_pep_generic,
    is_hint_pep_tuple_empty,
    is_hint_pep_typevar,
)
from beartype._util.hint.utilhinttest import (
    is_hint_forwardref,
    is_hint_ignorable,
)
from typing import Any, Callable, NoReturn, Optional, Tuple

# See the "beartype.cave" submodule for further commentary.
__all__ = ['STAR_IMPORTS_CONSIDERED_HARMFUL']

# ....................{ CLASSES                           }....................
class CauseSleuth(object):
    '''
    **Type-checking error cause sleuth** (i.e., object recursively fabricating
    the human-readable string describing the failure of the pith associated
    with this object to satisfy this PEP-compliant type hint also associated
    with this object).

    Attributes
    ----------
    cause_indent : str
        **Indentation** (i.e., string of zero or more spaces) preceding each
        line of the string returned by this getter if this string spans
        multiple lines *or* ignored otherwise (i.e., if this string is instead
        embedded in the current line).
    exception_label : str
        Human-readable label describing the parameter or return value from
        which this object originates, typically embedded in exceptions raised
        from this getter in the event of unexpected runtime failure.
    func : Callable
        Decorated callable generating this type-checking error.
    hint_sign : Any
        Unsubscripted :mod:`typing` attribute identifying this hint if this hint
        is PEP-compliant *or* ``None`` otherwise.
    hint_childs : Optional[Tuple]
        Either:

        * If this hint is PEP-compliant:

          * If this hint is a generic, tuple of the one or more unerased
            pseudo-superclasses (i.e., :mod:`typing` objects originally listed
            as superclasses prior to their implicit type erasure by the
            :mod:`typing` module) subclassed by this generic.
          * Else, the possibly empty tuple of all arguments subscripting this
            hint if this

        * Else, ``None``.
    pith : Any
        Arbitrary object to be validated.
    random_int: Optional[int]
        **Pseudo-random integer** (i.e., unsigned 32-bit integer
        pseudo-randomly generated by the parent :func:`beartype.beartype`
        wrapper function in type-checking randomly indexed container items by
        the current call to that function) if that function generated such an
        integer *or* ``None`` otherwise (i.e., if that function generated *no*
        such integer). See the same parameter accepted by the higher-level
        :func:`beartype._decor._error.errormain.raise_pep_call_exception`
        function for further details.

    Attributes (Private)
    ----------
    _hint : Any
        Type hint to validate this object against.
    '''

    # ..................{ CLASS VARIABLES                   }..................
    # Slot *ALL* instance variables defined on this object to both:
    # * Prevent accidental declaration of erroneous instance variables.
    # * Minimize space and time complexity.
    __slots__ = (
        'cause_indent',
        'exception_label',
        'func',
        'hint_sign',
        'hint_childs',
        'pith',
        'random_int',
        '_hint',
    )


    _INIT_PARAM_NAMES = frozenset((
        'cause_indent',
        'exception_label',
        'func',
        'hint',
        'pith',
        'random_int',
    ))
    '''
    Frozen set of the names of all parameters accepted by the :meth:`init`
    method, defined as a set to enable efficient membership testing.
    '''

    # ..................{ INITIALIZERS                      }..................
    #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
    # CAUTION: Whenever adding, deleting, or renaming any parameter accepted by
    # this method, make similar changes to the "_INIT_PARAM_NAMES" set above.
    #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
    def __init__(
        self,
        func: Callable,
        pith: Any,
        hint: Any,
        cause_indent: str,
        exception_label: str,
        random_int: int,
    ) -> None:
        '''
        Initialize this object.
        '''
        assert callable(func), f'{repr(func)} not callable.'
        assert isinstance(cause_indent, str), (
            f'{repr(cause_indent)} not string.')
        assert isinstance(exception_label, str), (
            f'{repr(exception_label)} not string.')
        assert isinstance(random_int, NoneTypeOr[int]), (
            f'{repr(random_int)} not integer or "None".')

        # Classify all passed parameters.
        self.func = func
        self.pith = pith
        self.cause_indent = cause_indent
        self.exception_label = exception_label
        self.random_int = random_int

        # Nullify all remaining parameters for safety.
        self.hint_sign: Any = None
        self.hint_childs: Tuple = None  # type: ignore[assignment]

        # Classify this hint *AFTER* initializing all parameters above.
        self.hint = hint

    # ..................{ PROPERTIES                        }..................
    @property
    def hint(self) -> Any:
        '''
        Type hint to validate this object against.
        '''

        return self._hint


    @hint.setter
    def hint(self, hint: Any) -> None:
        '''
        Set the type hint to validate this object against.
        '''

        # ................{ REDUCTION                         }................
        #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
        # CAVEATS: Synchronize changes here with the corresponding block of the
        # beartype._decor._code._pep._pephint.pep_code_check_hint() function.
        #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
        #
        # This logic reduces the currently visited hint to an arbitrary object
        # associated with this hint when this hint conditionally satisfies any
        # of various conditions.
        #
        # ................{ REDUCTION ~ pep 484 ~ none        }................
        # If this is the PEP 484-compliant "None" singleton, reduce this hint
        # to the type of that singleton. While not explicitly defined by the
        # "typing" module, PEP 484 explicitly supports this singleton:
        #     When used in a type hint, the expression None is considered
        #     equivalent to type(None).
        if hint is None:
            hint = NoneType
        # ................{ REDUCTION ~ pep 593               }................
        # If this is a PEP 593-compliant type metahint, ignore all annotations
        # on this hint (i.e., "hint_curr.__metadata__" tuple) by reducing this
        # hint to its origin (e.g., "str" in "Annotated[str, 50, False]").
        elif is_hint_pep593(hint):
            # If the first argument subscripting this metahint is
            # beartype-agnostic (e.g., *NOT* an instance of the
            # "beartype.vale._SubscriptedIs" class produced by subscripting the
            # "Is" class), ignore all annotations on this hint by reducing this
            # hint to its origin (e.g., "str" in "Annotated[str, 50, False]").
            if not is_hint_pep593_beartype(hint):
                hint = get_hint_pep593_metahint(hint)
            # Else, that argument is beartype-specific. In this case, preserve
            # this hint as is for subsequent handling below.
        # ................{ REDUCTION ~ pep 544               }................
        # If this is a PEP 484-compliant IO generic base class *AND* the active
        # Python interpreter targets at least Python >= 3.8 and thus supports
        # PEP 544-compliant protocols, reduce this functionally useless hint to
        # the corresponding functionally useful beartype-specific PEP
        # 544-compliant protocol implementing this hint.
        #
        # Note that PEP 484-compliant IO generic base classes are technically
        # usable under Python < 3.8 (e.g., by explicitly subclassing those
        # classes from third-party classes). Ergo, we can neither safely emit
        # warnings nor raise exceptions on visiting these classes under *ANY*
        # Python version.
        elif is_hint_pep544_io_generic(hint):
            hint = get_hint_pep544_io_protocol_from_generic(hint)
        # ................{ REDUCTION ~ pep 484 ~ new type    }................
        # If this is a PEP 484-compliant new type hint, reduce this hint to the
        # user-defined class aliased by this hint. Although this logic could
        # also be performed below, doing so here simplifies matters.
        elif is_hint_pep484_newtype(hint):
            hint = get_hint_pep484_newtype_class(hint)
        # ................{ REDUCTION ~ end                   }................

        # If this hint is PEP-compliant...
        if is_hint_pep(hint):
            # Arbitrary object uniquely identifying this hint.
            self.hint_sign = get_hint_pep_sign(hint)

            # Tuple of either...
            self.hint_childs = (
                # If this hint is a generic, the one or more unerased
                # pseudo-superclasses originally subclassed by this hint.
                get_hint_pep_generic_bases_unerased(hint)
                if is_hint_pep_generic(hint) else
                # Else, the zero or more arguments subscripting this hint.
                get_hint_pep_args(hint)
            )

        # Classify this hint *AFTER* all other assignments above.
        self._hint = hint

    # ..................{ GETTERS                           }..................
    def get_cause_or_none(self) -> Optional[str]:
        '''
        Human-readable string describing the failure of this pith to satisfy
        this PEP-compliant type hint if this pith fails to satisfy this pith
        *or* ``None`` otherwise (i.e., if this pith satisfies this hint).

        Design
        ----------
        This getter is intentionally generalized to support objects both
        satisfying and *not* satisfying hints as equally valid use cases. While
        the parent :func:`.errormain.raise_pep_call_exception` function
        calling this getter is *always* passed an object *not* satisfying the
        passed hint, this getter is under no such constraints. Why? Because
        this getter is also called to find which of an arbitrary number of
        objects transitively nested in the object passed to
        :func:`.errormain.raise_pep_call_exception` fails to satisfy the
        corresponding hint transitively nested in the hint passed to that
        function.

        For example, consider the PEP-compliant type hint ``List[Union[int,
        str]]`` describing a list whose items are either integers or strings
        and the list ``list(range(256)) + [False,]`` consisting of the integers
        0 through 255 followed by boolean ``False``. Since this list is a
        standard sequence, the
        :func:`._peperrorsequence.get_cause_or_none_sequence_standard`
        function must decide the cause of this list's failure to comply with
        this hint by finding the list item that is neither an integer nor a
        string, implemented by by iteratively passing each list item to the
        :func:`._peperrorunion.get_cause_or_none_union` function. Since
        the first 256 items of this list are integers satisfying this hint,
        :func:`._peperrorunion.get_cause_or_none_union` returns
        ``None`` to
        :func:`._peperrorsequence.get_cause_or_none_sequence_standard`
        before finally finding the non-compliant boolean item and returning the
        human-readable cause.

        Returns
        ----------
        Optional[str]
            Either:

            * If this object fails to satisfy this hint, human-readable string
            describing the failure of this object to do so.
            * Else, ``None``.

        Raises
        ----------
        _BeartypeCallHintPepRaiseException
            If this type hint is either:

            * PEP-noncompliant (e.g., tuple union).
            * PEP-compliant but no getter function has been implemented to
              handle this category of PEP-compliant type hint yet.
        '''

        # Getter function returning the desired string.
        get_cause_or_none = None

        # If this hint is ignorable, all possible objects satisfy this hint,
        # implying this hint *CANNOT* by definition be the cause of this
        # failure. In this case, immediately report None.
        if is_hint_ignorable(self.hint):
            return None
        # Else, this hint is unignorable.
        #
        # If *NO* sign uniquely identifies this hint, this hint is
        # PEP-noncompliant. In this case...
        elif self.hint_sign is None:
            # Avoid circular import dependencies.
            from beartype._decor._error._errortype import (
                get_cause_or_none_type,
                get_cause_or_none_types,
            )

            # Defer to the getter function supporting either...
            get_cause_or_none = (
                # If this hint is a tuple union, tuples of one or more
                # isinstanceable classes.
                get_cause_or_none_types
                if isinstance(self.hint, tuple) else
                # Else, isinstanceable classes under the generous assumption
                # this this hint is a type. If this is *NOT* the case, this
                # getter will raise a human-readable exception.
                get_cause_or_none_type
            )
        # Else, this hint is PEP-compliant.
        #
        # If this PEP-compliant hint is its own unsubscripted "typing"
        # attribute (e.g., "typing.List" rather than "typing.List[str]") and is
        # thus subscripted by *NO* child hints...
        elif self.hint is self.hint_sign:
            # If this hint is the non-standard "typing.NoReturn" type hint
            # specific to return values...
            if self.hint is NoReturn:
                # Avoid circular import dependencies.
                from beartype._decor._error._proposal._errorpep484noreturn import (
                    get_cause_or_none_noreturn)

                # Defer to the getter function specific to this hint.
                get_cause_or_none = get_cause_or_none_noreturn
            # Else, this hint is a standard PEP-compliant type hint supported
            # by both parameters and return values. In this case, we assume
            # this hint to originate from an origin type.
            else:
                # Avoid circular import dependencies.
                from beartype._decor._error._errortype import (
                    get_cause_or_none_type_origin)

                # Defer to the getter function supporting hints originating
                # from origin types.
                get_cause_or_none = get_cause_or_none_type_origin
        # Else, this PEP-compliant hint is *NOT* its own unsubscripted "typing"
        # attribute. In this case...
        else:
            # If this hint is neither...
            if not (
                # Subscripted by no child hints *NOR*...
                self.hint_childs or
                # An empty fixed-length tuple hint, whose PEP 585 (but *NOT*
                # PEP 484)-compliant implementation is subscripted by no child
                # hints *NOR*...
                is_hint_pep_tuple_empty(self.hint) or
                # A forward reference nor type variable, whose designs reside
                # well outside the standard "typing" dunder variable API and
                # are thus *NEVER* subscripted by child hints...
                is_hint_forwardref(self.hint) or
                is_hint_pep_typevar(self.hint)
            ):
            # Then this hint should have been subscripted by one or more child
            # hints but wasn't. In this case, raise an exception.
                raise _BeartypeCallHintPepRaiseException(
                    f'{self.exception_label} PEP type hint '
                    f'{repr(self.hint)} unsubscripted.'
                )
            # Else, this hint is subscripted by one or more child hints (e.g.,
            # "typing.List[str]" rather than "typing.List").

            # Avoid circular import dependencies.
            from beartype._decor._error.errormain import (
                PEP_HINT_SIGN_TO_GET_CAUSE_FUNC)

            # Getter function returning the desired string for this attribute
            # if any *OR* "None" otherwise.
            get_cause_or_none = PEP_HINT_SIGN_TO_GET_CAUSE_FUNC.get(
                self.hint_sign, None)

            # If no such function has been implemented to handle this attribute
            # yet, raise an exception.
            if get_cause_or_none is None:
                raise _BeartypeCallHintPepRaiseException(
                    f'{self.exception_label} PEP type hint '
                    f'{repr(self.hint)} unsupported (i.e., no '
                    f'"get_cause_or_none_"-prefixed getter function defined '
                    f'for this category of hint).'
                )
            # Else, a getter function has been implemented to handle this
            # attribute.

        # Call this getter function with ourselves and return the string
        # returned by this getter.
        return get_cause_or_none(self)

    # ..................{ PERMUTERS                         }..................
    def permute(self, **kwargs) -> 'CauseSleuth':
        '''
        Shallow copy of this object such that each the passed keyword argument
        overwrites the instance variable of the same name in this copy.

        Parameters
        ----------
        Keyword arguments of the same name and type as instance variables of
        this object (e.g., ``hint``, ``pith``).

        Returns
        ----------
        CauseSleuth
            Shallow copy of this object such that each keyword argument
            overwrites the instance variable of the same name in this copy.

        Raises
        ----------
        _BeartypeCallHintPepRaiseException
            If the name of any passed keyword argument is *not* the name of an
            existing instance variable of this object.

        Examples
        ----------
            >>> sleuth = CauseSleuth(
            ...     pith=[42,]
            ...     hint=typing.List[int],
            ...     cause_indent='',
            ...     exception_label='List of integers',
            ... )
            >>> sleuth_copy = sleuth.permute(pith=[24,])
            >>> sleuth_copy.pith
            [24,]
            >>> sleuth_copy.hint
            typing.List[int]
        '''

        # For the name of each passed keyword argument...
        for param_name in kwargs.keys():
            # If this name is *NOT* that of a parameter accepted by the
            # __init__() method, raise an exception.
            if param_name not in self._INIT_PARAM_NAMES:
                raise _BeartypeCallHintPepRaiseException(
                    f'{self.__class__}.__init__() parameter '
                    f'{param_name} unrecognized.'
                )

        # For the name of each parameter accepted by the __init__() method...
        for param_name in self._INIT_PARAM_NAMES:
            # If this parameter was *NOT* explicitly passed by the caller,
            # default this parameter to its current value from this object.
            if param_name not in kwargs:
                kwargs[param_name] = getattr(self, param_name)

        # Return a new instance of this class initialized with these arguments.
        return CauseSleuth(**kwargs)
