"""Calculation of the least upper bound types (joins)."""

from __future__ import annotations

from typing import Sequence, overload

import mypy.typeops
from mypy.expandtype import expand_type
from mypy.maptype import map_instance_to_supertype
from mypy.nodes import CONTRAVARIANT, COVARIANT, INVARIANT, VARIANCE_NOT_READY
from mypy.state import state
from mypy.subtypes import (
    SubtypeContext,
    find_member,
    is_equivalent,
    is_proper_subtype,
    is_protocol_implementation,
    is_subtype,
)
from mypy.types import (
    AnyType,
    CallableType,
    DeletedType,
    ErasedType,
    FunctionLike,
    Instance,
    LiteralType,
    NoneType,
    Overloaded,
    Parameters,
    ParamSpecType,
    PartialType,
    ProperType,
    TupleType,
    Type,
    TypeAliasType,
    TypedDictType,
    TypeOfAny,
    TypeType,
    TypeVarId,
    TypeVarLikeType,
    TypeVarTupleType,
    TypeVarType,
    TypeVisitor,
    UnboundType,
    UninhabitedType,
    UnionType,
    UnpackType,
    find_unpack_in_list,
    get_proper_type,
    get_proper_types,
    split_with_prefix_and_suffix,
)

class InstanceJoiner:
    def __init__(self) -> None:
        self.seen_instances: list[tuple[Instance, Instance]] = []

    def join_instances(self, t: Instance, s: Instance) -> ProperType:
        if (t, s) in self.seen_instances or (s, t) in self.seen_instances:
            return object_from_instance(t)

        self.seen_instances.append((t, s))

        # Calculate the join of two instance types
        if t.type == s.type:
            # Simplest case: join two types with the same base type (but
            # potentially different arguments).

            # Combine type arguments.
            args: list[Type] = []
            # N.B: We use zip instead of indexing because the lengths might have
            # mismatches during daemon reprocessing.
            if t.type.has_type_var_tuple_type:
                # We handle joins of variadic instances by simply creating correct mapping
                # for type arguments and compute the individual joins same as for regular
                # instances. All the heavy lifting is done in the join of tuple types.
                assert s.type.type_var_tuple_prefix is not None
                assert s.type.type_var_tuple_suffix is not None
                prefix = s.type.type_var_tuple_prefix
                suffix = s.type.type_var_tuple_suffix
                tvt = s.type.defn.type_vars[prefix]
                assert isinstance(tvt, TypeVarTupleType)
                fallback = tvt.tuple_fallback
                s_prefix, s_middle, s_suffix = split_with_prefix_and_suffix(s.args, prefix, suffix)
                t_prefix, t_middle, t_suffix = split_with_prefix_and_suffix(t.args, prefix, suffix)
                s_args = s_prefix + (TupleType(list(s_middle), fallback),) + s_suffix
                t_args = t_prefix + (TupleType(list(t_middle), fallback),) + t_suffix
            else:
                t_args = t.args
                s_args = s.args
            for ta, sa, type_var in zip(t_args, s_args, t.type.defn.type_vars):
                ta_proper = get_proper_type(ta)
                sa_proper = get_proper_type(sa)
                new_type: Type | None = None
                if isinstance(ta_proper, AnyType):
                    new_type = AnyType(TypeOfAny.from_another_any, ta_proper)
                elif isinstance(sa_proper, AnyType):
                    new_type = AnyType(TypeOfAny.from_another_any, sa_proper)
                elif isinstance(type_var, TypeVarType):
                    if type_var.variance in (COVARIANT, VARIANCE_NOT_READY):
                        new_type = join_types(ta, sa, self)
                        if len(type_var.values) != 0 and new_type not in type_var.values:
                            self.seen_instances.pop()
                            return object_from_instance(t)
                        if not is_subtype(new_type, type_var.upper_bound):
                            self.seen_instances.pop()
                            return object_from_instance(t)
                    # TODO: contravariant case should use meet but pass seen instances as
                    # an argument to keep track of recursive checks.
                    elif type_var.variance in (INVARIANT, CONTRAVARIANT):
                        if isinstance(ta_proper, UninhabitedType) and ta_proper.ambiguous:
                            new_type = sa
                        elif isinstance(sa_proper, UninhabitedType) and sa_proper.ambiguous:
                            new_type = ta
                        elif not is_equivalent(ta, sa):
                            self.seen_instances.pop()
                            return object_from_instance(t)
                        else:
                            # If the types are different but equivalent, then an Any is involved
                            # so using a join in the contravariant case is also OK.
                            new_type = join_types(ta, sa, self)
                elif isinstance(type_var, TypeVarTupleType):
                    new_type = get_proper_type(join_types(ta, sa, self))
                    # Put the joined arguments back into instance in the normal form:
                    #   a) Tuple[X, Y, Z] -> [X, Y, Z]
                    #   b) tuple[X, ...] -> [*tuple[X, ...]]
                    if isinstance(new_type, Instance):
                        assert new_type.type.fullname == "builtins.tuple"
                        new_type = UnpackType(new_type)
                    else:
                        assert isinstance(new_type, TupleType)
                        args.extend(new_type.items)
                        continue
                else:
                    # ParamSpec type variables behave the same, independent of variance
                    if not is_equivalent(ta, sa):
                        return get_proper_type(type_var.upper_bound)
                    new_type = join_types(ta, sa, self)
                assert new_type is not None
                args.append(new_type)
            result: ProperType = Instance(t.type, args)
        elif t.type.bases and is_proper_subtype(
            t, s, subtype_context=SubtypeContext(ignore_type_params=True)
        ):
            result = self.join_instances_via_supertype(t, s)
        else:
            # Now t is not a subtype of s, and t != s. Now s could be a subtype
            # of t; alternatively, we need to find a common supertype. This works
            # in of the both cases.
            result = self.join_instances_via_supertype(s, t)

        self.seen_instances.pop()
        return result

    def join_instances_via_supertype(self, t: Instance, s: Instance) -> ProperType:
        # Give preference to joins via duck typing relationship, so that
        # join(int, float) == float, for example.
        for p in t.type._promote:
            if is_subtype(p, s):
                return join_types(p, s, self)
        for p in s.type._promote:
            if is_subtype(p, t):
                return join_types(t, p, self)

        # Compute the "best" supertype of t when joined with s.
        # The definition of "best" may evolve; for now it is the one with
        # the longest MRO.  Ties are broken by using the earlier base.
        best: ProperType | None = None
        for base in t.type.bases:
            mapped = map_instance_to_supertype(t, base.type)
            res = self.join_instances(mapped, s)
            if best is None or is_better(res, best):
                best = res
        assert best is not None
        for promote in t.type._promote:
            if isinstance(promote, Instance):
                res = self.join_instances(promote, s)
                if is_better(res, best):
                    best = res
        return best

