"""'Runtime subtype' check for RTypes.

A type S is a runtime subtype of T if a value of type S can be used at runtime
when a value of type T is expected without requiring any runtime conversions.

For boxed types, runtime subtyping is the same as regular subtyping.
Unboxed subtypes, on the other hand, are not runtime subtypes of object
(since they require boxing to be used as an object), but short ints
are runtime subtypes of int.

Subtyping is used to determine whether an object can be in a
particular place and runtime subtyping is used to determine whether a
coercion is necessary first.
"""

from __future__ import annotations

from mypyc.ir.rtypes import (
    RArray,
    RInstance,
    RPrimitive,
    RStruct,
    RTuple,
    RType,
    RTypeVisitor,
    RUnion,
    RVoid,
    is_bit_rprimitive,
    is_bool_rprimitive,
    is_int_rprimitive,
    is_short_int_rprimitive,
)
from mypyc.subtype import is_subtype


def is_runtime_subtype(left: RType, right: RType) -> bool:
    return left.accept(RTSubtypeVisitor(right))


class RTSubtypeVisitor(RTypeVisitor[bool]):
    """Is left a runtime subtype of right?

    A few special cases such as right being 'object' are handled in
    is_runtime_subtype and don't need to be covered here.
    """

    def __init__(self, right: RType) -> None:
        self.right = right

    def visit_rinstance(self, left: RInstance) -> bool:
        return is_subtype(left, self.right)

    def visit_runion(self, left: RUnion) -> bool:
        return not self.right.is_unboxed and is_subtype(left, self.right)

    def visit_rprimitive(self, left: RPrimitive) -> bool:
        if is_short_int_rprimitive(left) and is_int_rprimitive(self.right):
            return True
        if is_bit_rprimitive(left) and is_bool_rprimitive(self.right):
            return True
        return left is self.right

    def visit_rtuple(self, left: RTuple) -> bool:
        if isinstance(self.right, RTuple):
            return len(self.right.types) == len(left.types) and all(
                is_runtime_subtype(t1, t2) for t1, t2 in zip(left.types, self.right.types)
            )
        return False

    def visit_rstruct(self, left: RStruct) -> bool:
        return isinstance(self.right, RStruct) and self.right.name == left.name

    def visit_rarray(self, left: RArray) -> bool:
        return left == self.right

    def visit_rvoid(self, left: RVoid) -> bool:
        return isinstance(self.right, RVoid)
