"""
Searching for names with given scope and name. This is very central in Jedi and
Python. The name resolution is quite complicated with descripter,
``__getattribute__``, ``__getattr__``, ``global``, etc.

If you want to understand name resolution, please read the first few chapters
in http://blog.ionelmc.ro/2015/02/09/understanding-python-metaclasses/.

Flow checks
+++++++++++

Flow checks are not really mature. There's only a check for ``isinstance``.  It
would check whether a flow has the form of ``if isinstance(a, type_or_tuple)``.
Unfortunately every other thing is being ignored (e.g. a == '' would be easy to
check for -> a is a string). There's big potential in these checks.
"""

from parso.python import tree
from parso.tree import search_ancestor
from rp.libs.jedi import debug
from rp.libs.jedi import settings
from rp.libs.jedi.evaluate import compiled
from rp.libs.jedi.evaluate import analysis
from rp.libs.jedi.evaluate import flow_analysis
from rp.libs.jedi.evaluate.arguments import TreeArguments
from rp.libs.jedi.evaluate import helpers
from rp.libs.jedi.evaluate.context import iterable
from rp.libs.jedi.evaluate.filters import get_global_filters
from rp.libs.jedi.evaluate.names import TreeNameDefinition
from rp.libs.jedi.evaluate.base_context import ContextSet, NO_CONTEXTS
from rp.libs.jedi.parser_utils import is_scope, get_parent_scope
from rp.libs.jedi.evaluate.gradual.conversion import convert_contexts


class NameFinder(object):
    def __init__(self, evaluator, context, name_context, name_or_str,
                 position=None, analysis_errors=True):
        self._evaluator = evaluator
        # Make sure that it's not just a syntax tree node.
        self._context = context
        self._name_context = name_context
        self._name = name_or_str
        if isinstance(name_or_str, tree.Name):
            self._string_name = name_or_str.value
        else:
            self._string_name = name_or_str
        self._position = position
        self._found_predefined_types = None
        self._analysis_errors = analysis_errors

    def find(self, filters, attribute_lookup):
        """
        :params bool attribute_lookup: Tell to logic if we're accessing the
            attribute or the contents of e.g. a function.
        """
        names = self.filter_name(filters)
        if self._found_predefined_types is not None and names:
            check = flow_analysis.reachability_check(
                context=self._context,
                context_scope=self._context.tree_node,
                node=self._name,
            )
            if check is flow_analysis.UNREACHABLE:
                return NO_CONTEXTS
            return self._found_predefined_types

        types = self._names_to_types(names, attribute_lookup)

        if not names and self._analysis_errors and not types \
                and not (isinstance(self._name, tree.Name) and
                         isinstance(self._name.parent.parent, tree.Param)):
            if isinstance(self._name, tree.Name):
                if attribute_lookup:
                    analysis.add_attribute_error(
                        self._name_context, self._context, self._name)
                else:
                    message = ("NameError: name '%s' is not defined."
                               % self._string_name)
                    analysis.add(self._name_context, 'name-error', self._name, message)

        return types

    def _get_origin_scope(self):
        if isinstance(self._name, tree.Name):
            scope = self._name
            while scope.parent is not None:
                # TODO why if classes?
                if not isinstance(scope, tree.Scope):
                    break
                scope = scope.parent
            return scope
        else:
            return None

    def get_filters(self, search_global=False):
        origin_scope = self._get_origin_scope()
        if search_global:
            position = self._position

            # For functions and classes the defaults don't belong to the
            # function and get evaluated in the context before the function. So
            # make sure to exclude the function/class name.
            if origin_scope is not None:
                ancestor = search_ancestor(origin_scope, 'funcdef', 'classdef', 'lambdef')
                lambdef = None
                if ancestor == 'lambdef':
                    # For lambdas it's even more complicated since parts will
                    # be evaluated later.
                    lambdef = ancestor
                    ancestor = search_ancestor(origin_scope, 'funcdef', 'classdef')
                if ancestor is not None:
                    colon = ancestor.children[-2]
                    if position is not None and position < colon.start_pos:
                        if lambdef is None or position < lambdef.children[-2].start_pos:
                            position = ancestor.start_pos

            return get_global_filters(self._evaluator, self._context, position, origin_scope)
        else:
            return self._get_context_filters(origin_scope)

    def _get_context_filters(self, origin_scope):
        for f in self._context.get_filters(False, self._position, origin_scope=origin_scope):
            yield f
        # This covers the case where a stub files are incomplete.
        if self._context.is_stub():
            for c in convert_contexts(ContextSet({self._context})):
                for f in c.get_filters():
                    yield f

    def filter_name(self, filters):
        """
        Searches names that are defined in a scope (the different
        ``filters``), until a name fits.
        """
        names = []
        # This paragraph is currently needed for proper branch evaluation
        # (static analysis).
        if self._context.predefined_names and isinstance(self._name, tree.Name):
            node = self._name
            while node is not None and not is_scope(node):
                node = node.parent
                if node.type in ("if_stmt", "for_stmt", "comp_for", 'sync_comp_for'):
                    try:
                        name_dict = self._context.predefined_names[node]
                        types = name_dict[self._string_name]
                    except KeyError:
                        continue
                    else:
                        self._found_predefined_types = types
                        break

        for filter in filters:
            names = filter.get(self._string_name)
            if names:
                if len(names) == 1:
                    n, = names
                    if isinstance(n, TreeNameDefinition):
                        # Something somewhere went terribly wrong. This
                        # typically happens when using goto on an import in an
                        # __init__ file. I think we need a better solution, but
                        # it's kind of hard, because for Jedi it's not clear
                        # that that name has not been defined, yet.
                        if n.tree_name == self._name:
                            def_ = self._name.get_definition()
                            if def_ is not None and def_.type == 'import_from':
                                continue
                break

        debug.dbg('finder.filter_name %s in (%s): %s@%s',
                  self._string_name, self._context, names, self._position)
        return list(names)

    def _check_getattr(self, inst):
        """Checks for both __getattr__ and __getattribute__ methods"""
        # str is important, because it shouldn't be `Name`!
        name = compiled.create_simple_object(self._evaluator, self._string_name)

        # This is a little bit special. `__getattribute__` is in Python
        # executed before `__getattr__`. But: I know no use case, where
        # this could be practical and where Jedi would return wrong types.
        # If you ever find something, let me know!
        # We are inversing this, because a hand-crafted `__getattribute__`
        # could still call another hand-crafted `__getattr__`, but not the
        # other way around.
        names = (inst.get_function_slot_names(u'__getattr__') or
                 inst.get_function_slot_names(u'__getattribute__'))
        return inst.execute_function_slots(names, name)

    def _names_to_types(self, names, attribute_lookup):
        contexts = ContextSet.from_sets(name.infer() for name in names)

        debug.dbg('finder._names_to_types: %s -> %s', names, contexts)
        if not names and self._context.is_instance() and not self._context.is_compiled():
            # handling __getattr__ / __getattribute__
            return self._check_getattr(self._context)

        # Add isinstance and other if/assert knowledge.
        if not contexts and isinstance(self._name, tree.Name) and \
                not self._name_context.is_instance() and not self._context.is_compiled():
            flow_scope = self._name
            base_nodes = [self._name_context.tree_node]

            if any(b.type in ('comp_for', 'sync_comp_for') for b in base_nodes):
                return contexts
            while True:
                flow_scope = get_parent_scope(flow_scope, include_flows=True)
                n = _check_flow_information(self._name_context, flow_scope,
                                            self._name, self._position)
                if n is not None:
                    return n
                if flow_scope in base_nodes:
                    break
        return contexts