def join_simple(declaration: Type | None, s: Type, t: Type) -> ProperType:
    """Return a simple least upper bound given the declared type.

    This function should be only used by binder, and should not recurse.
    For all other uses, use `join_types()`.
    """
    declaration = get_proper_type(declaration)
    s = get_proper_type(s)
    t = get_proper_type(t)

    if (s.can_be_true, s.can_be_false) != (t.can_be_true, t.can_be_false):
        # if types are restricted in different ways, use the more general versions
        s = mypy.typeops.true_or_false(s)
        t = mypy.typeops.true_or_false(t)

    if isinstance(s, AnyType):
        return s

    if isinstance(s, ErasedType):
        return t

    if is_proper_subtype(s, t, ignore_promotions=True):
        return t

    if is_proper_subtype(t, s, ignore_promotions=True):
        return s

    if isinstance(declaration, UnionType):
        return mypy.typeops.make_simplified_union([s, t])

    if isinstance(s, NoneType) and not isinstance(t, NoneType):
        s, t = t, s

    if isinstance(s, UninhabitedType) and not isinstance(t, UninhabitedType):
        s, t = t, s

    # Meets/joins require callable type normalization.
    s, t = normalize_callables(s, t)

    if isinstance(s, UnionType) and not isinstance(t, UnionType):
        s, t = t, s

    value = t.accept(TypeJoinVisitor(s))
    if declaration is None or is_subtype(value, declaration):
        return value

    return declaration

def trivial_join(s: Type, t: Type) -> Type:
    """Return one of types (expanded) if it is a supertype of other, otherwise top type."""
    if is_subtype(s, t):
        return t
    elif is_subtype(t, s):
        return s
    else:
        return object_or_any_from_type(get_proper_type(t))

@overload
def join_types(
    s: ProperType, t: ProperType, instance_joiner: InstanceJoiner | None = None
) -> ProperType: ...

@overload
def join_types(s: Type, t: Type, instance_joiner: InstanceJoiner | None = None) -> Type: ...

def join_types(s: Type, t: Type, instance_joiner: InstanceJoiner | None = None) -> Type:
    """Return the least upper bound of s and t.

    For example, the join of 'int' and 'object' is 'object'.
    """
    if mypy.typeops.is_recursive_pair(s, t):
        # This case can trigger an infinite recursion, general support for this will be
        # tricky so we use a trivial join (like for protocols).
        return trivial_join(s, t)
    s = get_proper_type(s)
    t = get_proper_type(t)

    if (s.can_be_true, s.can_be_false) != (t.can_be_true, t.can_be_false):
        # if types are restricted in different ways, use the more general versions
        s = mypy.typeops.true_or_false(s)
        t = mypy.typeops.true_or_false(t)

    if isinstance(s, UnionType) and not isinstance(t, UnionType):
        s, t = t, s

    if isinstance(s, AnyType):
        return s

    if isinstance(s, ErasedType):
        return t

    if isinstance(s, NoneType) and not isinstance(t, NoneType):
        s, t = t, s

    if isinstance(s, UninhabitedType) and not isinstance(t, UninhabitedType):
        s, t = t, s

    # Meets/joins require callable type normalization.
    s, t = normalize_callables(s, t)

    # Use a visitor to handle non-trivial cases.
    return t.accept(TypeJoinVisitor(s, instance_joiner))

