from parso.python import token
from parso.python import tree
from parso.tree import search_ancestor, Leaf

from jedi import debug
from jedi import settings
from jedi.api import classes
from jedi.api import helpers
from jedi.evaluate import imports
from jedi.api import keywords
from jedi.evaluate.helpers import evaluate_call_of_leaf
from jedi.evaluate.filters import get_global_filters
from jedi.parser_utils import get_statement_of_position


def get_call_signature_param_names(call_signatures):
    # add named params
    for call_sig in call_signatures:
        for p in call_sig.params:
            # Allow protected access, because it's a public API.
            tree_name = p._name.tree_name
            # Compiled modules typically don't allow keyword arguments.
            if tree_name is not None:
                # Allow access on _definition here, because it's a
                # public API and we don't want to make the internal
                # Name object public.
                tree_param = tree.search_ancestor(tree_name, 'param')
                if tree_param.star_count == 0:  # no *args/**kwargs
                    yield p._name


def filter_names(evaluator, completion_names, stack, like_name):
    comp_dct = {}
    for name in completion_names:
        if settings.case_insensitive_completion \
                and name.string_name.lower().startswith(like_name.lower()) \
                or name.string_name.startswith(like_name):

            new = classes.Completion(
                evaluator,
                name,
                stack,
                len(like_name)
            )
            k = (new.name, new.complete)  # key
            if k in comp_dct and settings.no_completion_duplicates:
                comp_dct[k]._same_name_completions.append(new)
            else:
                comp_dct[k] = new
                yield new


def get_user_scope(module_context, position):
    """
    Returns the scope in which the user resides. This includes flows.
    """
    user_stmt = get_statement_of_position(module_context.tree_node, position)
    if user_stmt is None:
        def scan(scope):
            for s in scope.children:
                if s.start_pos <= position <= s.end_pos:
                    if isinstance(s, (tree.Scope, tree.Flow)):
                        return scan(s) or s
                    elif s.type in ('suite', 'decorated'):
                        return scan(s)
            return None

        scanned_node = scan(module_context.tree_node)
        if scanned_node:
            return module_context.create_context(scanned_node, node_is_context=True)
        return module_context
    else:
        return module_context.create_context(user_stmt)


def get_flow_scope_node(module_node, position):
    node = module_node.get_leaf_for_position(position, include_prefixes=True)
    while not isinstance(node, (tree.Scope, tree.Flow)):
        node = node.parent

    return node