def _check_flow_information(context, flow, search_name, pos):
    """ Try to find out the type of a variable just with the information that
    is given by the flows: e.g. It is also responsible for assert checks.::

        if isinstance(k, str):
            k.  # <- completion here

    ensures that `k` is a string.
    """
    if not settings.dynamic_flow_information:
        return None

    result = None
    if is_scope(flow):
        # Check for asserts.
        module_node = flow.get_root_node()
        try:
            names = module_node.get_used_names()[search_name.value]
        except KeyError:
            return None
        names = reversed([
            n for n in names
            if flow.start_pos <= n.start_pos < (pos or flow.end_pos)
        ])

        for name in names:
            ass = search_ancestor(name, 'assert_stmt')
            if ass is not None:
                result = _check_isinstance_type(context, ass.assertion, search_name)
                if result is not None:
                    return result

    if flow.type in ('if_stmt', 'while_stmt'):
        potential_ifs = [c for c in flow.children[1::4] if c != ':']
        for if_test in reversed(potential_ifs):
            if search_name.start_pos > if_test.end_pos:
                return _check_isinstance_type(context, if_test, search_name)
    return result


def _check_isinstance_type(context, element, search_name):
    try:
        assert element.type in ('power', 'atom_expr')
        # this might be removed if we analyze and, etc
        assert len(element.children) == 2
        first, trailer = element.children
        assert first.type == 'name' and first.value == 'isinstance'
        assert trailer.type == 'trailer' and trailer.children[0] == '('
        assert len(trailer.children) == 3

        # arglist stuff
        arglist = trailer.children[1]
        args = TreeArguments(context.evaluator, context, arglist, trailer)
        param_list = list(args.unpack())
        # Disallow keyword arguments
        assert len(param_list) == 2
        (key1, lazy_context_object), (key2, lazy_context_cls) = param_list
        assert key1 is None and key2 is None
        call = helpers.call_of_leaf(search_name)
        is_instance_call = helpers.call_of_leaf(lazy_context_object.data)
        # Do a simple get_code comparison. They should just have the same code,
        # and everything will be all right.
        normalize = context.evaluator.grammar._normalize
        assert normalize(is_instance_call) == normalize(call)
    except AssertionError:
        return None

    context_set = NO_CONTEXTS
    for cls_or_tup in lazy_context_cls.infer():
        if isinstance(cls_or_tup, iterable.Sequence) and cls_or_tup.array_type == 'tuple':
            for lazy_context in cls_or_tup.py__iter__():
                context_set |= lazy_context.infer().execute_evaluated()
        else:
            context_set |= cls_or_tup.execute_evaluated()
    return context_set
