import keyword
import sys
import warnings

import rope.base.codeanalyze
import rope.base.evaluate
from rope.base import pyobjects, pyobjectsdef, pynames, builtins, exceptions, worder
from rope.base.codeanalyze import SourceLinesAdapter
from rope.contrib import fixsyntax
from rope.refactor import functionutils


def code_assist(project, source_code, offset, resource=None,
                templates=None, maxfixes=1, later_locals=True):
    """Return python code completions as a list of `CodeAssistProposal`\s

    `resource` is a `rope.base.resources.Resource` object.  If
    provided, relative imports are handled.

    `maxfixes` is the maximum number of errors to fix if the code has
    errors in it.

    If `later_locals` is `False` names defined in this scope and after
    this line is ignored.

    """
    if templates is not None:
        warnings.warn('Codeassist no longer supports templates',
                      DeprecationWarning, stacklevel=2)
    assist = _PythonCodeAssist(
        project, source_code, offset, resource=resource,
        maxfixes=maxfixes, later_locals=later_locals)
    return assist()


def starting_offset(source_code, offset):
    """Return the offset in which the completion should be inserted

    Usually code assist proposals should be inserted like::

        completion = proposal.name
        result = (source_code[:starting_offset] +
                  completion + source_code[offset:])

    Where starting_offset is the offset returned by this function.

    """
    word_finder = worder.Worder(source_code, True)
    expression, starting, starting_offset = \
        word_finder.get_splitted_primary_before(offset)
    return starting_offset


def get_doc(project, source_code, offset, resource=None, maxfixes=1):
    """Get the pydoc"""
    fixer = fixsyntax.FixSyntax(project.pycore, source_code,
                                resource, maxfixes)
    pymodule = fixer.get_pymodule()
    pyname = fixer.pyname_at(offset)
    if pyname is None:
        return None
    pyobject = pyname.get_object()
    return PyDocExtractor().get_doc(pyobject)


def get_calltip(project, source_code, offset, resource=None,
                maxfixes=1, ignore_unknown=False, remove_self=False):
    """Get the calltip of a function

    The format of the returned string is
    ``module_name.holding_scope_names.function_name(arguments)``.  For
    classes `__init__()` and for normal objects `__call__()` function
    is used.

    Note that the offset is on the function itself *not* after the its
    open parenthesis.  (Actually it used to be the other way but it
    was easily confused when string literals were involved.  So I
    decided it is better for it not to try to be too clever when it
    cannot be clever enough).  You can use a simple search like::

        offset = source_code.rindex('(', 0, offset) - 1

    to handle simple situations.

    If `ignore_unknown` is `True`, `None` is returned for functions
    without source-code like builtins and extensions.

    If `remove_self` is `True`, the first parameter whose name is self
    will be removed for methods.
    """
    fixer = fixsyntax.FixSyntax(project.pycore, source_code,
                                resource, maxfixes)
    pymodule = fixer.get_pymodule()
    pyname = fixer.pyname_at(offset)
    if pyname is None:
        return None
    pyobject = pyname.get_object()
    return PyDocExtractor().get_calltip(pyobject, ignore_unknown, remove_self)


def get_definition_location(project, source_code, offset,
                            resource=None, maxfixes=1):
    """Return the definition location of the python name at `offset`

    Return a (`rope.base.resources.Resource`, lineno) tuple.  If no
    `resource` is given and the definition is inside the same module,
    the first element of the returned tuple would be `None`.  If the
    location cannot be determined ``(None, None)`` is returned.

    """
    fixer = fixsyntax.FixSyntax(project.pycore, source_code,
                                resource, maxfixes)
    pymodule = fixer.get_pymodule()
    pyname = fixer.pyname_at(offset)
    if pyname is not None:
        module, lineno = pyname.get_definition_location()
        if module is not None:
            return module.get_module().get_resource(), lineno
    return (None, None)


def find_occurrences(*args, **kwds):
    import rope.contrib.findit
    warnings.warn('Use `rope.contrib.findit.find_occurrences()` instead',
                  DeprecationWarning, stacklevel=2)
    return rope.contrib.findit.find_occurrences(*args, **kwds)


class CompletionProposal(object):
    """A completion proposal

    The `scope` instance variable shows where proposed name came from
    and can be 'global', 'local', 'builtin', 'attribute', 'keyword',
    'imported', 'parameter_keyword'.

    The `type` instance variable shows the approximate type of the
    proposed object and can be 'instance', 'class', 'function', 'module',
    and `None`.

    All possible relations between proposal's `scope` and `type` are shown
    in the table below (different scopes in rows and types in columns):

                      | instance | class | function | module | None
                local |    +     |   +   |    +     |   +    |
               global |    +     |   +   |    +     |   +    |
              builtin |    +     |   +   |    +     |        |
            attribute |    +     |   +   |    +     |   +    |
             imported |    +     |   +   |    +     |   +    |
              keyword |          |       |          |        |  +
    parameter_keyword |          |       |          |        |  +

    """

    def __init__(self, name, scope, pyname=None):
        self.name = name
        self.pyname = pyname
        self.scope = self._get_scope(scope)

    def __str__(self):
        return '%s (%s, %s)' % (self.name, self.scope, self.type)

    def __repr__(self):
        return str(self)

    @property
    def parameters(self):
        """The names of the parameters the function takes.

        Returns None if this completion is not a function.
        """
        pyname = self.pyname
        if isinstance(pyname, pynames.ImportedName):
            pyname = pyname._get_imported_pyname()
        if isinstance(pyname, pynames.DefinedName):
            pyobject = pyname.get_object()
            if isinstance(pyobject, pyobjects.AbstractFunction):
                return pyobject.get_param_names()

    @property
    def type(self):
        pyname = self.pyname
        if isinstance(pyname, builtins.BuiltinName):
            pyobject = pyname.get_object()
            if isinstance(pyobject, builtins.BuiltinFunction):
                return 'function'
            elif isinstance(pyobject, builtins.BuiltinClass):
                clsobj = pyobject.builtin
                return 'class'
            elif isinstance(pyobject, builtins.BuiltinObject) or \
                 isinstance(pyobject, builtins.BuiltinName):
                return 'instance'
        elif isinstance(pyname, pynames.ImportedModule):
            return 'module'
        elif isinstance(pyname, pynames.ImportedName) or \
           isinstance(pyname, pynames.DefinedName):
            pyobject = pyname.get_object()
            if isinstance(pyobject, pyobjects.AbstractFunction):
                return 'function'
            if isinstance(pyobject, pyobjects.AbstractClass):
                return 'class'
        return 'instance'

    def _get_scope(self, scope):
        if isinstance(self.pyname, builtins.BuiltinName):
            return 'builtin'
        if isinstance(self.pyname, pynames.ImportedModule) or \
           isinstance(self.pyname, pynames.ImportedName):
            return 'imported'
        return scope

    def get_doc(self):
        """Get the proposed object's docstring.