# -*- coding: utf-8 -*-
#
# Copyright © Spyder Project Contributors
# Licensed under the terms of the MIT License
# (see spyder/__init__.py for details)

"""
Patching rope:

[1] For compatibility with Spyder's standalone version, built with py2exe or
    cx_Freeze

[2] For better performance, see this thread:
    https://groups.google.com/forum/#!topic/rope-dev/V95XMfICU3o

[3] To avoid considering folders without __init__.py as Python packages, thus
    avoiding side effects as non-working introspection features on a Python
    module or package when a folder in current directory has the same name.
    See this thread:
    https://groups.google.com/forum/#!topic/rope-dev/kkxLWmJo5hg

[4] To avoid rope adding a 2 spaces indent to every docstring it gets, because
    it breaks the work of Sphinx on the Help plugin. Also, to better
    control how to get calltips and docstrings of forced builtin objects.

[5] To make matplotlib return its docstrings in proper rst, instead of a mix
    of rst and plain text.
"""

def apply():
    """Monkey patching rope

    See [1], [2], [3], [4] and [5] in module docstring."""
    from spyder.utils.programs import is_module_installed
    if is_module_installed('rope', '<0.9.4'):
        import rope
        raise ImportError("rope %s can't be patched" % rope.VERSION)

    # [1] Patching project.Project for compatibility with py2exe/cx_Freeze
    #     distributions
    from spyder.config.base import is_py2exe_or_cx_Freeze
    if is_py2exe_or_cx_Freeze():
        from rope.base import project
        class PatchedProject(project.Project):
            def _default_config(self):
                # py2exe/cx_Freeze distribution
                from spyder.config.base import get_module_source_path
                fname = get_module_source_path('spyder',
                                               'default_config.py')
                return open(fname, 'rb').read()
        project.Project = PatchedProject
    
    # Patching pycore.PyCore...
    from rope.base import pycore
    class PatchedPyCore(pycore.PyCore):
        # [2] ...so that forced builtin modules (i.e. modules that were 
        # declared as 'extension_modules' in rope preferences) will be indeed
        # recognized as builtins by rope, as expected
        # 
        # This patch is included in rope 0.9.4+ but applying it anyway is ok
        def get_module(self, name, folder=None):
            """Returns a `PyObject` if the module was found."""
            # check if this is a builtin module
            pymod = self._builtin_module(name)
            if pymod is not None:
                return pymod
            module = self.find_module(name, folder)
            if module is None:
                raise pycore.ModuleNotFoundError(
                                            'Module %s not found' % name)
            return self.resource_to_pyobject(module)
        # [3] ...to avoid considering folders without __init__.py as Python
        # packages
        def _find_module_in_folder(self, folder, modname):
            module = folder
            packages = modname.split('.')
            for pkg in packages[:-1]:
                if  module.is_folder() and module.has_child(pkg):
                    module = module.get_child(pkg)
                else:
                    return None
            if module.is_folder():
                if module.has_child(packages[-1]) and \
                   module.get_child(packages[-1]).is_folder() and \
                   module.get_child(packages[-1]).has_child('__init__.py'):
                    return module.get_child(packages[-1])
                elif module.has_child(packages[-1] + '.py') and \
                     not module.get_child(packages[-1] + '.py').is_folder():
                    return module.get_child(packages[-1] + '.py')
    pycore.PyCore = PatchedPyCore

    # [2] Patching BuiltinName for the go to definition feature to simply work 
    # with forced builtins
    from rope.base import builtins, libutils, pyobjects
    import inspect
    import os.path as osp
    class PatchedBuiltinName(builtins.BuiltinName):
        def _pycore(self):
            p = self.pyobject
            while p.parent is not None:
                p = p.parent
            if isinstance(p, builtins.BuiltinModule) and p.pycore is not None:
                return p.pycore
        def get_definition_location(self):
            if not inspect.isbuiltin(self.pyobject):
                _lines, lineno = inspect.getsourcelines(self.pyobject.builtin)
                path = inspect.getfile(self.pyobject.builtin)
                if path.endswith('pyc') and osp.isfile(path[:-1]):
                    path = path[:-1]
                pycore = self._pycore()
                if pycore and pycore.project:
                    resource = libutils.path_to_resource(pycore.project, path)
                    module = pyobjects.PyModule(pycore, None, resource)
                    return (module, lineno)
            return (None, None)
    builtins.BuiltinName = PatchedBuiltinName
    
    # [4] Patching several PyDocExtractor methods:
    # 1. get_doc:
    # To force rope to return the docstring of any object which has one, even
    # if it's not an instance of AbstractFunction, AbstractClass, or
    # AbstractModule.
    # Also, to use utils.dochelpers.getdoc to get docs from forced builtins.
    #
    # 2. _get_class_docstring and _get_single_function_docstring:
    # To not let rope add a 2 spaces indentation to every docstring, which was
    # breaking our rich text mode. The only value that we are modifying is the
    # 'indents' keyword of those methods, from 2 to 0.
    #
    # 3. get_calltip
    # To easily get calltips of forced builtins
    from rope.contrib import codeassist
    from spyder_kernels.utils.dochelpers import getdoc
    from rope.base import exceptions
    class PatchedPyDocExtractor(codeassist.PyDocExtractor):
        def get_builtin_doc(self, pyobject):
            buitin = pyobject.builtin
            return getdoc(buitin)
            
        def get_doc(self, pyobject):
            if hasattr(pyobject, 'builtin'):
                doc = self.get_builtin_doc(pyobject)
                return doc
            elif isinstance(pyobject, builtins.BuiltinModule):
                docstring = pyobject.get_doc()
                if docstring is not None:
                    docstring = self._trim_docstring(docstring)
                else:
                    docstring = ''
                # TODO: Add a module_name key, so that the name could appear
                # on the OI text filed but not be used by sphinx to render
                # the page
                doc = {'name': '',
                       'argspec': '',
                       'note': '',
                       'docstring': docstring
                       }
                return doc
            elif isinstance(pyobject, pyobjects.AbstractFunction):
                return self._get_function_docstring(pyobject)
            elif isinstance(pyobject, pyobjects.AbstractClass):
                return self._get_class_docstring(pyobject)
            elif isinstance(pyobject, pyobjects.AbstractModule):
                return self._trim_docstring(pyobject.get_doc())
            elif pyobject.get_doc() is not None:  # Spyder patch
                return self._trim_docstring(pyobject.get_doc())
            return None

        def get_calltip(self, pyobject, ignore_unknown=False, remove_self=False):
            if hasattr(pyobject, 'builtin'):
                doc = self.get_builtin_doc(pyobject)
                return doc['name'] + doc['argspec']
            try:
                if isinstance(pyobject, pyobjects.AbstractClass):
                    pyobject = pyobject['__init__'].get_object()
                if not isinstance(pyobject, pyobjects.AbstractFunction):
                    pyobject = pyobject['__call__'].get_object()
            except exceptions.AttributeNotFoundError:
                return None
            if ignore_unknown and not isinstance(pyobject, pyobjects.PyFunction):
                return
            if isinstance(pyobject, pyobjects.AbstractFunction):
                result = self._get_function_signature(pyobject, add_module=True)
                if remove_self and self._is_method(pyobject):
                    return result.replace('(self)', '()').replace('(self, ', '(')
                return result
        
        def _get_class_docstring(self, pyclass):
            contents = self._trim_docstring(pyclass.get_doc(), indents=0)
            supers = [super.get_name() for super in pyclass.get_superclasses()]
            doc = 'class %s(%s):\n\n' % (pyclass.get_name(), ', '.join(supers)) + contents

            if '__init__' in pyclass:
                init = pyclass['__init__'].get_object()
                if isinstance(init, pyobjects.AbstractFunction):
                    doc += '\n\n' + self._get_single_function_docstring(init)
            return doc
            
        def _get_single_function_docstring(self, pyfunction):
            docs = pyfunction.get_doc()
            docs = self._trim_docstring(docs, indents=0)
            return docs
    codeassist.PyDocExtractor = PatchedPyDocExtractor


    # [5] Get the right matplotlib docstrings for Help
    try:
        import matplotlib as mpl
        mpl.rcParams['docstring.hardcopy'] = True
    except:
        pass
