import typing

from dagster.core.errors import DagsterInvalidDefinitionError
from dagster.core.types import Bool, Float, Int, PythonDict, PythonSet, PythonTuple, String

from .builtin_enum import BuiltinEnum
from .typing_api import is_closed_python_optional_type, is_python_list_type
from .wrapping import remap_to_dagster_list_type, remap_to_dagster_optional_type

__RUNTIME_TYPE_REGISTRY = {}
'''Python types corresponding to user-defined RunTime types created using @dagster_type or
as_dagster_type are registered here so that we can remap the Python types to runtime types.'''


__ANONYMOUS_TYPE_REGISTRY = {}
'''Python types autogenerated by the system are registered here so that we can remap the
Python types to runtime types.'''


def register_python_type(python_type, runtime_type):
    if python_type in __RUNTIME_TYPE_REGISTRY:
        # This would be just a great place to insert a short URL pointing to the type system
        # documentation into the error message
        # https://github.com/dagster-io/dagster/issues/1831
        raise DagsterInvalidDefinitionError(
            'A Dagster runtime type has already been registered for the Python type {python_type}. '
            'You can resolve this collision by decorating a subclass of {python_type} with the '
            '@dagster_type decorator, instead of decorating {python_type} or passing it to '
            'as_dagster_type directly.'.format(python_type=python_type)
        )

    __RUNTIME_TYPE_REGISTRY[python_type] = runtime_type


def _clear_runtime_type_registry():
    '''Intended to support tests.'''
    __RUNTIME_TYPE_REGISTRY = {}


def create_anonymous_type(ttype):
    from .runtime import define_python_dagster_type

    if ttype in __ANONYMOUS_TYPE_REGISTRY:
        return __ANONYMOUS_TYPE_REGISTRY[ttype]

    dagster_type = define_python_dagster_type(
        name='Implicit[' + ttype.__name__ + ']',
        # Again, https://github.com/dagster-io/dagster/issues/1831 -- would be great to link to
        # type system docs here
        description=(
            'Anonymous Dagster type autogenerated to wrap the Python type {python_type}. In '
            'general, you may prefer to define your own types using the @dagster_type decorator '
            'or the as_dagster_type function.'
        ),
        python_type=ttype,
    )

    __ANONYMOUS_TYPE_REGISTRY[ttype] = dagster_type

    return dagster_type


def remap_python_type(ttype):
    from .config import ConfigType
    from .runtime import RuntimeType

    if ttype in __RUNTIME_TYPE_REGISTRY:
        return __RUNTIME_TYPE_REGISTRY[ttype]

    if BuiltinEnum.contains(ttype):
        return ttype

    if ttype == int:
        return Int
    if ttype == float:
        return Float
    if ttype == bool:
        return Bool
    if ttype == str:
        return String
    if ttype == dict:
        return PythonDict
    if ttype == tuple:
        return PythonTuple
    if ttype == set:
        return PythonSet

    if is_python_list_type(ttype):
        return remap_to_dagster_list_type(ttype)
    if is_closed_python_optional_type(ttype):
        return remap_to_dagster_optional_type(ttype)

    # In Python 3.7+, concrete parametrized generics are not instances of type
    # See: https://bugs.python.org/issue34568
    # In 3.8, we get the new typing.get_origin and typing.get_args APIs, but we will still need
    # the following for backwards compat.
    if not isinstance(ttype, type):
        return ttype

    # On Python 3.5 and 3.6, we need to check the __origin__ of parametrized generics. Note that
    # the more intuitive check issubclass(ttype, typing.Dict), etc., is not viable here because,
    # e.g., collections.Counter will pass this test, as will user-defined subclasses of the
    # built-ins.
    if hasattr(ttype, '__origin__'):
        if ttype.__origin__ in [typing.Dict, typing.Tuple, typing.List, typing.Set]:
            return ttype

    if issubclass(ttype, RuntimeType) or issubclass(ttype, ConfigType):
        return ttype

    if ttype in __ANONYMOUS_TYPE_REGISTRY:
        return __ANONYMOUS_TYPE_REGISTRY[ttype]

    return create_anonymous_type(ttype)
