from __future__ import annotations

from collections.abc import Iterable, Mapping
from typing import Final, TypeVar, cast, overload

from mypy.nodes import ARG_STAR, FakeInfo, Var
from mypy.state import state
from mypy.types import (
    ANY_STRATEGY,
    AnyType,
    BoolTypeQuery,
    CallableType,
    DeletedType,
    ErasedType,
    FunctionLike,
    Instance,
    LiteralType,
    NoneType,
    Overloaded,
    Parameters,
    ParamSpecFlavor,
    ParamSpecType,
    PartialType,
    ProperType,
    TrivialSyntheticTypeTranslator,
    TupleType,
    Type,
    TypeAliasType,
    TypedDictType,
    TypeOfAny,
    TypeType,
    TypeVarId,
    TypeVarLikeType,
    TypeVarTupleType,
    TypeVarType,
    UnboundType,
    UninhabitedType,
    UnionType,
    UnpackType,
    flatten_nested_unions,
    get_proper_type,
    split_with_prefix_and_suffix,
)
from mypy.typevartuples import split_with_instance

# Solving the import cycle:
import mypy.type_visitor  # ruff: isort: skip

# WARNING: these functions should never (directly or indirectly) depend on
# is_subtype(), meet_types(), join_types() etc.
# TODO: add a static dependency test for this.


@overload
def expand_type(typ: CallableType, env: Mapping[TypeVarId, Type]) -> CallableType: ...


@overload
def expand_type(typ: ProperType, env: Mapping[TypeVarId, Type]) -> ProperType: ...


@overload
def expand_type(typ: Type, env: Mapping[TypeVarId, Type]) -> Type: ...


def expand_type(typ: Type, env: Mapping[TypeVarId, Type]) -> Type:
    """Substitute any type variable references in a type given by a type
    environment.
    """
    return typ.accept(ExpandTypeVisitor(env))


@overload
def expand_type_by_instance(typ: CallableType, instance: Instance) -> CallableType: ...


@overload
def expand_type_by_instance(typ: ProperType, instance: Instance) -> ProperType: ...


@overload
def expand_type_by_instance(typ: Type, instance: Instance) -> Type: ...


def expand_type_by_instance(typ: Type, instance: Instance) -> Type:
    """Substitute type variables in type using values from an Instance.
    Type variables are considered to be bound by the class declaration."""
    if not instance.args and not instance.type.has_type_var_tuple_type:
        return typ
    else:
        variables: dict[TypeVarId, Type] = {}
        if instance.type.has_type_var_tuple_type:
            assert instance.type.type_var_tuple_prefix is not None
            assert instance.type.type_var_tuple_suffix is not None

            args_prefix, args_middle, args_suffix = split_with_instance(instance)
            tvars_prefix, tvars_middle, tvars_suffix = split_with_prefix_and_suffix(
                tuple(instance.type.defn.type_vars),
                instance.type.type_var_tuple_prefix,
                instance.type.type_var_tuple_suffix,
            )
            tvar = tvars_middle[0]
            assert isinstance(tvar, TypeVarTupleType)
            variables = {tvar.id: TupleType(list(args_middle), tvar.tuple_fallback)}
            instance_args = args_prefix + args_suffix
            tvars = tvars_prefix + tvars_suffix
        else:
            tvars = tuple(instance.type.defn.type_vars)
            instance_args = instance.args

        for binder, arg in zip(tvars, instance_args):
            assert isinstance(binder, TypeVarLikeType)
            variables[binder.id] = arg

        return expand_type(typ, variables)


F = TypeVar("F", bound=FunctionLike)


