"""Loads functions that are mixed in to the standard library. E.g. builtins are
written in C (binaries), but my autocompletion only understands Python code. By
mixing in Python code, the autocompletion should work much better for builtins.
"""

import contextlib
import inspect
import os

from jedi._compatibility import builtins, is_py3, unicode
from jedi.evaluate.helpers import FakeName
from jedi.parser import Parser, load_grammar
from jedi.parser import tree as pt

modules = {}


def _load_faked_module(module):
    module_name = module.__name__
    if module_name == '__builtin__' and not is_py3:
        module_name = 'builtins'

    try:
        return modules[module_name]
    except KeyError:
        path = os.path.dirname(os.path.abspath(__file__))
        try:
            with open(os.path.join(path, 'fake', module_name) + '.pym') as f:
                source = f.read()
        except OSError:
            modules[module_name] = None
            return None
        grammar = load_grammar('grammar3.4')
        module = Parser(grammar, unicode(source), module_name).module
        modules[module_name] = module

        if module_name == 'builtins' and not is_py3:
            # There are two implementations of `open` for either python 2/3.
            # -> Rename the python2 version (`look at fake/builtins.pym`).
            open_func = search_scope(module, 'open')
            open_func.children[1] = FakeName('open_python3')
            open_func = search_scope(module, 'open_python2')
            open_func.children[1] = FakeName('open')
        return module


def search_scope(scope, obj_name):
    for s in scope.subscopes:
        if str(s.name) == obj_name:
            return s
    return None


def get_module(obj):
    if inspect.ismodule(obj):
        return obj
    with contextlib.suppress(AttributeError):
        obj = obj.__objclass__

    try:
        imp_plz = obj.__module__
    except AttributeError:
        # Unfortunately in some cases like `int` there's no __module__
        return builtins
    else:
        return __import__(imp_plz)


def _faked(module, obj, name):
    # Crazy underscore actions to try to escape all the internal madness.
    if module is None:
        module = get_module(obj)

    faked_mod = _load_faked_module(module)
    if faked_mod is None:
        return None

    # Having the module as a `parser.representation.module`, we need to scan
    # for methods.
    if name is None:
        if inspect.isbuiltin(obj):
            return search_scope(faked_mod, obj.__name__)
        if not inspect.isclass(obj):
            # object is a method or descriptor
            cls = search_scope(faked_mod, obj.__objclass__.__name__)
            if cls is None:
                return None
            return search_scope(cls, obj.__name__)
        return None
    if obj == module:
        return search_scope(faked_mod, name)
    cls = search_scope(faked_mod, obj.__name__)
    if cls is None:
        return None
    return search_scope(cls, name)


def get_faked(module, obj, name=None):
    obj = obj.__class__ if is_class_instance(obj) else obj
    result = _faked(module, obj, name)
    if result is None or isinstance(result, pt.Class):
        # We're not interested in classes. What we want is functions.
        return None
    # Set the docstr which was previously not set (faked modules don't
    # contain it).
    doc = f'"""{obj.__doc__}"""'  # TODO need escapes.
    suite = result.children[-1]
    string = pt.String(pt.zero_position_modifier, doc, (0, 0), '')
    new_line = pt.Whitespace('\n', (0, 0), '')
    docstr_node = pt.Node('simple_stmt', [string, new_line])
    suite.children.insert(2, docstr_node)
    return result


def is_class_instance(obj) -> bool:
    """Like inspect.* methods."""
    return not (inspect.isclass(obj) or inspect.ismodule(obj)
                or inspect.isbuiltin(obj) or inspect.ismethod(obj)
                or inspect.ismethoddescriptor(obj) or inspect.iscode(obj)
                or inspect.isgenerator(obj))
