"""Elpy backend using the Jedi library.

This backend uses the Jedi library:

https://github.com/davidhalter/jedi

"""

import sys
import traceback

import jedi

from elpy import rpc


class JediBackend(object):
    """The Jedi backend class.

    Implements the RPC calls we can pass on to Jedi.

    Documentation: http://jedi.jedidjah.ch/en/latest/docs/plugin-api.html

    """
    name = "jedi"

    def __init__(self, project_root):
        self.project_root = project_root
        self.completions = {}
        sys.path.append(project_root)

    def rpc_get_completions(self, filename, source, offset):
        line, column = pos_to_linecol(source, offset)
        proposals = run_with_debug(jedi, 'completions',
                                   source=source, line=line, column=column,
                                   path=filename, encoding='utf-8')
        if proposals is None:
            return []
        self.completions = dict((proposal.name, proposal)
                                for proposal in proposals)
        return [{'name': proposal.name.rstrip("="),
                 'suffix': proposal.complete.rstrip("="),
                 'annotation': proposal.type,
                 'meta': proposal.description}
                for proposal in proposals]

    def rpc_get_completion_docstring(self, completion):
        proposal = self.completions.get(completion)
        if proposal is None:
            return None
        else:
            return proposal.docstring(fast=False)

    def rpc_get_completion_location(self, completion):
        proposal = self.completions.get(completion)
        if proposal is None:
            return None
        else:
            return (proposal.module_path, proposal.line)

    def rpc_get_docstring(self, filename, source, offset):
        line, column = pos_to_linecol(source, offset)
        try:
            locations = run_with_debug(jedi, 'goto_definitions',
                                       source=source, line=line, column=column,
                                       path=filename, encoding='utf-8',
                                       re_raise=jedi.NotFoundError)
        except jedi.NotFoundError:
            return None
        if locations:
            return ('Documentation for {0}:\n\n'.format(
                locations[-1].full_name) + locations[-1].docstring())
        else:
            return None

    def rpc_get_definition(self, filename, source, offset):
        line, column = pos_to_linecol(source, offset)
        try:
            locations = run_with_debug(jedi, 'goto_definitions',
                                       source=source, line=line, column=column,
                                       path=filename, encoding='utf-8',
                                       re_raise=jedi.NotFoundError)
        except jedi.NotFoundError:
            return None
        # goto_definitions() can return silly stuff like __builtin__
        # for int variables, so we fall back on goto() in those
        # cases. See issue #76.
        if (
                locations and
                locations[0].module_path is None
        ):
            locations = run_with_debug(jedi, 'goto_assignments',
                                       source=source, line=line,
                                       column=column,
                                       path=filename, encoding='utf-8')
        if not locations:
            return None
        else:
            loc = locations[-1]
            try:
                if loc.module_path:
                    if loc.module_path == filename:
                        offset = linecol_to_pos(source,
                                                loc.line,
                                                loc.column)
                    else:
                        with open(loc.module_path) as f:
                            offset = linecol_to_pos(f.read(),
                                                    loc.line,
                                                    loc.column)
                else:
                    return None
            except IOError:
                return None
            return (loc.module_path, offset)

    def rpc_get_calltip(self, filename, source, offset):
        line, column = pos_to_linecol(source, offset)
        calls = run_with_debug(jedi, 'call_signatures',
                               source=source, line=line, column=column,
                               path=filename, encoding='utf-8')
        if calls:
            call = calls[0]
        else:
            call = None
        if not call:
            return None
        try:
            call.index
        except AttributeError as e:
            if "get_definition" in str(e):
                # Bug #627 / jedi#573
                return None
            elif "get_subscope_by_name" in str(e):
                # Bug #677 / jedi#628
                return None
            else:
                raise
        return {"name": call.name,
                "index": call.index,
                "params": [param.description for param in call.params]}

    def rpc_get_usages(self, filename, source, offset):
        """Return the uses of the symbol at offset.

        Returns a list of occurrences of the symbol, as dicts with the
        fields name, filename, and offset.

        """
        line, column = pos_to_linecol(source, offset)
        try:
            uses = run_with_debug(jedi, 'usages',
                                  source=source, line=line, column=column,
                                  path=filename, encoding='utf-8',
                                  re_raise=(jedi.NotFoundError,))
        except jedi.NotFoundError:
            return []

        if uses is None:
            return None
        result = []
        for use in uses:
            if use.module_path == filename:
                offset = linecol_to_pos(source, use.line, use.column)
            elif use.module_path is not None:
                with open(use.module_path) as f:
                    text = f.read()
                offset = linecol_to_pos(text, use.line, use.column)

            result.append({"name": use.name,
                           "filename": use.module_path,
                           "offset": offset})

        return result


# From the Jedi documentation:
#
#   line is the current line you want to perform actions on (starting
#   with line #1 as the first line). column represents the current
#   column/indent of the cursor (starting with zero). source_path
#   should be the path of your file in the file system.