def freshen_function_type_vars(callee: F) -> F:
    """Substitute fresh type variables for generic function type variables."""
    if isinstance(callee, CallableType):
        if not callee.is_generic():
            return callee
        tvs = []
        tvmap: dict[TypeVarId, Type] = {}
        for v in callee.variables:
            tv = v.new_unification_variable(v)
            tvs.append(tv)
            tvmap[v.id] = tv
        fresh = expand_type(callee, tvmap).copy_modified(variables=tvs)
        return cast(F, fresh)
    else:
        assert isinstance(callee, Overloaded)
        fresh_overload = Overloaded([freshen_function_type_vars(item) for item in callee.items])
        return cast(F, fresh_overload)


class HasGenericCallable(BoolTypeQuery):
    def __init__(self) -> None:
        super().__init__(ANY_STRATEGY)

    def visit_callable_type(self, t: CallableType) -> bool:
        return t.is_generic() or super().visit_callable_type(t)


# Share a singleton since this is performance sensitive
has_generic_callable: Final = HasGenericCallable()


T = TypeVar("T", bound=Type)


def freshen_all_functions_type_vars(t: T) -> T:
    result: Type
    has_generic_callable.reset()
    if not t.accept(has_generic_callable):
        return t  # Fast path to avoid expensive freshening
    else:
        result = t.accept(FreshenCallableVisitor())
        assert isinstance(result, type(t))
        return result


class FreshenCallableVisitor(mypy.type_visitor.TypeTranslator):
    def visit_callable_type(self, t: CallableType) -> Type:
        result = super().visit_callable_type(t)
        assert isinstance(result, ProperType) and isinstance(result, CallableType)
        return freshen_function_type_vars(result)

    def visit_type_alias_type(self, t: TypeAliasType) -> Type:
        # Same as for ExpandTypeVisitor
        return t.copy_modified(args=[arg.accept(self) for arg in t.args])


