""" The classes returned by the api """

import re
import os

import settings
import evaluate
import imports
import parsing
import keywords


class BaseDefinition(object):
    _mapping = {'posixpath': 'os.path',
               'riscospath': 'os.path',
               'ntpath': 'os.path',
               'os2emxpath': 'os.path',
               'macpath': 'os.path',
               'genericpath': 'os.path',
               '_io': 'io',
               '__builtin__': '',
               'builtins': '',
               }

    _tuple_mapping = dict((tuple(k.split('.')), v) for (k, v) in {
        'argparse._ActionsContainer': 'argparse.ArgumentParser',
        '_sre.SRE_Match': 're.MatchObject',
        '_sre.SRE_Pattern': 're.RegexObject',
    }.items())

    def __init__(self, definition, start_pos):
        self.start_pos = start_pos
        self.definition = definition
        self.is_keyword = isinstance(definition, keywords.Keyword)

        # generate a path to the definition
        self.module_path = str(definition.get_parent_until().path)

    @property
    def type(self):
        # generate the type
        stripped = self.definition
        if isinstance(self.definition, evaluate.InstanceElement):
            stripped = self.definition.var
        return type(stripped).__name__

    @property
    def path(self):
        path = []
        if not isinstance(self.definition, keywords.Keyword):
            par = self.definition
            while par is not None:
                try:
                    path.insert(0, par.name)
                except AttributeError:
                    pass
                par = par.parent
        return path

    @property
    def module_name(self):
        path = self.module_path
        sep = os.path.sep
        p = re.sub(r'^.*?([\w\d]+)(%s__init__)?.py$' % sep, r'\1', path)
        return p

    def in_builtin_module(self):
        return not self.module_path.endswith('.py')

    @property
    def line_nr(self):
        return self.start_pos[0]

    @property
    def column(self):
        return self.start_pos[1]

    @property
    def doc(self):
        """ Return a document string for this completion object. """
        try:
            return self.definition.doc
        except AttributeError:
            return self.raw_doc

    @property
    def raw_doc(self):
        """ Returns the raw docstring `__doc__` for any object """
        try:
            return str(self.definition.docstr)
        except AttributeError:
            return ''

    @property
    def description(self):
        return str(self.definition)

    @property
    def full_name(self):
        """
        Returns the path to a certain class/function, see #61.
        """
        path = [str(p) for p in self.path]
        # TODO add further checks, the mapping should only occur on stdlib.
        try:
            path[0] = self._mapping[path[0]]
        except KeyError:
            pass
        for key, repl in self._tuple_mapping.items():
            if tuple(path[:len(key)]) == key:
                path = [repl] + path[len(key):]

        return '.'.join(path if path[0] else path[1:])

    def __repr__(self):
        return "<%s %s>" % (type(self).__name__, self.description)


class Completion(BaseDefinition):
    """ `Completion` objects are returned from `Script.complete`. Providing
    some useful functions for IDE's. """
    def __init__(self, name, needs_dot, like_name_length, base):
        super(Completion, self).__init__(name.parent, name.start_pos)

        self.name = name
        self.needs_dot = needs_dot
        self.like_name_length = like_name_length
        self.base = base

        self._followed_definitions = None

    @property
    def complete(self):
        """ Delievers the rest of the word, e.g. completing `isinstance`
        >>> isinstan

        would return the string 'ce'. It also adds additional stuff, depending
        on your `settings.py`
        """
        dot = '.' if self.needs_dot else ''
        append = ''
        if settings.add_bracket_after_function \
                    and self.type == 'Function':
            append = '('

        if settings.add_dot_after_module:
            if isinstance(self.base, parsing.Module):
                append += '.'
        if isinstance(self.base, parsing.Param):
            append += '='
        return dot + self.name.names[-1][self.like_name_length:] + append

    @property
    def word(self):
        """ In contrary to `complete` returns the whole word, e.g.
        >>> isinstan

        would return 'isinstance'.
        """
        return str(self.name.names[-1])

    @property
    def description(self):
        """ Provides a description of the completion object
        TODO return value is just __repr__ of some objects, improve! """
        parent = self.name.parent
        if parent is None:
            return ''
        t = self.type
        if t == 'Statement' or t == 'Import':
            desc = self.definition.get_code(False)
        else:
            desc = '.'.join(str(p) for p in self.path)

        line_nr = '' if self.in_builtin_module else '@%s' % self.line_nr
        return '%s: %s%s' % (t, desc, line_nr)

    def follow_definition(self):
        """ Returns you the original definitions. I strongly recommend not
        using it for your completions, because it might slow down Jedi. If you
        want to read only a few objects (<=20). I think it might be useful,
        especially to get the original docstrings.
        The basic problem of this function is that it follows all results. This
        means with 1000 completions (e.g. numpy), it's just PITA slow.
        """
        if self._followed_definitions is None:
            if self.definition.isinstance(parsing.Statement):
                defs = evaluate.follow_statement(self.definition)
            elif self.definition.isinstance(parsing.Import):
                defs = imports.strip_imports([self.definition])
            else:
                return [self]

            self._followed_definitions = \
                            [BaseDefinition(d, d.start_pos) for d in defs]
            evaluate.clear_caches()

        return self._followed_definitions

    def __repr__(self):
        return '<%s: %s>' % (type(self).__name__, self.name)