class TypeJoinVisitor(TypeVisitor[ProperType]):
    """Implementation of the least upper bound algorithm.

    Attributes:
      s: The other (left) type operand.
    """

    def __init__(self, s: ProperType, instance_joiner: InstanceJoiner | None = None) -> None:
        self.s = s
        self.instance_joiner = instance_joiner

    def visit_unbound_type(self, t: UnboundType) -> ProperType:
        return AnyType(TypeOfAny.special_form)

    def visit_union_type(self, t: UnionType) -> ProperType:
        if is_proper_subtype(self.s, t):
            return t
        else:
            return mypy.typeops.make_simplified_union([self.s, t])

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

    def visit_none_type(self, t: NoneType) -> ProperType:
        if state.strict_optional:
            if isinstance(self.s, (NoneType, UninhabitedType)):
                return t
            elif isinstance(self.s, UnboundType):
                return AnyType(TypeOfAny.special_form)
            else:
                return mypy.typeops.make_simplified_union([self.s, t])
        else:
            return self.s

    def visit_uninhabited_type(self, t: UninhabitedType) -> ProperType:
        return self.s

    def visit_deleted_type(self, t: DeletedType) -> ProperType:
        return self.s

    def visit_erased_type(self, t: ErasedType) -> ProperType:
        return self.s

    def visit_type_var(self, t: TypeVarType) -> ProperType:
        if isinstance(self.s, TypeVarType) and self.s.id == t.id:
            return self.s
        else:
            return self.default(self.s)

    def visit_param_spec(self, t: ParamSpecType) -> ProperType:
        if self.s == t:
            return t
        return self.default(self.s)

    def visit_type_var_tuple(self, t: TypeVarTupleType) -> ProperType:
        if self.s == t:
            return t
        return self.default(self.s)

    def visit_unpack_type(self, t: UnpackType) -> UnpackType:
        raise NotImplementedError

    def visit_parameters(self, t: Parameters) -> ProperType:
        if isinstance(self.s, Parameters):
            if len(t.arg_types) != len(self.s.arg_types):
                return self.default(self.s)
            from mypy.meet import meet_types

            return t.copy_modified(
                arg_types=[
                    meet_types(s_a, t_a) for s_a, t_a in zip(self.s.arg_types, t.arg_types)
                ],
                arg_names=combine_arg_names(self.s, t),
            )
        else:
            return self.default(self.s)

    def visit_instance(self, t: Instance) -> ProperType:
        if isinstance(self.s, Instance):
            if self.instance_joiner is None:
                self.instance_joiner = InstanceJoiner()
            nominal = self.instance_joiner.join_instances(t, self.s)
            structural: Instance | None = None
            if t.type.is_protocol and is_protocol_implementation(self.s, t):
                structural = t
            elif self.s.type.is_protocol and is_protocol_implementation(t, self.s):
                structural = self.s
            # Structural join is preferred in the case where we have found both
            # structural and nominal and they have same MRO length (see two comments
            # in join_instances_via_supertype). Otherwise, just return the nominal join.
            if not structural or is_better(nominal, structural):
                return nominal
            return structural
        elif isinstance(self.s, FunctionLike):
            if t.type.is_protocol:
                call = unpack_callback_protocol(t)
                if call:
                    return join_types(call, self.s)
            return join_types(t, self.s.fallback)
        elif isinstance(self.s, TypeType):
            return join_types(t, self.s)
        elif isinstance(self.s, TypedDictType):
            return join_types(t, self.s)
        elif isinstance(self.s, TupleType):
            return join_types(t, self.s)
        elif isinstance(self.s, LiteralType):
            return join_types(t, self.s)
        else:
            return self.default(self.s)

    def visit_callable_type(self, t: CallableType) -> ProperType:
        if isinstance(self.s, CallableType) and is_similar_callables(t, self.s):
            if is_equivalent(t, self.s):
                return combine_similar_callables(t, self.s)
            result = join_similar_callables(t, self.s)
            # We set the from_type_type flag to suppress error when a collection of
            # concrete class objects gets inferred as their common abstract superclass.
            if not (
                (t.is_type_obj() and t.type_object().is_abstract)
                or (self.s.is_type_obj() and self.s.type_object().is_abstract)
            ):
                result.from_type_type = True
            if any(
                isinstance(tp, (NoneType, UninhabitedType))
                for tp in get_proper_types(result.arg_types)
            ):
                # We don't want to return unusable Callable, attempt fallback instead.
                return join_types(t.fallback, self.s)
            return result
        elif isinstance(self.s, Overloaded):
            # Switch the order of arguments to that we'll get to visit_overloaded.
            return join_types(t, self.s)
        elif isinstance(self.s, Instance) and self.s.type.is_protocol:
            call = unpack_callback_protocol(self.s)
            if call:
                return join_types(t, call)
        return join_types(t.fallback, self.s)

    def visit_overloaded(self, t: Overloaded) -> ProperType:
        # This is more complex than most other cases. Here are some
        # examples that illustrate how this works.
        #
        # First let's define a concise notation:
        #  - Cn are callable types (for n in 1, 2, ...)
        #  - Ov(C1, C2, ...) is an overloaded type with items C1, C2, ...
        #  - Callable[[T, ...], S] is written as [T, ...] -> S.
        #
        # We want some basic properties to hold (assume Cn are all
        # unrelated via Any-similarity):
        #
        #   join(Ov(C1, C2), C1) == C1
        #   join(Ov(C1, C2), Ov(C1, C2)) == Ov(C1, C2)
        #   join(Ov(C1, C2), Ov(C1, C3)) == C1
        #   join(Ov(C2, C2), C3) == join of fallback types
        #
        # The presence of Any types makes things more interesting. The join is the
        # most general type we can get with respect to Any:
        #
        #   join(Ov([int] -> int, [str] -> str), [Any] -> str) == Any -> str
        #
        # We could use a simplification step that removes redundancies, but that's not
        # implemented right now. Consider this example, where we get a redundancy:
        #
        #   join(Ov([int, Any] -> Any, [str, Any] -> Any), [Any, int] -> Any) ==
        #       Ov([Any, int] -> Any, [Any, int] -> Any)
        #
        # TODO: Consider more cases of callable subtyping.
        result: list[CallableType] = []
        s = self.s
        if isinstance(s, FunctionLike):
            # The interesting case where both types are function types.
            for t_item in t.items:
                for s_item in s.items:
                    if is_similar_callables(t_item, s_item):
                        if is_equivalent(t_item, s_item):
                            result.append(combine_similar_callables(t_item, s_item))
                        elif is_subtype(t_item, s_item):
                            result.append(s_item)
            if result:
                # TODO: Simplify redundancies from the result.
                if len(result) == 1:
                    return result[0]
                else:
                    return Overloaded(result)
            return join_types(t.fallback, s.fallback)
        elif isinstance(s, Instance) and s.type.is_protocol:
            call = unpack_callback_protocol(s)
            if call:
                return join_types(t, call)
        return join_types(t.fallback, s)

    def join_tuples(self, s: TupleType, t: TupleType) -> list[Type] | None:
        """Join two tuple types while handling variadic entries.

        This is surprisingly tricky, and we don't handle some tricky corner cases.
        Most of the trickiness comes from the variadic tuple items like *tuple[X, ...]
        since they can have arbitrary partial overlaps (while *Ts can't be split).
        """
        s_unpack_index = find_unpack_in_list(s.items)
        t_unpack_index = find_unpack_in_list(t.items)
        if s_unpack_index is None and t_unpack_index is None:
            if s.length() == t.length():
                items: list[Type] = []
                for i in range(t.length()):
                    items.append(join_types(t.items[i], s.items[i]))
                return items
            return None
        if s_unpack_index is not None and t_unpack_index is not None:
            # The most complex case: both tuples have an upack item.
            s_unpack = s.items[s_unpack_index]
            assert isinstance(s_unpack, UnpackType)
            s_unpacked = get_proper_type(s_unpack.type)
            t_unpack = t.items[t_unpack_index]
            assert isinstance(t_unpack, UnpackType)
            t_unpacked = get_proper_type(t_unpack.type)
            if s.length() == t.length() and s_unpack_index == t_unpack_index:
                # We can handle a case where arity is perfectly aligned, e.g.
                # join(Tuple[X1, *tuple[Y1, ...], Z1], Tuple[X2, *tuple[Y2, ...], Z2]).
                # We can essentially perform the join elementwise.
                prefix_len = t_unpack_index
                suffix_len = t.length() - t_unpack_index - 1
                items = []
                for si, ti in zip(s.items[:prefix_len], t.items[:prefix_len]):
                    items.append(join_types(si, ti))
                joined = join_types(s_unpacked, t_unpacked)
                if isinstance(joined, TypeVarTupleType):
                    items.append(UnpackType(joined))
                elif isinstance(joined, Instance) and joined.type.fullname == "builtins.tuple":
                    items.append(UnpackType(joined))
                else:
                    if isinstance(t_unpacked, Instance):
                        assert t_unpacked.type.fullname == "builtins.tuple"
                        tuple_instance = t_unpacked
                    else:
                        assert isinstance(t_unpacked, TypeVarTupleType)
                        tuple_instance = t_unpacked.tuple_fallback
                    items.append(
                        UnpackType(
                            tuple_instance.copy_modified(
                                args=[object_from_instance(tuple_instance)]
                            )
                        )
                    )
                if suffix_len:
                    for si, ti in zip(s.items[-suffix_len:], t.items[-suffix_len:]):
                        items.append(join_types(si, ti))
                return items
            if s.length() == 1 or t.length() == 1:
                # Another case we can handle is when one of tuple is purely variadic
                # (i.e. a non-normalized form of tuple[X, ...]), in this case the join
                # will be again purely variadic.
                if not (isinstance(s_unpacked, Instance) and isinstance(t_unpacked, Instance)):
                    return None
                assert s_unpacked.type.fullname == "builtins.tuple"
                assert t_unpacked.type.fullname == "builtins.tuple"
                mid_joined = join_types(s_unpacked.args[0], t_unpacked.args[0])
                t_other = [a for i, a in enumerate(t.items) if i != t_unpack_index]
                s_other = [a for i, a in enumerate(s.items) if i != s_unpack_index]
                other_joined = join_type_list(s_other + t_other)
                mid_joined = join_types(mid_joined, other_joined)
                return [UnpackType(s_unpacked.copy_modified(args=[mid_joined]))]
            # TODO: are there other case we can handle (e.g. both prefix/suffix are shorter)?
            return None
        if s_unpack_index is not None:
            variadic = s
            unpack_index = s_unpack_index
            fixed = t
        else:
            assert t_unpack_index is not None
            variadic = t
            unpack_index = t_unpack_index
            fixed = s
        # Case where one tuple has variadic item and the other one doesn't. The join will
        # be variadic, since fixed tuple is a subtype of variadic, but not vice versa.
        unpack = variadic.items[unpack_index]
        assert isinstance(unpack, UnpackType)
        unpacked = get_proper_type(unpack.type)
        if not isinstance(unpacked, Instance):
            return None
        if fixed.length() < variadic.length() - 1:
            # There are no non-trivial types that are supertype of both.
            return None
        prefix_len = unpack_index
        suffix_len = variadic.length() - prefix_len - 1
        prefix, middle, suffix = split_with_prefix_and_suffix(
            tuple(fixed.items), prefix_len, suffix_len
        )
        items = []
        for fi, vi in zip(prefix, variadic.items[:prefix_len]):
            items.append(join_types(fi, vi))
        mid_joined = join_type_list(list(middle))
        mid_joined = join_types(mid_joined, unpacked.args[0])
        items.append(UnpackType(unpacked.copy_modified(args=[mid_joined])))
        if suffix_len:
            for fi, vi in zip(suffix, variadic.items[-suffix_len:]):
                items.append(join_types(fi, vi))
        return items

    def visit_tuple_type(self, t: TupleType) -> ProperType:
        # When given two fixed-length tuples:
        # * If they have the same length, join their subtypes item-wise:
        #   Tuple[int, bool] + Tuple[bool, bool] becomes Tuple[int, bool]
        # * If lengths do not match, return a variadic tuple:
        #   Tuple[bool, int] + Tuple[bool] becomes Tuple[int, ...]
        #
        # Otherwise, `t` is a fixed-length tuple but `self.s` is NOT:
        # * Joining with a variadic tuple returns variadic tuple:
        #   Tuple[int, bool] + Tuple[bool, ...] becomes Tuple[int, ...]
        # * Joining with any Sequence also returns a Sequence:
        #   Tuple[int, bool] + List[bool] becomes Sequence[int]
        if isinstance(self.s, TupleType):
            if self.instance_joiner is None:
                self.instance_joiner = InstanceJoiner()
            fallback = self.instance_joiner.join_instances(
                mypy.typeops.tuple_fallback(self.s), mypy.typeops.tuple_fallback(t)
            )
            assert isinstance(fallback, Instance)
            items = self.join_tuples(self.s, t)
            if items is not None:
                return TupleType(items, fallback)
            else:
                # TODO: should this be a default fallback behaviour like for meet?
                if is_proper_subtype(self.s, t):
                    return t
                if is_proper_subtype(t, self.s):
                    return self.s
                return fallback
        else:
            return join_types(self.s, mypy.typeops.tuple_fallback(t))

    def visit_typeddict_type(self, t: TypedDictType) -> ProperType:
        if isinstance(self.s, TypedDictType):
            items = {
                item_name: s_item_type
                for (item_name, s_item_type, t_item_type) in self.s.zip(t)
                if (
                    is_equivalent(s_item_type, t_item_type)
                    and (item_name in t.required_keys) == (item_name in self.s.required_keys)
                )
            }
            fallback = self.s.create_anonymous_fallback()
            all_keys = set(items.keys())
            # We need to filter by items.keys() since some required keys present in both t and
            # self.s might be missing from the join if the types are incompatible.
            required_keys = all_keys & t.required_keys & self.s.required_keys
            # If one type has a key as readonly, we mark it as readonly for both:
            readonly_keys = (t.readonly_keys | t.readonly_keys) & all_keys
            return TypedDictType(items, required_keys, readonly_keys, fallback)
        elif isinstance(self.s, Instance):
            return join_types(self.s, t.fallback)
        else:
            return self.default(self.s)

    def visit_literal_type(self, t: LiteralType) -> ProperType:
        if isinstance(self.s, LiteralType):
            if t == self.s:
                return t
            if self.s.fallback.type.is_enum and t.fallback.type.is_enum:
                return mypy.typeops.make_simplified_union([self.s, t])
            return join_types(self.s.fallback, t.fallback)
        else:
            return join_types(self.s, t.fallback)

    def visit_partial_type(self, t: PartialType) -> ProperType:
        # We only have partial information so we can't decide the join result. We should
        # never get here.
        assert False, "Internal error"

    def visit_type_type(self, t: TypeType) -> ProperType:
        if isinstance(self.s, TypeType):
            return TypeType.make_normalized(join_types(t.item, self.s.item), line=t.line)
        elif isinstance(self.s, Instance) and self.s.type.fullname == "builtins.type":
            return self.s
        else:
            return self.default(self.s)

    def visit_type_alias_type(self, t: TypeAliasType) -> ProperType:
        assert False, f"This should be never called, got {t}"

    def default(self, typ: Type) -> ProperType:
        typ = get_proper_type(typ)
        if isinstance(typ, Instance):
            return object_from_instance(typ)
        elif isinstance(typ, UnboundType):
            return AnyType(TypeOfAny.special_form)
        elif isinstance(typ, TupleType):
            return self.default(mypy.typeops.tuple_fallback(typ))
        elif isinstance(typ, TypedDictType):
            return self.default(typ.fallback)
        elif isinstance(typ, FunctionLike):
            return self.default(typ.fallback)
        elif isinstance(typ, TypeVarType):
            return self.default(typ.upper_bound)
        elif isinstance(typ, ParamSpecType):
            return self.default(typ.upper_bound)
        else:
            return AnyType(TypeOfAny.special_form)