class ExpandTypeVisitor(TrivialSyntheticTypeTranslator):
    """Visitor that substitutes type variables with values."""

    variables: Mapping[TypeVarId, Type]  # TypeVar id -> TypeVar value

    def __init__(self, variables: Mapping[TypeVarId, Type]) -> None:
        super().__init__()
        self.variables = variables
        self.recursive_tvar_guard: dict[TypeVarId, Type | None] | None = None

    def visit_unbound_type(self, t: UnboundType) -> Type:
        return t

    def visit_any(self, t: AnyType) -> Type:
        return t

    def visit_none_type(self, t: NoneType) -> Type:
        return t

    def visit_uninhabited_type(self, t: UninhabitedType) -> Type:
        return t

    def visit_deleted_type(self, t: DeletedType) -> Type:
        return t

    def visit_erased_type(self, t: ErasedType) -> Type:
        # This may happen during type inference if some function argument
        # type is a generic callable, and its erased form will appear in inferred
        # constraints, then solver may check subtyping between them, which will trigger
        # unify_generic_callables(), this is why we can get here. Another example is
        # when inferring type of lambda in generic context, the lambda body contains
        # a generic method in generic class.
        return t

    def visit_instance(self, t: Instance) -> Type:
        if len(t.args) == 0:
            return t

        args = self.expand_type_tuple_with_unpack(t.args)

        if isinstance(t.type, FakeInfo):
            # The type checker expands function definitions and bodies
            # if they depend on constrained type variables but the body
            # might contain a tuple type comment (e.g., # type: (int, float)),
            # in which case 't.type' is not yet available.
            #
            # See: https://github.com/python/mypy/issues/16649
            return t.copy_modified(args=args)

        if t.type.fullname == "builtins.tuple":
            # Normalize Tuple[*Tuple[X, ...], ...] -> Tuple[X, ...]
            arg = args[0]
            if isinstance(arg, UnpackType):
                unpacked = get_proper_type(arg.type)
                if isinstance(unpacked, Instance):
                    # TODO: this and similar asserts below may be unsafe because get_proper_type()
                    # may be called during semantic analysis before all invalid types are removed.
                    assert unpacked.type.fullname == "builtins.tuple"
                    args = list(unpacked.args)
        return t.copy_modified(args=args)

    def visit_type_var(self, t: TypeVarType) -> Type:
        # Normally upper bounds can't contain other type variables, the only exception is
        # special type variable Self`0 <: C[T, S], where C is the class where Self is used.
        if t.id.is_self():
            t = t.copy_modified(upper_bound=t.upper_bound.accept(self))
        repl = self.variables.get(t.id, t)
        if isinstance(repl, ProperType) and isinstance(repl, Instance):
            # TODO: do we really need to do this?
            # If I try to remove this special-casing ~40 tests fail on reveal_type().
            return repl.copy_modified(last_known_value=None)
        if isinstance(repl, TypeVarType) and repl.has_default():
            if self.recursive_tvar_guard is None:
                self.recursive_tvar_guard = {}
            if (tvar_id := repl.id) in self.recursive_tvar_guard:
                return self.recursive_tvar_guard[tvar_id] or repl
            self.recursive_tvar_guard[tvar_id] = None
            repl.default = repl.default.accept(self)
            expanded = repl.accept(self)  # Note: `expanded is repl` may be true.
            repl = repl if isinstance(expanded, TypeVarType) else expanded
            self.recursive_tvar_guard[tvar_id] = repl
        return repl

    def visit_param_spec(self, t: ParamSpecType) -> Type:
        # Set prefix to something empty, so we don't duplicate it below.
        repl = self.variables.get(t.id, t.copy_modified(prefix=Parameters([], [], [])))
        if isinstance(repl, ParamSpecType):
            return repl.copy_modified(
                flavor=t.flavor,
                prefix=t.prefix.copy_modified(
                    arg_types=self.expand_types(t.prefix.arg_types) + repl.prefix.arg_types,
                    arg_kinds=t.prefix.arg_kinds + repl.prefix.arg_kinds,
                    arg_names=t.prefix.arg_names + repl.prefix.arg_names,
                ),
            )
        elif isinstance(repl, Parameters):
            assert t.flavor == ParamSpecFlavor.BARE
            return Parameters(
                self.expand_types(t.prefix.arg_types) + repl.arg_types,
                t.prefix.arg_kinds + repl.arg_kinds,
                t.prefix.arg_names + repl.arg_names,
                variables=[*t.prefix.variables, *repl.variables],
                imprecise_arg_kinds=repl.imprecise_arg_kinds,
            )
        else:
            # We could encode Any as trivial parameters etc., but it would be too verbose.
            # TODO: assert this is a trivial type, like Any, Never, or object.
            return repl

    def visit_type_var_tuple(self, t: TypeVarTupleType) -> Type:
        # Sometimes solver may need to expand a type variable with (a copy of) itself
        # (usually together with other TypeVars, but it is hard to filter out TypeVarTuples).
        repl = self.variables.get(t.id, t)
        if isinstance(repl, TypeVarTupleType):
            return repl
        elif isinstance(repl, ProperType) and isinstance(repl, (AnyType, UninhabitedType)):
            # Some failed inference scenarios will try to set all type variables to Never.
            # Instead of being picky and require all the callers to wrap them,
            # do this here instead.
            # Note: most cases when this happens are handled in expand unpack below, but
            # in rare cases (e.g. ParamSpec containing Unpack star args) it may be skipped.
            return t.tuple_fallback.copy_modified(args=[repl])
        raise NotImplementedError

    def visit_unpack_type(self, t: UnpackType) -> Type:
        # It is impossible to reasonably implement visit_unpack_type, because
        # unpacking inherently expands to something more like a list of types.
        #
        # Relevant sections that can call unpack should call expand_unpack()
        # instead.
        # However, if the item is a variadic tuple, we can simply carry it over.
        # In particular, if we expand A[*tuple[T, ...]] with substitutions {T: str},
        # it is hard to assert this without getting proper type. Another important
        # example is non-normalized types when called from semanal.py.
        return UnpackType(t.type.accept(self))

    def expand_unpack(self, t: UnpackType) -> list[Type]:
        assert isinstance(t.type, TypeVarTupleType)
        repl = get_proper_type(self.variables.get(t.type.id, t.type))
        if isinstance(repl, UnpackType):
            repl = get_proper_type(repl.type)
        if isinstance(repl, TupleType):
            return repl.items
        elif (
            isinstance(repl, Instance)
            and repl.type.fullname == "builtins.tuple"
            or isinstance(repl, TypeVarTupleType)
        ):
            return [UnpackType(typ=repl)]
        elif isinstance(repl, (AnyType, UninhabitedType)):
            # Replace *Ts = Any with *Ts = *tuple[Any, ...] and same for Never.
            # These types may appear here as a result of user error or failed inference.
            return [UnpackType(t.type.tuple_fallback.copy_modified(args=[repl]))]
        else:
            raise RuntimeError(f"Invalid type replacement to expand: {repl}")

    def visit_parameters(self, t: Parameters) -> Type:
        return t.copy_modified(arg_types=self.expand_types(t.arg_types))

    def interpolate_args_for_unpack(self, t: CallableType, var_arg: UnpackType) -> list[Type]:
        star_index = t.arg_kinds.index(ARG_STAR)
        prefix = self.expand_types(t.arg_types[:star_index])
        suffix = self.expand_types(t.arg_types[star_index + 1 :])

        var_arg_type = get_proper_type(var_arg.type)
        new_unpack: Type
        if isinstance(var_arg_type, TupleType):
            # We have something like Unpack[Tuple[Unpack[Ts], X1, X2]]
            expanded_tuple = var_arg_type.accept(self)
            assert isinstance(expanded_tuple, ProperType) and isinstance(expanded_tuple, TupleType)
            expanded_items = expanded_tuple.items
            fallback = var_arg_type.partial_fallback
            new_unpack = UnpackType(TupleType(expanded_items, fallback))
        elif isinstance(var_arg_type, TypeVarTupleType):
            # We have plain Unpack[Ts]
            fallback = var_arg_type.tuple_fallback
            expanded_items = self.expand_unpack(var_arg)
            new_unpack = UnpackType(TupleType(expanded_items, fallback))
        # Since get_proper_type() may be called in semanal.py before callable
        # normalization happens, we need to also handle non-normal cases here.
        elif isinstance(var_arg_type, Instance):
            # we have something like Unpack[Tuple[Any, ...]]
            new_unpack = UnpackType(var_arg.type.accept(self))
        else:
            # We have invalid type in Unpack. This can happen when expanding aliases
            # to Callable[[*Invalid], Ret]
            new_unpack = AnyType(TypeOfAny.from_error, line=var_arg.line, column=var_arg.column)
        return prefix + [new_unpack] + suffix

    def visit_callable_type(self, t: CallableType) -> CallableType:
        param_spec = t.param_spec()
        if param_spec is not None:
            repl = self.variables.get(param_spec.id)
            # If a ParamSpec in a callable type is substituted with a
            # callable type, we can't use normal substitution logic,
            # since ParamSpec is actually split into two components
            # *P.args and **P.kwargs in the original type. Instead, we
            # must expand both of them with all the argument types,
            # kinds and names in the replacement. The return type in
            # the replacement is ignored.
            if isinstance(repl, Parameters):
                # We need to expand both the types in the prefix and the ParamSpec itself
                expanded = t.copy_modified(
                    arg_types=self.expand_types(t.arg_types[:-2]) + repl.arg_types,
                    arg_kinds=t.arg_kinds[:-2] + repl.arg_kinds,
                    arg_names=t.arg_names[:-2] + repl.arg_names,
                    ret_type=t.ret_type.accept(self),
                    type_guard=(t.type_guard.accept(self) if t.type_guard is not None else None),
                    type_is=(t.type_is.accept(self) if t.type_is is not None else None),
                    imprecise_arg_kinds=(t.imprecise_arg_kinds or repl.imprecise_arg_kinds),
                    variables=[*repl.variables, *t.variables],
                )
                var_arg = expanded.var_arg()
                if var_arg is not None and isinstance(var_arg.typ, UnpackType):
                    # Sometimes we get new unpacks after expanding ParamSpec.
                    expanded.normalize_trivial_unpack()
                return expanded
            elif isinstance(repl, ParamSpecType):
                # We're substituting one ParamSpec for another; this can mean that the prefix
                # changes, e.g. substitute Concatenate[int, P] in place of Q.
                prefix = repl.prefix
                clean_repl = repl.copy_modified(prefix=Parameters([], [], []))
                return t.copy_modified(
                    arg_types=self.expand_types(t.arg_types[:-2])
                    + prefix.arg_types
                    + [
                        clean_repl.with_flavor(ParamSpecFlavor.ARGS),
                        clean_repl.with_flavor(ParamSpecFlavor.KWARGS),
                    ],
                    arg_kinds=t.arg_kinds[:-2] + prefix.arg_kinds + t.arg_kinds[-2:],
                    arg_names=t.arg_names[:-2] + prefix.arg_names + t.arg_names[-2:],
                    ret_type=t.ret_type.accept(self),
                    from_concatenate=t.from_concatenate or bool(repl.prefix.arg_types),
                    imprecise_arg_kinds=(t.imprecise_arg_kinds or prefix.imprecise_arg_kinds),
                )

        var_arg = t.var_arg()
        needs_normalization = False
        if var_arg is not None and isinstance(var_arg.typ, UnpackType):
            needs_normalization = True
            arg_types = self.interpolate_args_for_unpack(t, var_arg.typ)
        else:
            arg_types = self.expand_types(t.arg_types)
        expanded = t.copy_modified(
            arg_types=arg_types,
            ret_type=t.ret_type.accept(self),
            type_guard=(t.type_guard.accept(self) if t.type_guard is not None else None),
            type_is=(t.type_is.accept(self) if t.type_is is not None else None),
        )
        if needs_normalization:
            return expanded.with_normalized_var_args()
        return expanded

    def visit_overloaded(self, t: Overloaded) -> Type:
        items: list[CallableType] = []
        for item in t.items:
            new_item = item.accept(self)
            assert isinstance(new_item, ProperType)
            assert isinstance(new_item, CallableType)
            items.append(new_item)
        return Overloaded(items)

    def expand_type_list_with_unpack(self, typs: list[Type]) -> list[Type]:
        """Expands a list of types that has an unpack."""
        items: list[Type] = []
        for item in typs:
            if isinstance(item, UnpackType) and isinstance(item.type, TypeVarTupleType):
                items.extend(self.expand_unpack(item))
            else:
                items.append(item.accept(self))
        return items

    def expand_type_tuple_with_unpack(self, typs: tuple[Type, ...]) -> list[Type]:
        """Expands a tuple of types that has an unpack."""
        # Micro-optimization: Specialized variant of expand_type_list_with_unpack
        items: list[Type] = []
        for item in typs:
            if isinstance(item, UnpackType) and isinstance(item.type, TypeVarTupleType):
                items.extend(self.expand_unpack(item))
            else:
                items.append(item.accept(self))
        return items

    def visit_tuple_type(self, t: TupleType) -> Type:
        items = self.expand_type_list_with_unpack(t.items)
        if len(items) == 1:
            # Normalize Tuple[*Tuple[X, ...]] -> Tuple[X, ...]
            item = items[0]
            if isinstance(item, UnpackType):
                unpacked = get_proper_type(item.type)
                if isinstance(unpacked, Instance):
                    assert unpacked.type.fullname == "builtins.tuple"
                    if t.partial_fallback.type.fullname != "builtins.tuple":
                        # If it is a subtype (like named tuple) we need to preserve it,
                        # this essentially mimics the logic in tuple_fallback().
                        return t.partial_fallback.accept(self)
                    return unpacked
        fallback = t.partial_fallback.accept(self)
        assert isinstance(fallback, ProperType) and isinstance(fallback, Instance)
        return t.copy_modified(items=items, fallback=fallback)

    def visit_typeddict_type(self, t: TypedDictType) -> Type:
        if cached := self.get_cached(t):
            return cached
        fallback = t.fallback.accept(self)
        assert isinstance(fallback, ProperType) and isinstance(fallback, Instance)
        result = t.copy_modified(item_types=self.expand_types(t.items.values()), fallback=fallback)
        self.set_cached(t, result)
        return result

    def visit_literal_type(self, t: LiteralType) -> Type:
        # TODO: Verify this implementation is correct
        return t

    def visit_union_type(self, t: UnionType) -> Type:
        # Use cache to avoid O(n**2) or worse expansion of types during translation
        # (only for large unions, since caching adds overhead)
        use_cache = len(t.items) > 3
        if use_cache and (cached := self.get_cached(t)):
            return cached

        expanded = self.expand_types(t.items)
        # After substituting for type variables in t.items, some resulting types
        # might be subtypes of others, however calling  make_simplified_union()
        # can cause recursion, so we just remove strict duplicates.
        simplified = UnionType.make_union(
            remove_trivial(flatten_nested_unions(expanded)), t.line, t.column
        )
        # This call to get_proper_type() is unfortunate but is required to preserve
        # the invariant that ProperType will stay ProperType after applying expand_type(),
        # otherwise a single item union of a type alias will break it. Note this should not
        # cause infinite recursion since pathological aliases like A = Union[A, B] are
        # banned at the semantic analysis level.
        result = get_proper_type(simplified)

        if use_cache:
            self.set_cached(t, result)
        return result

    def visit_partial_type(self, t: PartialType) -> Type:
        return t

    def visit_type_type(self, t: TypeType) -> Type:
        # TODO: Verify that the new item type is valid (instance or
        # union of instances or Any).  Sadly we can't report errors
        # here yet.
        item = t.item.accept(self)
        return TypeType.make_normalized(item, is_type_form=t.is_type_form)

    def visit_type_alias_type(self, t: TypeAliasType) -> Type:
        # Target of the type alias cannot contain type variables (not bound by the type
        # alias itself), so we just expand the arguments.
        if len(t.args) == 0:
            return t
        args = self.expand_type_list_with_unpack(t.args)
        # TODO: normalize if target is Tuple, and args are [*tuple[X, ...]]?
        return t.copy_modified(args=args)

    def expand_types(self, types: Iterable[Type]) -> list[Type]:
        a: list[Type] = []
        for t in types:
            a.append(t.accept(self))
        return a


