from typing import cast, List

from mypy.join import is_similar_callables, combine_similar_callables
from mypy.types import (
    Type, AnyType, TypeVisitor, UnboundType, Void, ErrorType, NoneTyp, TypeVarType,
    Instance, CallableType, TupleType, ErasedType, TypeList, UnionType, PartialType, DeletedType
)
from mypy.sametypes import is_same_type
from mypy.subtypes import is_subtype
from mypy.nodes import TypeInfo

# TODO Describe this module.


def meet_types(s: Type, t: Type) -> Type:
    """Return the greatest lower bound of two types."""
    if isinstance(s, ErasedType):
        return s
    if isinstance(s, AnyType):
        return t
    if isinstance(s, UnionType) and not isinstance(t, UnionType):
        s, t = t, s
    return t.accept(TypeMeetVisitor(s))


def meet_simple(s: Type, t: Type, default_right: bool = True) -> Type:
    if s == t:
        return s
    if isinstance(s, UnionType):
        return UnionType.make_simplified_union([meet_types(x, t) for x in s.items])
    elif not is_overlapping_types(s, t, use_promotions=True):
        return NoneTyp()
    else:
        if default_right:
            return t
        else:
            return s


def is_overlapping_types(t: Type, s: Type, use_promotions: bool = False) -> bool:
    """Can a value of type t be a value of type s, or vice versa?

    Note that this effectively checks against erased types, since X[Any] is always
    compatible with X[T].

    If use_promitions is True, also consider type promotions (int and
    float would only be overlapping if it's True).
    """
    if isinstance(t, Instance):
        if isinstance(s, Instance):
            # Only consider two classes non-disjoint if one is included in the mro
            # of another.
            if use_promotions:
                # Consider cases like int vs float to be overlapping where
                # there is only a type promition relationship but not proper
                # subclassing.
                if t.type._promote and is_overlapping_types(t.type._promote, s):
                    return True
                if s.type._promote and is_overlapping_types(s.type._promote, t):
                    return True
            return t.type in s.type.mro or s.type in t.type.mro
    if isinstance(t, UnionType):
        return any(is_overlapping_types(item, s)
                   for item in t.items)
    if isinstance(s, UnionType):
        return any(is_overlapping_types(t, item)
                   for item in s.items)
    # We conservatively assume that non-instance, non-union types can overlap any other
    # types.
    return True


def nearest_builtin_ancestor(type: TypeInfo) -> TypeInfo:
    for base in type.mro:
        if base.defn.is_builtinclass:
            return base
    else:
        return None
        assert False, 'No built-in ancestor found for {}'.format(type.name())


class TypeMeetVisitor(TypeVisitor[Type]):
    def __init__(self, s: Type) -> None:
        self.s = s

    def visit_unbound_type(self, t: UnboundType) -> Type:
        if isinstance(self.s, Void) or isinstance(self.s, ErrorType):
            return ErrorType()
        elif isinstance(self.s, NoneTyp):
            return self.s
        else:
            return AnyType()

    def visit_error_type(self, t: ErrorType) -> Type:
        return t

    def visit_type_list(self, t: TypeList) -> Type:
        assert False, 'Not supported'

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

    def visit_union_type(self, t: UnionType) -> Type:
        if isinstance(self.s, UnionType):
            meets = []  # type: List[Type]
            for x in t.items:
                for y in self.s.items:
                    meets.append(meet_types(x, y))
        else:
            meets = [meet_types(x, self.s)
                     for x in t.items]
        return UnionType.make_simplified_union(meets)

    def visit_void(self, t: Void) -> Type:
        if isinstance(self.s, Void):
            return t
        else:
            return ErrorType()

    def visit_none_type(self, t: NoneTyp) -> Type:
        if not isinstance(self.s, Void) and not isinstance(self.s, ErrorType):
            return t
        else:
            return ErrorType()

    def visit_deleted_type(self, t: DeletedType) -> Type:
        if not isinstance(self.s, Void) and not isinstance(self.s, ErrorType):
            if isinstance(self.s, NoneTyp):
                return self.s
            else:
                return t
        else:
            return ErrorType()

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

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

    def visit_instance(self, t: Instance) -> Type:
        if isinstance(self.s, Instance):
            si = cast(Instance, self.s)
            if t.type == si.type:
                if is_subtype(t, self.s) or is_subtype(self.s, t):
                    # Combine type arguments. We could have used join below
                    # equivalently.
                    args = []  # type: List[Type]
                    for i in range(len(t.args)):
                        args.append(self.meet(t.args[i], si.args[i]))
                    return Instance(t.type, args)
                else:
                    return NoneTyp()
            else:
                if is_subtype(t, self.s):
                    return t
                elif is_subtype(self.s, t):
                    # See also above comment.
                    return self.s
                else:
                    return NoneTyp()
        else:
            return self.default(self.s)

    def visit_callable_type(self, t: CallableType) -> Type:
        if isinstance(self.s, CallableType) and is_similar_callables(
                t, cast(CallableType, self.s)):
            return combine_similar_callables(t, cast(CallableType, self.s))
        else:
            return self.default(self.s)

    def visit_tuple_type(self, t: TupleType) -> Type:
        if isinstance(self.s, TupleType) and (
                cast(TupleType, self.s).length() == t.length()):
            items = []  # type: List[Type]
            for i in range(t.length()):
                items.append(self.meet(t.items[i],
                                       (cast(TupleType, self.s)).items[i]))
            # TODO: What if the fallbacks are different?
            return TupleType(items, t.fallback)
        else:
            return self.default(self.s)

    def visit_partial_type(self, t: PartialType) -> Type:
        # We can't determine the meet of partial types. We should never get here.
        assert False, 'Internal error'

    def meet(self, s, t):
        return meet_types(s, t)

    def default(self, typ):
        if isinstance(typ, UnboundType):
            return AnyType()
        elif isinstance(typ, Void) or isinstance(typ, ErrorType):
            return ErrorType()
        else:
            return NoneTyp()