def is_better(t: Type, s: Type) -> bool:
    # Given two possible results from join_instances_via_supertype(),
    # indicate whether t is the better one.
    t = get_proper_type(t)
    s = get_proper_type(s)

    if isinstance(t, Instance):
        if not isinstance(s, Instance):
            return True
        # Use len(mro) as a proxy for the better choice.
        if len(t.type.mro) > len(s.type.mro):
            return True
    return False

def normalize_callables(s: ProperType, t: ProperType) -> tuple[ProperType, ProperType]:
    if isinstance(s, (CallableType, Overloaded)):
        s = s.with_unpacked_kwargs()
    if isinstance(t, (CallableType, Overloaded)):
        t = t.with_unpacked_kwargs()
    return s, t

def is_similar_callables(t: CallableType, s: CallableType) -> bool:
    """Return True if t and s have identical numbers of
    arguments, default arguments and varargs.
    """
    return (
        len(t.arg_types) == len(s.arg_types)
        and t.min_args == s.min_args
        and t.is_var_arg == s.is_var_arg
    )

def update_callable_ids(c: CallableType, ids: list[TypeVarId]) -> CallableType:
    tv_map = {}
    tvs = []
    for tv, new_id in zip(c.variables, ids):
        new_tv = tv.copy_modified(id=new_id)
        tvs.append(new_tv)
        tv_map[tv.id] = new_tv
    return expand_type(c, tv_map).copy_modified(variables=tvs)