@overload
def expand_self_type(var: Var, typ: ProperType, replacement: ProperType) -> ProperType: ...


@overload
def expand_self_type(var: Var, typ: Type, replacement: Type) -> Type: ...


def expand_self_type(var: Var, typ: Type, replacement: Type) -> Type:
    """Expand appearances of Self type in a variable type."""
    if var.info.self_type is not None and not var.is_property:
        return expand_type(typ, {var.info.self_type.id: replacement})
    return typ


def remove_trivial(types: Iterable[Type]) -> list[Type]:
    """Make trivial simplifications on a list of types without calling is_subtype().

    This makes following simplifications:
        * Remove bottom types (taking into account strict optional setting)
        * Remove everything else if there is an `object`
        * Remove strict duplicate types
    """
    removed_none = False
    new_types = []
    all_types = set()
    for t in types:
        p_t = get_proper_type(t)
        if isinstance(p_t, UninhabitedType):
            continue
        if isinstance(p_t, NoneType) and not state.strict_optional:
            removed_none = True
            continue
        if isinstance(p_t, Instance) and p_t.type.fullname == "builtins.object":
            return [p_t]
        if p_t not in all_types:
            new_types.append(t)
            all_types.add(p_t)
    if new_types:
        return new_types
    if removed_none:
        return [NoneType()]
    return [UninhabitedType()]
