from __future__ import print_function
import inspect
import types
import sys
import operator as op
from collections import namedtuple
import warnings
import re

from jedi._compatibility import unicode, is_py3, builtins, \
    py_version, force_unicode
from jedi.inference.compiled.getattr_static import getattr_static

ALLOWED_GETITEM_TYPES = (str, list, tuple, unicode, bytes, bytearray, dict)

MethodDescriptorType = type(str.replace)
# These are not considered classes and access is granted even though they have
# a __class__ attribute.
NOT_CLASS_TYPES = (
    types.BuiltinFunctionType,
    types.CodeType,
    types.FrameType,
    types.FunctionType,
    types.GeneratorType,
    types.GetSetDescriptorType,
    types.LambdaType,
    types.MemberDescriptorType,
    types.MethodType,
    types.ModuleType,
    types.TracebackType,
    MethodDescriptorType
)

if is_py3:
    NOT_CLASS_TYPES += (
        types.MappingProxyType,
        types.SimpleNamespace,
        types.DynamicClassAttribute,
    )


# Those types don't exist in typing.
MethodDescriptorType = type(str.replace)
WrapperDescriptorType = type(set.__iter__)
# `object.__subclasshook__` is an already executed descriptor.
object_class_dict = type.__dict__["__dict__"].__get__(object)
ClassMethodDescriptorType = type(object_class_dict['__subclasshook__'])

_sentinel = object()

# Maps Python syntax to the operator module.
COMPARISON_OPERATORS = {
    '==': op.eq,
    '!=': op.ne,
    'is': op.is_,
    'is not': op.is_not,
    '<': op.lt,
    '<=': op.le,
    '>': op.gt,
    '>=': op.ge,
}

_OPERATORS = {
    '+': op.add,
    '-': op.sub,
}
_OPERATORS.update(COMPARISON_OPERATORS)

ALLOWED_DESCRIPTOR_ACCESS = (
    types.FunctionType,
    types.GetSetDescriptorType,
    types.MemberDescriptorType,
    MethodDescriptorType,
    WrapperDescriptorType,
    ClassMethodDescriptorType,
    staticmethod,
    classmethod,
)


def safe_getattr(obj, name, default=_sentinel):
    try:
        attr, is_get_descriptor = getattr_static(obj, name)
    except AttributeError:
        if default is _sentinel:
            raise
        return default
    else:
        if isinstance(attr, ALLOWED_DESCRIPTOR_ACCESS):
            # In case of descriptors that have get methods we cannot return
            # it's value, because that would mean code execution.
            # Since it's an isinstance call, code execution is still possible,
            # but this is not really a security feature, but much more of a
            # safety feature. Code execution is basically always possible when
            # a module is imported. This is here so people don't shoot
            # themselves in the foot.
            return getattr(obj, name)
    return attr


SignatureParam = namedtuple(
    'SignatureParam',
    'name has_default default default_string has_annotation annotation annotation_string kind_name'
)


def shorten_repr(func):
    def wrapper(self):
        r = func(self)
        if len(r) > 50:
            r = r[:50] + '..'
        return r
    return wrapper


def create_access(inference_state, obj):
    return inference_state.compiled_subprocess.get_or_create_access_handle(obj)


def load_module(inference_state, dotted_name, sys_path):
    temp, sys.path = sys.path, sys_path
    try:
        __import__(dotted_name)
    except ImportError:
        # If a module is "corrupt" or not really a Python module or whatever.
        print('Module %s not importable in path %s.' % (dotted_name, sys_path), file=sys.stderr)
        return None
    except Exception:
        # Since __import__ pretty much makes code execution possible, just
        # catch any error here and print it.
        import traceback
        print("Cannot import:\n%s" % traceback.format_exc(), file=sys.stderr)
        return None
    finally:
        sys.path = temp

    # Just access the cache after import, because of #59 as well as the very
    # complicated import structure of Python.
    module = sys.modules[dotted_name]
    return create_access_path(inference_state, module)


