import abc
import sys
import typing
from typing import (
    ClassVar, Type, Generic, Callable, GenericMeta, TypingMeta,
    Counter, DefaultDict, Deque, TypeVar, Tuple, Final, final,
    NewType, overload, Text, TYPE_CHECKING, Literal, TypedDict, Protocol,
    runtime_checkable,
    # We use internal typing helpers here, but this significantly reduces
    # code duplication. (Also this is only until Protocol is in typing.)
    _type_vars, _tp_cache, _type_check,
)

# Please keep __all__ alphabetized within each category.
__all__ = [
    # Super-special typing primitives.
    'ClassVar',
    'Final',
    'Protocol',
    'Type',
    'TypedDict',

    # Concrete collection types.
    'ContextManager',
    'Counter',
    'Deque',
    'DefaultDict',

    # One-off things.
    'final',
    'IntVar',
    'Literal',
    'NewType',
    'overload',
    'runtime_checkable',
    'Text',
    'TYPE_CHECKING',
]


if hasattr(typing, 'NoReturn'):
    NoReturn = typing.NoReturn
else:
    # TODO: Remove once typing.py has been updated
    class _NoReturnMeta(typing.TypingMeta):
        """Metaclass for NoReturn."""

        def __new__(cls, name, bases, namespace):
            cls.assert_no_subclassing(bases)
            self = super(_NoReturnMeta, cls).__new__(cls, name, bases, namespace)
            return self

    class _NoReturn(typing._FinalTypingBase):
        """Special type indicating functions that never return.
        Example::
          from typing import NoReturn
          def stop() -> NoReturn:
              raise Exception('no way')
        This type is invalid in other positions, e.g., ``List[NoReturn]``
        will fail in static type checkers.
        """
        __metaclass__ = _NoReturnMeta
        __slots__ = ()

        def __instancecheck__(self, obj):
            raise TypeError("NoReturn cannot be used with isinstance().")

        def __subclasscheck__(self, cls):
            raise TypeError("NoReturn cannot be used with issubclass().")

    NoReturn = _NoReturn(_root=True)


T_co = typing.TypeVar('T_co', covariant=True)

if hasattr(typing, 'ContextManager'):
    ContextManager = typing.ContextManager
else:
    # TODO: Remove once typing.py has been updated
    class ContextManager(typing.Generic[T_co]):
        __slots__ = ()

        def __enter__(self):
            return self

        @abc.abstractmethod
        def __exit__(self, exc_type, exc_value, traceback):
            return None

        @classmethod
        def __subclasshook__(cls, C):
            if cls is ContextManager:
                # In Python 3.6+, it is possible to set a method to None to
                # explicitly indicate that the class does not implement an ABC
                # (https://bugs.python.org/issue25958), but we do not support
                # that pattern here because this fallback class is only used
                # in Python 3.5 and earlier.
                if (any("__enter__" in B.__dict__ for B in C.__mro__) and
                    any("__exit__" in B.__dict__ for B in C.__mro__)):
                    return True
            return NotImplemented


def IntVar(name):
    return TypeVar(name)


def _is_dunder(name):
    """Returns True if name is a __dunder_variable_name__."""
    return len(name) > 4 and name.startswith('__') and name.endswith('__')


class AnnotatedMeta(GenericMeta):
    """Metaclass for Annotated"""

    def __new__(cls, name, bases, namespace, **kwargs):
        if any(b is not object for b in bases):
            raise TypeError("Cannot subclass %s" % Annotated)
        return super(AnnotatedMeta, cls).__new__(cls, name, bases, namespace, **kwargs)

    @property
    def __metadata__(self):
        return self._subs_tree()[2]

    def _tree_repr(self, tree):
        cls, origin, metadata = tree
        if not isinstance(origin, tuple):
            tp_repr = typing._type_repr(origin)
        else:
            tp_repr = origin[0]._tree_repr(origin)
        metadata_reprs = ", ".join(repr(arg) for arg in metadata)
        return '%s[%s, %s]' % (cls, tp_repr, metadata_reprs)

    def _subs_tree(self, tvars=None, args=None):
        if self is Annotated:
            return Annotated
        res = super(AnnotatedMeta, self)._subs_tree(tvars=tvars, args=args)
        # Flatten nested Annotated
        if isinstance(res[1], tuple) and res[1][0] is Annotated:
            sub_tp = res[1][1]
            sub_annot = res[1][2]
            return (Annotated, sub_tp, sub_annot + res[2])
        return res

    def _get_cons(self):
        """Return the class used to create instance of this type."""
        if self.__origin__ is None:
            raise TypeError("Cannot get the underlying type of a non-specialized "
                            "Annotated type.")
        tree = self._subs_tree()
        while isinstance(tree, tuple) and tree[0] is Annotated:
            tree = tree[1]
        if isinstance(tree, tuple):
            return tree[0]
        else:
            return tree

    @_tp_cache
    def __getitem__(self, params):
        if not isinstance(params, tuple):
            params = (params,)
        if self.__origin__ is not None:  # specializing an instantiated type
            return super(AnnotatedMeta, self).__getitem__(params)
        elif not isinstance(params, tuple) or len(params) < 2:
            raise TypeError("Annotated[...] should be instantiated with at "
                            "least two arguments (a type and an annotation).")
        else:
            msg = "Annotated[t, ...]: t must be a type."
            tp = typing._type_check(params[0], msg)
            metadata = tuple(params[1:])
        return self.__class__(
            self.__name__,
            self.__bases__,
            dict(self.__dict__),
            tvars=_type_vars((tp,)),
            # Metadata is a tuple so it won't be touched by _replace_args et al.
            args=(tp, metadata),
            origin=self,
        )

    def __call__(self, *args, **kwargs):
        cons = self._get_cons()
        result = cons(*args, **kwargs)
        try:
            result.__orig_class__ = self
        except AttributeError:
            pass
        return result

    def __getattr__(self, attr):
        # For simplicity we just don't relay all dunder names
        if self.__origin__ is not None and not _is_dunder(attr):
            return getattr(self._get_cons(), attr)
        raise AttributeError(attr)

    def __setattr__(self, attr, value):
        if _is_dunder(attr) or attr.startswith('_abc_'):
            super(AnnotatedMeta, self).__setattr__(attr, value)
        elif self.__origin__ is None:
            raise AttributeError(attr)
        else:
            setattr(self._get_cons(), attr, value)


class Annotated(object):
    """Add context specific metadata to a type.

    Example: Annotated[int, runtime_check.Unsigned] indicates to the
    hypothetical runtime_check module that this type is an unsigned int.
    Every other consumer of this type can ignore this metadata and treat
    this type as int.

    The first argument to Annotated must be a valid type, the remaining
    arguments are kept as a tuple in the __metadata__ field.

    Details:

    - It's an error to call `Annotated` with less than two arguments.
    - Nested Annotated are flattened::

        Annotated[Annotated[int, Ann1, Ann2], Ann3] == Annotated[int, Ann1, Ann2, Ann3]

    - Instantiating an annotated type is equivalent to instantiating the
    underlying type::

        Annotated[C, Ann1](5) == C(5)

    - Annotated can be used as a generic type alias::

        Optimized = Annotated[T, runtime.Optimize()]
        Optimized[int] == Annotated[int, runtime.Optimize()]

        OptimizedList = Annotated[List[T], runtime.Optimize()]
        OptimizedList[int] == Annotated[List[int], runtime.Optimize()]
    """
    __metaclass__ = AnnotatedMeta
    __slots__ = ()


# This alias exists for backwards compatibility.
runtime = runtime_checkable
