import rope.base.exceptions
import rope.base.pyobjects
from rope.base.builtins import Lambda
from rope.base import worder


class DefinitionInfo(object):

    def __init__(self, function_name, is_method, args_with_defaults,
                 args_arg, keywords_arg):
        self.function_name = function_name
        self.is_method = is_method
        self.args_with_defaults = args_with_defaults
        self.args_arg = args_arg
        self.keywords_arg = keywords_arg

    def to_string(self):
        return '%s(%s)' % (self.function_name, self.arguments_to_string())

    def arguments_to_string(self, from_index=0):
        params = []
        for arg, default in self.args_with_defaults:
            if default is not None:
                params.append('%s=%s' % (arg, default))
            else:
                params.append(arg)
        if self.args_arg is not None:
            params.append('*' + self.args_arg)
        if self.keywords_arg:
            params.append('**' + self.keywords_arg)
        return ', '.join(params[from_index:])

    @staticmethod
    def _read(pyfunction, code):
        kind = pyfunction.get_kind()
        is_method = kind == 'method'
        is_lambda = kind == 'lambda'
        info = _FunctionParser(code, is_method, is_lambda)
        args, keywords = info.get_parameters()
        args_arg = None
        keywords_arg = None
        if args and args[-1].startswith('**'):
            keywords_arg = args[-1][2:]
            del args[-1]
        if args and args[-1].startswith('*'):
            args_arg = args[-1][1:]
            del args[-1]
        args_with_defaults = [(name, None) for name in args]
        args_with_defaults.extend(keywords)
        return DefinitionInfo(info.get_function_name(), is_method,
                              args_with_defaults, args_arg, keywords_arg)

    @staticmethod
    def read(pyfunction):
        pymodule = pyfunction.get_module()
        word_finder = worder.Worder(pymodule.source_code)
        lineno = pyfunction.get_ast().lineno
        start = pymodule.lines.get_line_start(lineno)
        if isinstance(pyfunction, Lambda):
            call = word_finder.get_lambda_and_args(start)
        else:
            call = word_finder.get_function_and_args_in_header(start)
        return DefinitionInfo._read(pyfunction, call)


class CallInfo(object):

    def __init__(self, function_name, args, keywords, args_arg,
                 keywords_arg, implicit_arg, constructor):
        self.function_name = function_name
        self.args = args
        self.keywords = keywords
        self.args_arg = args_arg
        self.keywords_arg = keywords_arg
        self.implicit_arg = implicit_arg
        self.constructor = constructor

    def to_string(self):
        function = self.function_name
        if self.implicit_arg:
            function = self.args[0] + '.' + self.function_name
        params = []
        start = 0
        if self.implicit_arg or self.constructor:
            start = 1
        if self.args[start:]:
            params.extend(self.args[start:])
        if self.keywords:
            params.extend(['%s=%s' % (name, value)
                          for name, value in self.keywords])
        if self.args_arg is not None:
            params.append('*' + self.args_arg)
        if self.keywords_arg:
            params.append('**' + self.keywords_arg)
        return '%s(%s)' % (function, ', '.join(params))

    @staticmethod
    def read(primary, pyname, definition_info, code):
        is_method_call = CallInfo._is_method_call(primary, pyname)
        is_constructor = CallInfo._is_class(pyname)
        is_classmethod = CallInfo._is_classmethod(pyname)
        info = _FunctionParser(code, is_method_call or is_classmethod)
        args, keywords = info.get_parameters()
        args_arg = None
        keywords_arg = None
        if args and args[-1].startswith('**'):
            keywords_arg = args[-1][2:]
            del args[-1]
        if args and args[-1].startswith('*'):
            args_arg = args[-1][1:]
            del args[-1]
        if is_constructor:
            args.insert(0, definition_info.args_with_defaults[0][0])
        return CallInfo(info.get_function_name(), args, keywords, args_arg,
                        keywords_arg, is_method_call or is_classmethod,
                        is_constructor)

    @staticmethod
    def _is_method_call(primary, pyname):
        return primary is not None and \
            isinstance(primary.get_object().get_type(),
                       rope.base.pyobjects.PyClass) and \
            CallInfo._is_method(pyname)

    @staticmethod
    def _is_class(pyname):
        return pyname is not None and \
            isinstance(pyname.get_object(),
                       rope.base.pyobjects.PyClass)

    @staticmethod
    def _is_method(pyname):
        if pyname is not None and \
           isinstance(pyname.get_object(), rope.base.pyobjects.PyFunction):
            return pyname.get_object().get_kind() == 'method'
        return False

    @staticmethod
    def _is_classmethod(pyname):
        if pyname is not None and \
           isinstance(pyname.get_object(), rope.base.pyobjects.PyFunction):
            return pyname.get_object().get_kind() == 'classmethod'
        return False


class ArgumentMapping(object):

    def __init__(self, definition_info, call_info):
        self.call_info = call_info
        self.param_dict = {}
        self.keyword_args = []
        self.args_arg = []
        for index, value in enumerate(call_info.args):
            if index < len(definition_info.args_with_defaults):
                name = definition_info.args_with_defaults[index][0]
                self.param_dict[name] = value
            else:
                self.args_arg.append(value)
        for name, value in call_info.keywords:
            index = -1
            for pair in definition_info.args_with_defaults:
                if pair[0] == name:
                    self.param_dict[name] = value
                    break
            else:
                self.keyword_args.append((name, value))

    def to_call_info(self, definition_info):
        args = []
        keywords = []
        for index in range(len(definition_info.args_with_defaults)):
            name = definition_info.args_with_defaults[index][0]
            if name in self.param_dict:
                args.append(self.param_dict[name])
            else:
                for i in range(index, len(definition_info.args_with_defaults)):
                    name = definition_info.args_with_defaults[i][0]
                    if name in self.param_dict:
                        keywords.append((name, self.param_dict[name]))
                break
        args.extend(self.args_arg)
        keywords.extend(self.keyword_args)
        return CallInfo(self.call_info.function_name, args, keywords,
                        self.call_info.args_arg, self.call_info.keywords_arg,
                        self.call_info.implicit_arg,
                        self.call_info.constructor)


class _FunctionParser(object):

    def __init__(self, call, implicit_arg, is_lambda=False):
        self.call = call
        self.implicit_arg = implicit_arg
        self.word_finder = worder.Worder(self.call)
        if is_lambda:
            self.last_parens = self.call.rindex(':')
        else:
            self.last_parens = self.call.rindex(')')
        self.first_parens = self.word_finder._find_parens_start(
            self.last_parens)

    def get_parameters(self):
        args, keywords = self.word_finder.get_parameters(self.first_parens,
                                                         self.last_parens)
        if self.is_called_as_a_method():
            instance = self.call[:self.call.rindex('.', 0, self.first_parens)]
            args.insert(0, instance.strip())
        return args, keywords

    def get_instance(self):
        if self.is_called_as_a_method():
            return self.word_finder.get_primary_at(
                self.call.rindex('.', 0, self.first_parens) - 1)

    def get_function_name(self):
        if self.is_called_as_a_method():
            return self.word_finder.get_word_at(self.first_parens - 1)
        else:
            return self.word_finder.get_primary_at(self.first_parens - 1)

    def is_called_as_a_method(self):
        return self.implicit_arg and '.' in self.call[:self.first_parens]
