"""
Implementation of the 'abstract instance and subclasses' protocol:
objects can return pseudo-classes as their '__class__' attribute, and
pseudo-classes can have a '__bases__' attribute with a tuple of other
pseudo-classes.  The standard built-in functions isinstance() and
issubclass() follow and trust these attributes is they are present, in
addition to checking for instances and subtypes in the normal way.
"""

from rpython.rlib import jit

from pypy.interpreter.baseobjspace import ObjSpace as BaseObjSpace
from pypy.interpreter.error import OperationError
from pypy.module.__builtin__.interp_classobj import W_ClassObject
from pypy.module.__builtin__.interp_classobj import W_InstanceObject


def _get_bases(space, w_cls):
    """Returns 'cls.__bases__'.  Returns None if there is
    no __bases__ or if cls.__bases__ is not a tuple.
    """
    try:
        w_bases = space.getattr(w_cls, space.wrap('__bases__'))
    except OperationError as e:
        if not e.match(space, space.w_AttributeError):
            raise       # propagate other errors
        return None
    if space.isinstance_w(w_bases, space.w_tuple):
        return w_bases
    else:
        return None

def abstract_isclass_w(space, w_obj):
    return _get_bases(space, w_obj) is not None

def check_class(space, w_obj, msg):
    if not abstract_isclass_w(space, w_obj):
        raise OperationError(space.w_TypeError, space.wrap(msg))


def abstract_getclass(space, w_obj):
    try:
        return space.getattr(w_obj, space.wrap('__class__'))
    except OperationError as e:
        if not e.match(space, space.w_AttributeError):
            raise       # propagate other errors
        return space.type(w_obj)

@jit.unroll_safe
def abstract_isinstance_w(space, w_obj, w_klass_or_tuple, allow_override=False):
    """Implementation for the full 'isinstance(obj, klass_or_tuple)'."""
    # -- case (anything, tuple)
    # XXX it might be risky that the JIT sees this
    if space.isinstance_w(w_klass_or_tuple, space.w_tuple):
        for w_klass in space.fixedview(w_klass_or_tuple):
            if abstract_isinstance_w(space, w_obj, w_klass, allow_override):
                return True
        return False

    # -- case (anything, type)
    try:
        if allow_override:
            w_result = space.isinstance_allow_override(w_obj, w_klass_or_tuple)
        else:
            w_result = space.isinstance(w_obj, w_klass_or_tuple)
    except OperationError as e:   # if w_klass_or_tuple was not a type, ignore it
        if not e.match(space, space.w_TypeError):
            raise       # propagate other errors
    else:
        if space.is_true(w_result):
            return True
        # From now on we know that w_klass_or_tuple is indeed a type.
        # Try also to compare it with obj.__class__, if this is not
        # the same as type(obj).
        try:
            w_pretendtype = space.getattr(w_obj, space.wrap('__class__'))
            if space.is_w(w_pretendtype, space.type(w_obj)):
                return False     # common case: obj.__class__ is type(obj)
            if not allow_override:
                return space.issubtype_w(w_pretendtype, w_klass_or_tuple)
            w_result = space.issubtype_allow_override(w_pretendtype,
                                                      w_klass_or_tuple)
        except OperationError as e:
            if e.async(space):
                raise
            return False      # ignore most exceptions
        else:
            return space.is_true(w_result)

    # -- case (old-style instance, old-style class)
    if isinstance(w_klass_or_tuple, W_ClassObject):
        if isinstance(w_obj, W_InstanceObject):
            return w_obj.w_class.is_subclass_of(w_klass_or_tuple)
    return _abstract_isinstance_w_helper(space, w_obj, w_klass_or_tuple)


def _abstract_isinstance_w_helper(space, w_obj, w_klass_or_tuple):
    # -- case (anything, abstract-class)
    check_class(space, w_klass_or_tuple,
                "isinstance() arg 2 must be a class, type,"
                " or tuple of classes and types")
    try:
        w_abstractclass = space.getattr(w_obj, space.wrap('__class__'))
    except OperationError as e:
        if e.async(space):      # ignore most exceptions
            raise
        return False
    else:
        return _issubclass_recurse(space, w_abstractclass, w_klass_or_tuple)