class Definition(BaseDefinition):
    """ These are the objects returned by either `Script.goto` or
    `Script.get_definition`. """
    def __init__(self, definition):
        super(Definition, self).__init__(definition, definition.start_pos)

    @property
    def description(self):
        """ A description of the Definition object, which is heavily used in
        testing. e.g. for `isinstance` it returns 'def isinstance' """
        d = self.definition
        if isinstance(d, evaluate.InstanceElement):
            d = d.var
        if isinstance(d, evaluate.parsing.Name):
            d = d.parent

        if isinstance(d, evaluate.Array):
            d = 'class ' + d.type
        elif isinstance(d, (parsing.Class, evaluate.Class, evaluate.Instance)):
            d = 'class ' + str(d.name)
        elif isinstance(d, (evaluate.Function, evaluate.parsing.Function)):
            d = 'def ' + str(d.name)
        elif isinstance(d, evaluate.parsing.Module):
            # only show module name
            d = 'module %s' % self.module_name
        elif self.is_keyword:
            d = 'keyword %s' % d.name
        else:
            d = d.get_code().replace('\n', '')
        return d

    @property
    def desc_with_module(self):
        """ In addition to the Definition, it also returns the module. Don't
        use it yet, its behaviour may change. If you really need it, talk to me
        TODO add full path. This function is should return a
        module.class.function path. """
        if self.module_path.endswith('.py') \
                    and not isinstance(self.definition, parsing.Module):
            position = '@%s' % (self.line_nr)
        else:
            # is a builtin or module
            position = ''
        return "%s:%s%s" % (self.module_name, self.description, position)


class RelatedName(BaseDefinition):
    def __init__(self, name_part, scope):
        super(RelatedName, self).__init__(scope, name_part.start_pos)
        self.name_part = name_part
        self.text = str(name_part)
        self.end_pos = name_part.end_pos

    @property
    def description(self):
        return "%s@%s,%s" % (self.text, self.start_pos[0], self.start_pos[1])

    def __eq__(self, other):
        return self.start_pos == other.start_pos \
            and self.module_path == other.module_path

    def __hash__(self):
        return hash((self.start_pos, self.module_path))


class CallDef(object):
    """ `CallDef` objects is the return value of `Script.get_in_function_call`.
    It knows what functions you are currently in. e.g. `isinstance(` would
    return the `isinstance` function. without `(` it would return nothing."""
    def __init__(self, executable, index, call):
        self.executable = executable
        self.index = index
        self.call = call

    @property
    def params(self):
        if self.executable.isinstance(evaluate.Function):
            if isinstance(self.executable, evaluate.InstanceElement):
                return self.executable.params[1:]
            return self.executable.params
        else:
            try:
                sub = self.executable.get_subscope_by_name('__init__')
                return sub.params[1:]  # ignore self
            except KeyError:
                return []

    @property
    def bracket_start(self):
        """ The indent of the bracket that is responsible for the last function
        call. """
        c = self.call
        while c.next is not None:
            c = c.next
        return c.name.end_pos

    @property
    def call_name(self):
        """ The name (e.g. 'isinstance') as a string. """
        return str(self.executable.name)

    @property
    def module(self):
        return self.executable.get_parent_until()

    def __repr__(self):
        return '<%s: %s index %s>' % (type(self).__name__, self.executable,
                                    self.index)