def match_generic_callables(t: CallableType, s: CallableType) -> tuple[CallableType, CallableType]:
    # The case where we combine/join/meet similar callables, situation where both are generic
    # requires special care. A more principled solution may involve unify_generic_callable(),
    # but it would have two problems:
    #   * This adds risk of infinite recursion: e.g. join -> unification -> solver -> join
    #   * Using unification is an incorrect thing for meets, as it "widens" the types
    # Finally, this effectively falls back to an old behaviour before namespaces were added to
    # type variables, and it worked relatively well.
    max_len = max(len(t.variables), len(s.variables))
    min_len = min(len(t.variables), len(s.variables))
    if min_len == 0:
        return t, s
    new_ids = [TypeVarId.new(meta_level=0) for _ in range(max_len)]
    # Note: this relies on variables being in order they appear in function definition.
    return update_callable_ids(t, new_ids), update_callable_ids(s, new_ids)

def join_similar_callables(t: CallableType, s: CallableType) -> CallableType:
    t, s = match_generic_callables(t, s)
    arg_types: list[Type] = []
    for i in range(len(t.arg_types)):
        arg_types.append(safe_meet(t.arg_types[i], s.arg_types[i]))
    # TODO in combine_similar_callables also applies here (names and kinds; user metaclasses)
    # The fallback type can be either 'function', 'type', or some user-provided metaclass.
    # The result should always use 'function' as a fallback if either operands are using it.
    if t.fallback.type.fullname == "builtins.function":
        fallback = t.fallback
    else:
        fallback = s.fallback
    return t.copy_modified(
        arg_types=arg_types,
        arg_names=combine_arg_names(t, s),
        ret_type=join_types(t.ret_type, s.ret_type),
        fallback=fallback,
        name=None,
    )

