"""
One of the really important features of |jedi| is to have an option to
understand code like this::

    def foo(bar):
        bar. # completion here
    foo(1)

There's no doubt wheter bar is an ``int`` or not, but if there's also a call
like ``foo('str')``, what would happen? Well, we'll just show both. Because
that's what a human would expect.

It works as follows:

- |Jedi| sees a param
- search for function calls named ``foo``
- execute these calls and check the input. This work with a ``ParamListener``.
"""
from itertools import chain

from jedi._compatibility import unicode
from jedi.parser import tree as pr
from jedi import settings
from jedi import debug
from jedi.evaluate.cache import memoize_default
from jedi.evaluate import imports


class ParamListener(object):
    """
    This listener is used to get the params for a function.
    """
    def __init__(self):
        self.param_possibilities = []

    def execute(self, params):
        self.param_possibilities += params


@debug.increase_indent
def search_params(evaluator, param):
    """
    A dynamic search for param values. If you try to complete a type:

    >>> def func(foo):
    ...     foo
    >>> func(1)
    >>> func("")

    It is not known what the type ``foo`` without analysing the whole code. You
    have to look for all calls to ``func`` to find out what ``foo`` possibly
    is.
    """
    if not settings.dynamic_params:
        return []
    debug.dbg('Dynamic param search for %s', param)

    func = param.get_parent_until(pr.Function)
    # Compare the param names.
    names = [n for n in search_function_call(evaluator, func)
             if n.value == param.name.value]
    # Evaluate the ExecutedParams to types.
    result = list(chain.from_iterable(n.parent.eval(evaluator) for n in names))
    debug.dbg('Dynamic param result %s', result)
    return result


@memoize_default([], evaluator_is_first_arg=True)
def search_function_call(evaluator, func):
    """
    Returns a list of param names.
    """
    from jedi.evaluate import representation as er

    def get_params_for_module(module):
        """
        Returns the values of a param, or an empty array.
        """
        @memoize_default([], evaluator_is_first_arg=True)
        def get_posibilities(evaluator, module, func_name):
            try:
                names = module.used_names[func_name]
            except KeyError:
                return []

            for name in names:
                parent = name.parent
                if pr.is_node(parent, 'trailer'):
                    parent = parent.parent

                trailer = None
                if pr.is_node(parent, 'power'):
                    for t in parent.children[1:]:
                        if t == '**':
                            break
                        if t.start_pos > name.start_pos and t.children[0] == '(':
                            trailer = t
                            break
                if trailer is not None:
                    types = evaluator.goto_definition(name)

                    # We have to remove decorators, because they are not the
                    # "original" functions, this way we can easily compare.
                    # At the same time we also have to remove InstanceElements.
                    undec = []
                    for escope in types:
                        if escope.isinstance(er.Function, er.Instance) \
                                and escope.decorates is not None:
                            undec.append(escope.decorates)
                        elif isinstance(escope, er.InstanceElement):
                            undec.append(escope.var)
                        else:
                            undec.append(escope)

                    if er.wrap(evaluator, compare) in undec:
                        # Only if we have the correct function we execute
                        # it, otherwise just ignore it.
                        evaluator.eval_trailer(types, trailer)
            return listener.param_possibilities
        return get_posibilities(evaluator, module, func_name)

    current_module = func.get_parent_until()
    func_name = unicode(func.name)
    compare = func
    if func_name == '__init__':
        cls = func.get_parent_scope()
        if isinstance(cls, pr.Class):
            func_name = unicode(cls.name)
            compare = cls

    # add the listener
    listener = ParamListener()
    func.listeners.add(listener)

    try:
        result = []
        # This is like backtracking: Get the first possible result.
        for mod in imports.get_modules_containing_name(evaluator, [current_module], func_name):
            result = get_params_for_module(mod)
            if result:
                break
    finally:
        # cleanup: remove the listener; important: should not stick.
        func.listeners.remove(listener)

    return result

[(68, 'jedi.evaluate.cache.memoize_default', 'memoize_default', False, 'from jedi.evaluate.cache import memoize_default\n'), (56, 'jedi.debug.dbg', 'debug.dbg', False, 'from jedi import debug\n'), (64, 'jedi.debug.dbg', 'debug.dbg', False, 'from jedi import debug\n'), (123, 'jedi._compatibility.unicode', 'unicode', False, 'from jedi._compatibility import unicode\n'), (79, 'jedi.evaluate.cache.memoize_default', 'memoize_default', False, 'from jedi.evaluate.cache import memoize_default\n'), (138, 'jedi.evaluate.imports.get_modules_containing_name', 'imports.get_modules_containing_name', False, 'from jedi.evaluate import imports\n'), (128, 'jedi._compatibility.unicode', 'unicode', False, 'from jedi._compatibility import unicode\n'), (88, 'jedi.parser.tree.is_node', 'pr.is_node', True, 'from jedi.parser import tree as pr\n'), (92, 'jedi.parser.tree.is_node', 'pr.is_node', True, 'from jedi.parser import tree as pr\n'), (115, 'jedi.evaluate.representation.wrap', 'er.wrap', True, 'from jedi.evaluate import representation as er\n')]