import types

from pure_eval.utils import of_type, CannotEval

_sentinel = object()


def _static_getmro(klass):
    return type.__dict__['__mro__'].__get__(klass)


def _check_instance(obj, attr):
    instance_dict = {}
    try:
        instance_dict = object.__getattribute__(obj, "__dict__")
    except AttributeError:
        pass
    return dict.get(instance_dict, attr, _sentinel)


def _check_class(klass, attr):
    for entry in _static_getmro(klass):
        if _shadowed_dict(type(entry)) is _sentinel:
            try:
                return entry.__dict__[attr]
            except KeyError:
                pass
        else:
            break
    return _sentinel


def _is_type(obj):
    try:
        _static_getmro(obj)
    except TypeError:
        return False
    return True


def _shadowed_dict(klass):
    dict_attr = type.__dict__["__dict__"]
    for entry in _static_getmro(klass):
        try:
            class_dict = dict_attr.__get__(entry)["__dict__"]
        except KeyError:
            pass
        else:
            if not (type(class_dict) is types.GetSetDescriptorType and
                    class_dict.__name__ == "__dict__" and
                    class_dict.__objclass__ is entry):
                return class_dict
    return _sentinel


def getattr_static(obj, attr):
    """Retrieve attributes without triggering dynamic lookup via the
       descriptor protocol,  __getattr__ or __getattribute__.

       Note: this function may not be able to retrieve all attributes
       that getattr can fetch (like dynamically created attributes)
       and may find attributes that getattr can't (like descriptors
       that raise AttributeError). It can also return descriptor objects
       instead of instance members in some cases. See the
       documentation for details.
    """
    instance_result = _sentinel
    if not _is_type(obj):
        klass = type(obj)
        dict_attr = _shadowed_dict(klass)
        if (dict_attr is _sentinel or
                type(dict_attr) is types.MemberDescriptorType):
            instance_result = _check_instance(obj, attr)
        else:
            raise CannotEval
    else:
        klass = obj

    klass_result = _check_class(klass, attr)

    if instance_result is not _sentinel and klass_result is not _sentinel:
        if _check_class(type(klass_result), "__get__") is not _sentinel and (
            _check_class(type(klass_result), "__set__") is not _sentinel
            or _check_class(type(klass_result), "__delete__") is not _sentinel
        ):
            return _resolve_descriptor(klass_result, obj, klass)

    if instance_result is not _sentinel:
        return instance_result
    if klass_result is not _sentinel:
        get = _check_class(type(klass_result), '__get__')
        if get is _sentinel:
            return klass_result
        else:
            if obj is klass:
                instance = None
            else:
                instance = obj
            return _resolve_descriptor(klass_result, instance, klass)

    if obj is klass:
        # for types we check the metaclass too
        for entry in _static_getmro(type(klass)):
            if _shadowed_dict(type(entry)) is _sentinel:
                try:
                    result = entry.__dict__[attr]
                    get = _check_class(type(result), '__get__')
                    if get is not _sentinel:
                        raise CannotEval
                    return result
                except KeyError:
                    pass
    raise CannotEval


class _foo:
    __slots__ = ['foo']
    method = lambda: 0


slot_descriptor = _foo.foo
wrapper_descriptor = str.__dict__['__add__']
method_descriptor = str.__dict__['startswith']
user_method_descriptor = _foo.__dict__['method']

safe_descriptors_raw = [
    slot_descriptor,
    wrapper_descriptor,
    method_descriptor,
    user_method_descriptor,
]

safe_descriptor_types = list(map(type, safe_descriptors_raw))


def _resolve_descriptor(d, instance, owner):
    try:
        return type(of_type(d, *safe_descriptor_types)).__get__(d, instance, owner)
    except AttributeError as e:
        raise CannotEval from e