def safe_join(t: Type, s: Type) -> Type:
    # This is a temporary solution to prevent crashes in combine_similar_callables() etc.,
    # until relevant TODOs on handling arg_kinds will be addressed there.
    if not isinstance(t, UnpackType) and not isinstance(s, UnpackType):
        return join_types(t, s)
    if isinstance(t, UnpackType) and isinstance(s, UnpackType):
        return UnpackType(join_types(t.type, s.type))
    return object_or_any_from_type(get_proper_type(t))

def safe_meet(t: Type, s: Type) -> Type:
    # Similar to above but for meet_types().
    from mypy.meet import meet_types

    if not isinstance(t, UnpackType) and not isinstance(s, UnpackType):
        return meet_types(t, s)
    if isinstance(t, UnpackType) and isinstance(s, UnpackType):
        unpacked = get_proper_type(t.type)
        if isinstance(unpacked, TypeVarTupleType):
            fallback_type = unpacked.tuple_fallback.type
        elif isinstance(unpacked, TupleType):
            fallback_type = unpacked.partial_fallback.type
        else:
            assert isinstance(unpacked, Instance) and unpacked.type.fullname == "builtins.tuple"
            fallback_type = unpacked.type
        res = meet_types(t.type, s.type)
        if isinstance(res, UninhabitedType):
            res = Instance(fallback_type, [res])
        return UnpackType(res)
    return UninhabitedType()

def combine_similar_callables(t: CallableType, s: CallableType) -> CallableType:
    t, s = match_generic_callables(t, s)
    arg_types: list[Type] = []
    for i in range(len(t.arg_types)):
        arg_types.append(safe_join(t.arg_types[i], s.arg_types[i]))
    # TODO kinds and argument names
    # TODO what should happen if one fallback is 'type' and the other is a user-provided metaclass?
    # The fallback type can be either 'function', 'type', or some user-provided metaclass.
    # The result should always use 'function' as a fallback if either operands are using it.
    if t.fallback.type.fullname == "builtins.function":
        fallback = t.fallback
    else:
        fallback = s.fallback
    return t.copy_modified(
        arg_types=arg_types,
        arg_names=combine_arg_names(t, s),
        ret_type=join_types(t.ret_type, s.ret_type),
        fallback=fallback,
        name=None,
    )

def combine_arg_names(
    t: CallableType | Parameters, s: CallableType | Parameters
) -> list[str | None]:
    """Produces a list of argument names compatible with both callables.

    For example, suppose 't' and 's' have the following signatures:

    - t: (a: int, b: str, X: str) -> None
    - s: (a: int, b: str, Y: str) -> None

    This function would return ["a", "b", None]. This information
    is then used above to compute the join of t and s, which results
    in a signature of (a: int, b: str, str) -> None.

    Note that the third argument's name is omitted and 't' and 's'
    are both valid subtypes of this inferred signature.

    Precondition: is_similar_types(t, s) is true.
    """
    num_args = len(t.arg_types)
    new_names = []
    for i in range(num_args):
        t_name = t.arg_names[i]
        s_name = s.arg_names[i]
        if t_name == s_name or t.arg_kinds[i].is_named() or s.arg_kinds[i].is_named():
            new_names.append(t_name)
        else:
            new_names.append(None)
    return new_names

def object_from_instance(instance: Instance) -> Instance:
    """Construct the type 'builtins.object' from an instance type."""
    # Use the fact that 'object' is always the last class in the mro.
    res = Instance(instance.type.mro[-1], [])
    return res

def object_or_any_from_type(typ: ProperType) -> ProperType:
    # Similar to object_from_instance() but tries hard for all types.
    # TODO: find a better way to get object, or make this more reliable.
    if isinstance(typ, Instance):
        return object_from_instance(typ)
    elif isinstance(typ, (CallableType, TypedDictType, LiteralType)):
        return object_from_instance(typ.fallback)
    elif isinstance(typ, TupleType):
        return object_from_instance(typ.partial_fallback)
    elif isinstance(typ, TypeType):
        return object_or_any_from_type(typ.item)
    elif isinstance(typ, TypeVarLikeType) and isinstance(typ.upper_bound, ProperType):
        return object_or_any_from_type(typ.upper_bound)
    elif isinstance(typ, UnionType):
        for item in typ.items:
            if isinstance(item, ProperType):
                candidate = object_or_any_from_type(item)
                if isinstance(candidate, Instance):
                    return candidate
    elif isinstance(typ, UnpackType):
        object_or_any_from_type(get_proper_type(typ.type))
    return AnyType(TypeOfAny.implementation_artifact)

def join_type_list(types: Sequence[Type]) -> Type:
    if not types:
        # This is a little arbitrary but reasonable. Any empty tuple should be compatible
        # with all variable length tuples, and this makes it possible.
        return UninhabitedType()
    joined = types[0]
    for t in types[1:]:
        joined = join_types(joined, t)
    return joined