def pos_to_linecol(text, pos):
    """Return a tuple of line and column for offset pos in text.

    Lines are one-based, columns zero-based.

    This is how Jedi wants it. Don't ask me why.

    """
    line_start = text.rfind("\n", 0, pos) + 1
    line = text.count("\n", 0, line_start) + 1
    col = pos - line_start
    return line, col


def linecol_to_pos(text, line, col):
    """Return the offset of this line and column in text.

    Lines are one-based, columns zero-based.

    This is how Jedi wants it. Don't ask me why.

    """
    nth_newline_offset = 0
    for i in range(line - 1):
        new_offset = text.find("\n", nth_newline_offset)
        if new_offset < 0:
            raise ValueError("Text does not have {0} lines."
                             .format(line))
        nth_newline_offset = new_offset + 1
    offset = nth_newline_offset + col
    if offset > len(text):
        raise ValueError("Line {0} column {1} is not within the text"
                         .format(line, col))
    return offset


def run_with_debug(jedi, name, *args, **kwargs):
    re_raise = kwargs.pop('re_raise', ())
    # Remove form feed characters, they confuse Jedi (jedi#424)
    if 'source' in kwargs:
        kwargs['source'] = kwargs['source'].replace("\f", " ")
    try:
        script = jedi.Script(*args, **kwargs)
        return getattr(script, name)()
    except Exception as e:
        if isinstance(e, re_raise):
            raise
        # Bug jedi#417
        if isinstance(e, TypeError) and str(e) == 'no dicts allowed':
            return None
        # Bug jedi#427
        if isinstance(e, UnicodeDecodeError):
            return None
        # Bug jedi#429
        if isinstance(e, IndexError):
            return None
        # Bug jedi#431
        if isinstance(e, AttributeError) and str(e).endswith("'end_pos'"):
            return None
        # Bug in Python 2.6, see #275
        if isinstance(e, OSError) and e.errno == 13:
            return None
        # Bug jedi#466
        if (
                isinstance(e, SyntaxError) and
                "EOL while scanning string literal" in str(e)
        ):
            return None
        # Bug jedi#482
        if isinstance(e, UnicodeEncodeError):
            return None
        # Bug jedi#485
        if (
                isinstance(e, ValueError) and
                "invalid \\x escape" in str(e)
        ):
            return None
        # Bug jedi#485 in Python 3
        if (
                isinstance(e, SyntaxError) and
                "truncated \\xXX escape" in str(e)
        ):
            return None
        # Bug jedi#465
        if (
                isinstance(e, SyntaxError) and
                "encoding declaration in Unicode string" in str(e)
        ):
            return None
        # Bug #337 / jedi#471
        if (
                isinstance(e, ImportError) and
                "No module named" in str(e)
        ):
            return None
        # Bug #365 / jedi#486 - fixed in Jedi 0.8.2
        if (
                isinstance(e, UnboundLocalError) and
                "local variable 'path' referenced before assignment" in str(e)
        ):
            return None
        # Bug #366 / jedi#491
        if (
                isinstance(e, ValueError) and
                "__loader__ is None" in str(e)
        ):
            return None
        # Bug #353
        if (
                isinstance(e, OSError) and
                "No such file or directory" in str(e)
        ):
            return None
        # Bug #561, #564, #570, #588, #593, #599 / jedi#572, jedi#579, jedi#590
        if isinstance(e, KeyError):
            return None
        # Bug #519 / jedi#610
        if (
                isinstance(e, RuntimeError) and
                "maximum recursion depth exceeded" in str(e)
        ):
            return None
        # Bug #563 / jedi#589
        if (
                isinstance(e, AttributeError) and
                "MergedNamesDict" in str(e)
        ):
            return None
        # Bug #615 / jedi#592
        if (
                isinstance(e, AttributeError) and
                "ListComprehension" in str(e)
        ):
            return None
        # Bug #569 / jedi#593
        if (
                isinstance(e, AttributeError) and
                "names_dict" in str(e)
        ):
            return None

        from jedi import debug

        debug_info = []

        def _debug(level, str_out):
            if level == debug.NOTICE:
                prefix = "[N]"
            elif level == debug.WARNING:
                prefix = "[W]"
            else:
                prefix = "[?]"
            debug_info.append(u"{0} {1}".format(prefix, str_out))

        jedi.set_debug_function(_debug, speed=False)
        try:
            script = jedi.Script(*args, **kwargs)
            return getattr(script, name)()
        except Exception as e:
            source = kwargs.get('source')
            sc_args = []
            sc_args.extend(repr(arg) for arg in args)
            sc_args.extend("{0}={1}".format(k, "source" if k == "source"
                                            else repr(v))
                           for (k, v) in kwargs.items())

            data = {
                "traceback": traceback.format_exc(),
                "jedi_debug_info": {'script_args': ", ".join(sc_args),
                                    'source': source,
                                    'method': name,
                                    'debug_info': debug_info}
            }
            raise rpc.Fault(message=str(e),
                            code=500,
                            data=data)
        finally:
            jedi.set_debug_function(None)