@jit.unroll_safe
def _issubclass_recurse(space, w_derived, w_top):
    """Internal helper for abstract cases.  Here, w_top cannot be a tuple."""
    if space.is_w(w_derived, w_top):
        return True
    w_bases = _get_bases(space, w_derived)
    if w_bases is not None:
        for w_base in space.fixedview(w_bases):
            if _issubclass_recurse(space, w_base, w_top):
                return True
    return False


@jit.unroll_safe
def abstract_issubclass_w(space, w_derived, w_klass_or_tuple,
                          allow_override=False):
    """Implementation for the full 'issubclass(derived, klass_or_tuple)'."""

    # -- case (class-like-object, tuple-of-classes)
    if space.isinstance_w(w_klass_or_tuple, space.w_tuple):
        for w_klass in space.fixedview(w_klass_or_tuple):
            if abstract_issubclass_w(space, w_derived, w_klass, allow_override):
                return True
        return False

    # -- case (type, type)
    try:
        if not allow_override:
            return space.issubtype_w(w_derived, w_klass_or_tuple)
        w_result = space.issubtype_allow_override(w_derived, w_klass_or_tuple)
    except OperationError as e:   # if one of the args was not a type, ignore it
        if not e.match(space, space.w_TypeError):
            raise       # propagate other errors
    else:
        return space.is_true(w_result)

    # -- case (old-style class, old-style class)
    if isinstance(w_derived, W_ClassObject):
        if isinstance(w_klass_or_tuple, W_ClassObject):
            return w_derived.is_subclass_of(w_klass_or_tuple)
    else:
        check_class(space, w_derived, "issubclass() arg 1 must be a class")
    # from here on, we are sure that w_derived is a class-like object

    # -- case (class-like-object, abstract-class)
    check_class(space, w_klass_or_tuple,
                "issubclass() arg 2 must be a class, type,"
                " or tuple of classes and types")
    return _issubclass_recurse(space, w_derived, w_klass_or_tuple)

# ------------------------------------------------------------
# Exception helpers

def exception_is_valid_obj_as_class_w(space, w_obj):
    if isinstance(w_obj, W_ClassObject):
        return True
    return BaseObjSpace.exception_is_valid_obj_as_class_w(space, w_obj)

def exception_is_valid_class_w(space, w_cls):
    if isinstance(w_cls, W_ClassObject):
        return True
    return BaseObjSpace.exception_is_valid_class_w(space, w_cls)

def exception_getclass(space, w_obj):
    if isinstance(w_obj, W_InstanceObject):
        return w_obj.w_class
    return BaseObjSpace.exception_getclass(space, w_obj)

def exception_issubclass_w(space, w_cls1, w_cls2):
    if isinstance(w_cls1, W_ClassObject):
        if isinstance(w_cls2, W_ClassObject):
            return w_cls1.is_subclass_of(w_cls2)
        return False
    if isinstance(w_cls2, W_ClassObject):
        return False
    return BaseObjSpace.exception_issubclass_w(space, w_cls1, w_cls2)

# ____________________________________________________________
# App-level interface

def issubclass(space, w_cls, w_klass_or_tuple):
    """Check whether a class 'cls' is a subclass (i.e., a derived class) of
another class.  When using a tuple as the second argument, check whether
'cls' is a subclass of any of the classes listed in the tuple."""
    result = abstract_issubclass_w(space, w_cls, w_klass_or_tuple, True)
    return space.wrap(result)

def isinstance(space, w_obj, w_klass_or_tuple):
    """Check whether an object is an instance of a class (or of a subclass
thereof).  When using a tuple as the second argument, check whether 'obj'
is an instance of any of the classes listed in the tuple."""
    result = abstract_isinstance_w(space, w_obj, w_klass_or_tuple, True)
    return space.wrap(result)

# avoid namespace pollution
app_issubclass = issubclass; del issubclass
app_isinstance = isinstance; del isinstance