def unpack_callback_protocol(t: Instance) -> ProperType | None:
    assert t.type.is_protocol
    if t.type.protocol_members == ["__call__"]:
        return get_proper_type(find_member("__call__", t, t, is_operator=True))
    return None


from __future__ import annotations

from typing import Any, Final, Iterable, Optional, Tuple
from typing_extensions import TypeAlias as _TypeAlias

from mypy.nodes import (
    LITERAL_NO,
    LITERAL_TYPE,
    LITERAL_YES,
    AssertTypeExpr,
    AssignmentExpr,
    AwaitExpr,
    BytesExpr,
    CallExpr,
    CastExpr,
    ComparisonExpr,
    ComplexExpr,
    ConditionalExpr,
    DictExpr,
    DictionaryComprehension,
    EllipsisExpr,
    EnumCallExpr,
    Expression,
    FloatExpr,
    GeneratorExpr,
    IndexExpr,
    IntExpr,
    LambdaExpr,
    ListComprehension,
    ListExpr,
    MemberExpr,
    NamedTupleExpr,
    NameExpr,
    NewTypeExpr,
    OpExpr,
    ParamSpecExpr,
    PromoteExpr,
    RevealExpr,
    SetComprehension,
    SetExpr,
    SliceExpr,
    StarExpr,
    StrExpr,
    SuperExpr,
    TempNode,
    TupleExpr,
    TypeAliasExpr,
    TypeApplication,
    TypedDictExpr,
    TypeVarExpr,
    TypeVarTupleExpr,
    UnaryExpr,
    Var,
    YieldExpr,
    YieldFromExpr,
)
from mypy.visitor import ExpressionVisitor

# [Note Literals and literal_hash]
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
# Mypy uses the term "literal" to refer to any expression built out of
# the following:
#
# * Plain literal expressions, like `1` (integer, float, string, etc.)
#
# * Compound literal expressions, like `(lit1, lit2)` (list, dict,
#   set, or tuple)
#
# * Operator expressions, like `lit1 + lit2`
#
# * Variable references, like `x`
#
# * Member references, like `lit.m`
#
# * Index expressions, like `lit[0]`
#
# A typical "literal" looks like `x[(i,j+1)].m`.
#
# An expression that is a literal has a `literal_hash`, with the
# following properties.
#
# * `literal_hash` is a Key: a tuple containing basic data types and
#   possibly other Keys. So it can be used as a key in a dictionary
#   that will be compared by value (as opposed to the Node itself,
#   which is compared by identity).
#
# * Two expressions have equal `literal_hash`es if and only if they
#   are syntactically equal expressions. (NB: Actually, we also
#   identify as equal expressions like `3` and `3.0`; is this a good
#   idea?)
#
# * The elements of `literal_hash` that are tuples are exactly the
#   subexpressions of the original expression (e.g. the base and index
#   of an index expression, or the operands of an operator expression).

def literal(e: Expression) -> int:
    if isinstance(e, ComparisonExpr):
        return min(literal(o) for o in e.operands)

    elif isinstance(e, OpExpr):
        return min(literal(e.left), literal(e.right))

    elif isinstance(e, (MemberExpr, UnaryExpr, StarExpr)):
        return literal(e.expr)

    elif isinstance(e, AssignmentExpr):
        return literal(e.target)

    elif isinstance(e, IndexExpr):
        if literal(e.index) == LITERAL_YES:
            return literal(e.base)
        else:
            return LITERAL_NO

    elif isinstance(e, NameExpr):
        if isinstance(e.node, Var) and e.node.is_final and e.node.final_value is not None:
            return LITERAL_YES
        return LITERAL_TYPE

    if isinstance(e, (IntExpr, FloatExpr, ComplexExpr, StrExpr, BytesExpr)):
        return LITERAL_YES

    if literal_hash(e):
        return LITERAL_YES

    return LITERAL_NO

Key: _TypeAlias = Tuple[Any, ...]

def subkeys(key: Key) -> Iterable[Key]:
    return [elt for elt in key if isinstance(elt, tuple)]

def literal_hash(e: Expression) -> Key | None:
    return e.accept(_hasher)

def extract_var_from_literal_hash(key: Key) -> Var | None:
    """If key refers to a Var node, return it.

    Return None otherwise.
    """
    if len(key) == 2 and key[0] == "Var" and isinstance(key[1], Var):
        return key[1]
    return None