class Completion:
    def __init__(self, evaluator, module, code_lines, position, call_signatures_method):
        self._evaluator = evaluator
        self._module_context = module
        self._module_node = module.tree_node
        self._code_lines = code_lines

        # The first step of completions is to get the name
        self._like_name = helpers.get_on_completion_name(self._module_node, code_lines, position)
        # The actual cursor position is not what we need to calculate
        # everything. We want the start of the name we're on.
        self._position = position[0], position[1] - len(self._like_name)
        self._call_signatures_method = call_signatures_method

    def completions(self):
        completion_names = self._get_context_completions()

        completions = filter_names(self._evaluator, completion_names,
                                   self.stack, self._like_name)

        return sorted(completions, key=lambda x: (x.name.startswith('__'),
                                                  x.name.startswith('_'),
                                                  x.name.lower()))

    def _get_context_completions(self):
        """
        Analyzes the context that a completion is made in and decides what to
        return.

        Technically this works by generating a parser stack and analysing the
        current stack for possible grammar nodes.

        Possible enhancements:
        - global/nonlocal search global
        - yield from / raise from <- could be only exceptions/generators
        - In args: */**: no completion
        - In params (also lambda): no completion before =
        """

        grammar = self._evaluator.grammar

        try:
            self.stack = helpers.get_stack_at_position(
                grammar, self._code_lines, self._module_node, self._position
            )
        except helpers.OnErrorLeaf as e:
            self.stack = None
            if e.error_leaf.value == '.':
                # After ErrorLeaf's that are dots, we will not do any
                # completions since this probably just confuses the user.
                return []
            # If we don't have a context, just use global completion.

            return self._global_completions()

        allowed_keywords, allowed_tokens = \
            helpers.get_possible_completion_types(grammar._pgen_grammar, self.stack)

        if 'if' in allowed_keywords:
            leaf = self._module_node.get_leaf_for_position(self._position, include_prefixes=True)
            previous_leaf = leaf.get_previous_leaf()

            indent = self._position[1]
            if not (leaf.start_pos <= self._position <= leaf.end_pos):
                indent = leaf.start_pos[1]

            if previous_leaf is not None:
                stmt = previous_leaf
                while True:
                    stmt = search_ancestor(
                        stmt, 'if_stmt', 'for_stmt', 'while_stmt', 'try_stmt',
                        'error_node',
                    )
                    if stmt is None:
                        break

                    type_ = stmt.type
                    if type_ == 'error_node':
                        first = stmt.children[0]
                        if isinstance(first, Leaf):
                            type_ = first.value + '_stmt'
                    # Compare indents
                    if stmt.start_pos[1] == indent:
                        if type_ == 'if_stmt':
                            allowed_keywords += ['elif', 'else']
                        elif type_ == 'try_stmt':
                            allowed_keywords += ['except', 'finally', 'else']
                        elif type_ == 'for_stmt':
                            allowed_keywords.append('else')

        completion_names = list(self._get_keyword_completion_names(allowed_keywords))

        if token.NAME in allowed_tokens or token.INDENT in allowed_tokens:
            # This means that we actually have to do type inference.

            symbol_names = list(self.stack.get_node_names(grammar._pgen_grammar))

            nodes = list(self.stack.get_nodes())

            if nodes and nodes[-1] in ('as', 'def', 'class'):
                # No completions for ``with x as foo`` and ``import x as foo``.
                # Also true for defining names as a class or function.
                return list(self._get_class_context_completions(is_function=True))
            elif "import_stmt" in symbol_names:
                level, names = self._parse_dotted_names(nodes, "import_from" in symbol_names)

                only_modules = not ("import_from" in symbol_names and 'import' in nodes)
                completion_names += self._get_importer_names(
                    names,
                    level,
                    only_modules=only_modules,
                )
            elif symbol_names[-1] in ('trailer', 'dotted_name') and nodes[-1] == '.':
                dot = self._module_node.get_leaf_for_position(self._position)
                completion_names += self._trailer_completions(dot.get_previous_leaf())
            else:
                completion_names += self._global_completions()
                completion_names += self._get_class_context_completions(is_function=False)

            if 'trailer' in symbol_names:
                call_signatures = self._call_signatures_method()
                completion_names += get_call_signature_param_names(call_signatures)

        return completion_names

    def _get_keyword_completion_names(self, keywords_):
        for k in keywords_:
            yield keywords.keyword(self._evaluator, k).name

    def _global_completions(self):
        context = get_user_scope(self._module_context, self._position)
        debug.dbg('global completion scope: %s', context)
        flow_scope_node = get_flow_scope_node(self._module_node, self._position)
        filters = get_global_filters(
            self._evaluator,
            context,
            self._position,
            origin_scope=flow_scope_node
        )
        completion_names = []
        for filter in filters:
            completion_names += filter.values()
        return completion_names

    def _trailer_completions(self, previous_leaf):
        user_context = get_user_scope(self._module_context, self._position)
        evaluation_context = self._evaluator.create_context(
            self._module_context, previous_leaf
        )
        contexts = evaluate_call_of_leaf(evaluation_context, previous_leaf)
        completion_names = []
        debug.dbg('trailer completion contexts: %s', contexts)
        for context in contexts:
            for filter in context.get_filters(
                    search_global=False, origin_scope=user_context.tree_node):
                completion_names += filter.values()
        return completion_names

    def _parse_dotted_names(self, nodes, is_import_from):
        level = 0
        names = []
        for node in nodes[1:]:
            if node in ('.', '...'):
                if not names:
                    level += len(node.value)
            elif node.type == 'dotted_name':
                names += node.children[::2]
            elif node.type == 'name':
                names.append(node)
            elif node == ',':
                if not is_import_from:
                    names = []
            else:
                # Here if the keyword `import` comes along it stops checking
                # for names.
                break
        return level, names

    def _get_importer_names(self, names, level=0, only_modules=True):
        names = [n.value for n in names]
        i = imports.Importer(self._evaluator, names, self._module_context, level)
        return i.completion_names(self._evaluator, only_modules=only_modules)

    def _get_class_context_completions(self, is_function=True):
        """
        Autocomplete inherited methods when overriding in child class.
        """
        leaf = self._module_node.get_leaf_for_position(self._position, include_prefixes=True)
        cls = tree.search_ancestor(leaf, 'classdef')
        if isinstance(cls, (tree.Class, tree.Function)):
            # Complete the methods that are defined in the super classes.
            random_context = self._module_context.create_context(
                cls,
                node_is_context=True
            )
        else:
            return

        if cls.start_pos[1] >= leaf.start_pos[1]:
            return

        filters = random_context.get_filters(search_global=False, is_instance=True)
        # The first dict is the dictionary of class itself.
        next(filters)
        for filter in filters:
            for name in filter.values():
                if (name.api_type == 'function') == is_function:
                    yield name