class AccessPath(object):
    def __init__(self, accesses):
        self.accesses = accesses

    # Writing both of these methods here looks a bit ridiculous. However with
    # the differences of Python 2/3 it's actually necessary, because we will
    # otherwise have a accesses attribute that is bytes instead of unicode.
    def __getstate__(self):
        return self.accesses

    def __setstate__(self, value):
        self.accesses = value


def create_access_path(inference_state, obj):
    access = create_access(inference_state, obj)
    return AccessPath(access.get_access_path_tuples())


def _force_unicode_decorator(func):
    return lambda *args, **kwargs: force_unicode(func(*args, **kwargs))


def get_api_type(obj):
    if inspect.isclass(obj):
        return u'class'
    elif inspect.ismodule(obj):
        return u'module'
    elif inspect.isbuiltin(obj) or inspect.ismethod(obj) \
            or inspect.ismethoddescriptor(obj) or inspect.isfunction(obj):
        return u'function'
    # Everything else...
    return u'instance'


class DirectObjectAccess(object):
    def __init__(self, inference_state, obj):
        self._inference_state = inference_state
        self._obj = obj

    def __repr__(self):
        return '%s(%s)' % (self.__class__.__name__, self.get_repr())

    def _create_access(self, obj):
        return create_access(self._inference_state, obj)

    def _create_access_path(self, obj):
        return create_access_path(self._inference_state, obj)

    def py__bool__(self):
        return bool(self._obj)

    def py__file__(self):
        try:
            return self._obj.__file__
        except AttributeError:
            return None

    def py__doc__(self):
        return force_unicode(inspect.getdoc(self._obj)) or u''

    def py__name__(self):
        if not _is_class_instance(self._obj) or \
                inspect.ismethoddescriptor(self._obj):  # slots
            cls = self._obj
        else:
            try:
                cls = self._obj.__class__
            except AttributeError:
                # happens with numpy.core.umath._UFUNC_API (you get it
                # automatically by doing `import numpy`.
                return None

        try:
            return force_unicode(cls.__name__)
        except AttributeError:
            return None

    def py__mro__accesses(self):
        return tuple(self._create_access_path(cls) for cls in self._obj.__mro__[1:])

    def py__getitem__all_values(self):
        if isinstance(self._obj, dict):
            return [self._create_access_path(v) for v in self._obj.values()]
        return self.py__iter__list()

    def py__simple_getitem__(self, index):
        if type(self._obj) not in ALLOWED_GETITEM_TYPES:
            # Get rid of side effects, we won't call custom `__getitem__`s.
            return None

        return self._create_access_path(self._obj[index])

    def py__iter__list(self):
        if not hasattr(self._obj, '__getitem__'):
            return None

        if type(self._obj) not in ALLOWED_GETITEM_TYPES:
            # Get rid of side effects, we won't call custom `__getitem__`s.
            return []

        lst = []
        for i, part in enumerate(self._obj):
            if i > 20:
                # Should not go crazy with large iterators
                break
            lst.append(self._create_access_path(part))
        return lst

    def py__class__(self):
        return self._create_access_path(self._obj.__class__)

    def py__bases__(self):
        return [self._create_access_path(base) for base in self._obj.__bases__]

    def py__path__(self):
        paths = getattr(self._obj, '__path__', None)
        # Avoid some weird hacks that would just fail, because they cannot be
        # used by pickle.
        if not isinstance(paths, list) \
                or not all(isinstance(p, (bytes, unicode)) for p in paths):
            return None
        return paths

    @_force_unicode_decorator
    @shorten_repr
    def get_repr(self):
        builtins = 'builtins', '__builtin__'

        if inspect.ismodule(self._obj):
            return repr(self._obj)
        # Try to avoid execution of the property.
        if safe_getattr(self._obj, '__module__', default='') in builtins:
            return repr(self._obj)

        type_ = type(self._obj)
        if type_ == type:
            return type.__repr__(self._obj)

        if safe_getattr(type_, '__module__', default='') in builtins:
            # Allow direct execution of repr for builtins.
            return repr(self._obj)
        return object.__repr__(self._obj)

    def is_class(self):
        return inspect.isclass(self._obj)

    def is_function(self):
        return inspect.isfunction(self._obj) or inspect.ismethod(self._obj)

    def is_module(self):
        return inspect.ismodule(self._obj)

    def is_instance(self):
        return _is_class_instance(self._obj)

    def ismethoddescriptor(self):
        return inspect.ismethoddescriptor(self._obj)

    def get_qualified_names(self):
        def try_to_get_name(obj):
            return getattr(obj, '__qualname__', getattr(obj, '__name__', None))

        if self.is_module():
            return ()
        name = try_to_get_name(self._obj)
        if name is None:
            name = try_to_get_name(type(self._obj))
            if name is None:
                return ()
        return tuple(force_unicode(n) for n in name.split('.'))

    def dir(self):
        return list(map(force_unicode, dir(self._obj)))

    def has_iter(self):
        try:
            iter(self._obj)
            return True
        except TypeError:
            return False

    def is_allowed_getattr(self, name, unsafe=False):
        # TODO this API is ugly.
        if unsafe:
            # Unsafe is mostly used to check for __getattr__/__getattribute__.
            # getattr_static works for properties, but the underscore methods
            # are just ignored (because it's safer and avoids more code
            # execution). See also GH #1378.

            # Avoid warnings, see comment in the next function.
            with warnings.catch_warnings(record=True):
                warnings.simplefilter("always")
                try:
                    return hasattr(self._obj, name), False
                except Exception:
                    # Obviously has an attribute (propably a property) that
                    # gets executed, so just avoid all exceptions here.
                    return False, False
        try:
            attr, is_get_descriptor = getattr_static(self._obj, name)
        except AttributeError:
            return False, False
        else:
            if is_get_descriptor and type(attr) not in ALLOWED_DESCRIPTOR_ACCESS:
                # In case of descriptors that have get methods we cannot return
                # it's value, because that would mean code execution.
                return True, True
        return True, False

    def getattr_paths(self, name, default=_sentinel):
        try:
            # Make sure no warnings are printed here, this is autocompletion,
            # warnings should not be shown. See also GH #1383.
            with warnings.catch_warnings(record=True):
                warnings.simplefilter("always")
                return_obj = getattr(self._obj, name)
        except Exception as e:
            if default is _sentinel:
                if isinstance(e, AttributeError):
                    # Happens e.g. in properties of
                    # PyQt4.QtGui.QStyleOptionComboBox.currentText
                    # -> just set it to None
                    raise
                # Just in case anything happens, return an AttributeError. It
                # should not crash.
                raise AttributeError
            return_obj = default
        access = self._create_access(return_obj)
        if inspect.ismodule(return_obj):
            return [access]

        try:
            module = return_obj.__module__
        except AttributeError:
            pass
        else:
            if module is not None:
                try:
                    __import__(module)
                    # For some modules like _sqlite3, the __module__ for classes is
                    # different, in this case it's sqlite3. So we have to try to
                    # load that "original" module, because it's not loaded yet. If
                    # we don't do that, we don't really have a "parent" module and
                    # we would fall back to builtins.
                except ImportError:
                    pass

        module = inspect.getmodule(return_obj)
        if module is None:
            module = inspect.getmodule(type(return_obj))
            if module is None:
                module = builtins
        return [self._create_access(module), access]

    def get_safe_value(self):
        if type(self._obj) in (bool, bytes, float, int, str, unicode, slice) or self._obj is None:
            return self._obj
        raise ValueError("Object is type %s and not simple" % type(self._obj))

    def get_api_type(self):
        return get_api_type(self._obj)

    def get_array_type(self):
        if isinstance(self._obj, dict):
            return 'dict'
        return None

    def get_key_paths(self):
        def iter_partial_keys():
            # We could use list(keys()), but that might take a lot more memory.
            for (i, k) in enumerate(self._obj.keys()):
                # Limit key listing at some point. This is artificial, but this
                # way we don't get stalled because of slow completions
                if i > 50:
                    break
                yield k

        return [self._create_access_path(k) for k in iter_partial_keys()]

    def get_access_path_tuples(self):
        accesses = [create_access(self._inference_state, o) for o in self._get_objects_path()]
        return [(access.py__name__(), access) for access in accesses]

    def _get_objects_path(self):
        def get():
            obj = self._obj
            yield obj
            try:
                obj = obj.__objclass__
            except AttributeError:
                pass
            else:
                yield obj

            try:
                # Returns a dotted string path.
                imp_plz = obj.__module__
            except AttributeError:
                # Unfortunately in some cases like `int` there's no __module__
                if not inspect.ismodule(obj):
                    yield builtins
            else:
                if imp_plz is None:
                    # Happens for example in `(_ for _ in []).send.__module__`.
                    yield builtins
                else:
                    try:
                        yield sys.modules[imp_plz]
                    except KeyError:
                        # __module__ can be something arbitrary that doesn't exist.
                        yield builtins

        return list(reversed(list(get())))

    def execute_operation(self, other_access_handle, operator):
        other_access = other_access_handle.access
        op = _OPERATORS[operator]
        return self._create_access_path(op(self._obj, other_access._obj))

    def get_annotation_name_and_args(self):
        """
        Returns Tuple[Optional[str], Tuple[AccessPath, ...]]
        """
        if sys.version_info < (3, 5):
            return None, ()

        name = None
        args = ()
        if safe_getattr(self._obj, '__module__', default='') == 'typing':
            m = re.match(r'typing.(\w+)\[', repr(self._obj))
            if m is not None:
                name = m.group(1)

                import typing
                if sys.version_info >= (3, 8):
                    args = typing.get_args(self._obj)
                else:
                    args = safe_getattr(self._obj, '__args__', default=None)
        return name, tuple(self._create_access_path(arg) for arg in args)

    def needs_type_completions(self):
        return inspect.isclass(self._obj) and self._obj != type

    def _annotation_to_str(self, annotation):
        if isinstance(annotation, type):
            return str(annotation.__name__)
        return str(annotation)

    def get_signature_params(self):
        return [
            SignatureParam(
                name=p.name,
                has_default=p.default is not p.empty,
                default=self._create_access_path(p.default),
                default_string=repr(p.default),
                has_annotation=p.annotation is not p.empty,
                annotation=self._create_access_path(p.annotation),
                annotation_string=self._annotation_to_str(p.annotation),
                kind_name=str(p.kind)
            ) for p in self._get_signature().parameters.values()
        ]

    def _get_signature(self):
        obj = self._obj
        if py_version < 33:
            raise ValueError("inspect.signature was introduced in 3.3")
        try:
            return inspect.signature(obj)
        except (RuntimeError, TypeError):
            # Reading the code of the function in Python 3.6 implies there are
            # at least these errors that might occur if something is wrong with
            # the signature. In that case we just want a simple escape for now.
            raise ValueError

    def get_return_annotation(self):
        try:
            o = self._obj.__annotations__.get('return')
        except AttributeError:
            return None

        if o is None:
            return None

        try:
            # Python 2 doesn't have typing.
            import typing
        except ImportError:
            pass
        else:
            try:
                o = typing.get_type_hints(self._obj).get('return')
            except Exception:
                pass

        return self._create_access_path(o)

    def negate(self):
        return self._create_access_path(-self._obj)

    def get_dir_infos(self):
        """
        Used to return a couple of infos that are needed when accessing the sub
        objects of an objects
        """
        tuples = dict(
            (force_unicode(name), self.is_allowed_getattr(name))
            for name in self.dir()
        )
        return self.needs_type_completions(), tuples


def _is_class_instance(obj):
    """Like inspect.* methods."""
    try:
        cls = obj.__class__
    except AttributeError:
        return False
    else:
        # The isinstance check for cls is just there so issubclass doesn't
        # raise an exception.
        return cls != type and isinstance(cls, type) and not issubclass(cls, NOT_CLASS_TYPES)