class _Hasher(ExpressionVisitor[Optional[Key]]):
    def visit_int_expr(self, e: IntExpr) -> Key:
        return ("Literal", e.value)

    def visit_str_expr(self, e: StrExpr) -> Key:
        return ("Literal", e.value)

    def visit_bytes_expr(self, e: BytesExpr) -> Key:
        return ("Literal", e.value)

    def visit_float_expr(self, e: FloatExpr) -> Key:
        return ("Literal", e.value)

    def visit_complex_expr(self, e: ComplexExpr) -> Key:
        return ("Literal", e.value)

    def visit_star_expr(self, e: StarExpr) -> Key:
        return ("Star", literal_hash(e.expr))

    def visit_name_expr(self, e: NameExpr) -> Key:
        if isinstance(e.node, Var) and e.node.is_final and e.node.final_value is not None:
            return ("Literal", e.node.final_value)
        # N.B: We use the node itself as the key, and not the name,
        # because using the name causes issues when there is shadowing
        # (for example, in list comprehensions).
        return ("Var", e.node)

    def visit_member_expr(self, e: MemberExpr) -> Key:
        return ("Member", literal_hash(e.expr), e.name)

    def visit_op_expr(self, e: OpExpr) -> Key:
        return ("Binary", e.op, literal_hash(e.left), literal_hash(e.right))

    def visit_comparison_expr(self, e: ComparisonExpr) -> Key:
        rest: tuple[str | Key | None, ...] = tuple(e.operators)
        rest += tuple(literal_hash(o) for o in e.operands)
        return ("Comparison",) + rest

    def visit_unary_expr(self, e: UnaryExpr) -> Key:
        return ("Unary", e.op, literal_hash(e.expr))

    def seq_expr(self, e: ListExpr | TupleExpr | SetExpr, name: str) -> Key | None:
        if all(literal(x) == LITERAL_YES for x in e.items):
            rest: tuple[Key | None, ...] = tuple(literal_hash(x) for x in e.items)
            return (name,) + rest
        return None

    def visit_list_expr(self, e: ListExpr) -> Key | None:
        return self.seq_expr(e, "List")

    def visit_dict_expr(self, e: DictExpr) -> Key | None:
        if all(a and literal(a) == literal(b) == LITERAL_YES for a, b in e.items):
            rest: tuple[Key | None, ...] = tuple(
                (literal_hash(a) if a else None, literal_hash(b)) for a, b in e.items
            )
            return ("Dict",) + rest
        return None

    def visit_tuple_expr(self, e: TupleExpr) -> Key | None:
        return self.seq_expr(e, "Tuple")

    def visit_set_expr(self, e: SetExpr) -> Key | None:
        return self.seq_expr(e, "Set")

    def visit_index_expr(self, e: IndexExpr) -> Key | None:
        if literal(e.index) == LITERAL_YES:
            return ("Index", literal_hash(e.base), literal_hash(e.index))
        return None

    def visit_assignment_expr(self, e: AssignmentExpr) -> Key | None:
        return literal_hash(e.target)

    def visit_call_expr(self, e: CallExpr) -> None:
        return None

    def visit_slice_expr(self, e: SliceExpr) -> None:
        return None

    def visit_cast_expr(self, e: CastExpr) -> None:
        return None

    def visit_assert_type_expr(self, e: AssertTypeExpr) -> None:
        return None

    def visit_conditional_expr(self, e: ConditionalExpr) -> None:
        return None

    def visit_ellipsis(self, e: EllipsisExpr) -> None:
        return None

    def visit_yield_from_expr(self, e: YieldFromExpr) -> None:
        return None

    def visit_yield_expr(self, e: YieldExpr) -> None:
        return None

    def visit_reveal_expr(self, e: RevealExpr) -> None:
        return None

    def visit_super_expr(self, e: SuperExpr) -> None:
        return None

    def visit_type_application(self, e: TypeApplication) -> None:
        return None

    def visit_lambda_expr(self, e: LambdaExpr) -> None:
        return None

    def visit_list_comprehension(self, e: ListComprehension) -> None:
        return None

    def visit_set_comprehension(self, e: SetComprehension) -> None:
        return None

    def visit_dictionary_comprehension(self, e: DictionaryComprehension) -> None:
        return None

    def visit_generator_expr(self, e: GeneratorExpr) -> None:
        return None

    def visit_type_var_expr(self, e: TypeVarExpr) -> None:
        return None

    def visit_paramspec_expr(self, e: ParamSpecExpr) -> None:
        return None

    def visit_type_var_tuple_expr(self, e: TypeVarTupleExpr) -> None:
        return None

    def visit_type_alias_expr(self, e: TypeAliasExpr) -> None:
        return None

    def visit_namedtuple_expr(self, e: NamedTupleExpr) -> None:
        return None

    def visit_enum_call_expr(self, e: EnumCallExpr) -> None:
        return None

    def visit_typeddict_expr(self, e: TypedDictExpr) -> None:
        return None

    def visit_newtype_expr(self, e: NewTypeExpr) -> None:
        return None

    def visit__promote_expr(self, e: PromoteExpr) -> None:
        return None

    def visit_await_expr(self, e: AwaitExpr) -> None:
        return None

    def visit_temp_node(self, e: TempNode) -> None:
        return None

_hasher: Final = _Hasher()


"""
This is a module for various lookup functions:
functions that will find a semantic node by its name.
"""

from __future__ import annotations

from mypy.nodes import MypyFile, SymbolTableNode, TypeInfo

# TODO: gradually move existing lookup functions to this module.

def lookup_fully_qualified(
    name: str, modules: dict[str, MypyFile], *, raise_on_missing: bool = False
) -> SymbolTableNode | None:
    """Find a symbol using it fully qualified name.

    The algorithm has two steps: first we try splitting the name on '.' to find
    the module, then iteratively look for each next chunk after a '.' (e.g. for
    nested classes).

    This function should *not* be used to find a module. Those should be looked
    in the modules dictionary.
    """
    head = name
    rest = []
    # 1. Find a module tree in modules dictionary.
    while True:
        if "." not in head:
            if raise_on_missing:
                assert "." in head, f"Cannot find module for {name}"
            return None
        head, tail = head.rsplit(".", maxsplit=1)
        rest.append(tail)
        mod = modules.get(head)
        if mod is not None:
            break
    names = mod.names
    # 2. Find the symbol in the module tree.
    if not rest:
        # Looks like a module, don't use this to avoid confusions.
        if raise_on_missing:
            assert rest, f"Cannot find {name}, got a module symbol"
        return None
    while True:
        key = rest.pop()
        if key not in names:
            if raise_on_missing:
                assert key in names, f"Cannot find component {key!r} for {name!r}"
            return None
        stnode = names[key]
        if not rest:
            return stnode
        node = stnode.node
        # In fine-grained mode, could be a cross-reference to a deleted module
        # or a Var made up for a missing module.
        if not isinstance(node, TypeInfo):
            if raise_on_missing:
                assert node, f"Cannot find {name}"
            return None
        names = node.names