import copy

import rope.base.exceptions
from rope.base import codeanalyze
from rope.base import evaluate
from rope.base import pyobjects
from rope.base import taskhandle
from rope.base import utils
from rope.base import worder
from rope.base.change import ChangeContents, ChangeSet
from rope.refactor import occurrences, functionutils


class ChangeSignature(object):
    def __init__(self, project, resource, offset):
        self.project = project
        self.resource = resource
        self.offset = offset
        self._set_name_and_pyname()
        if (
            self.pyname is None
            or self.pyname.get_object() is None
            or not isinstance(self.pyname.get_object(), pyobjects.PyFunction)
        ):
            raise rope.base.exceptions.RefactoringError(
                "Change method signature should be performed on functions"
            )

    def _set_name_and_pyname(self):
        self.name = worder.get_name_at(self.resource, self.offset)
        this_pymodule = self.project.get_pymodule(self.resource)
        self.primary, self.pyname = evaluate.eval_location2(this_pymodule, self.offset)
        if self.pyname is None:
            return
        pyobject = self.pyname.get_object()
        if isinstance(pyobject, pyobjects.PyClass) and "__init__" in pyobject:
            self.pyname = pyobject["__init__"]
            self.name = "__init__"
        pyobject = self.pyname.get_object()
        self.others = None
        if (
            self.name == "__init__"
            and isinstance(pyobject, pyobjects.PyFunction)
            and isinstance(pyobject.parent, pyobjects.PyClass)
        ):
            pyclass = pyobject.parent
            self.others = (pyclass.get_name(), pyclass.parent[pyclass.get_name()])

    def _change_calls(
        self,
        call_changer,
        in_hierarchy=None,
        resources=None,
        handle=taskhandle.NullTaskHandle(),
    ):
        if resources is None:
            resources = self.project.get_python_files()
        changes = ChangeSet("Changing signature of <%s>" % self.name)
        job_set = handle.create_jobset("Collecting Changes", len(resources))
        finder = occurrences.create_finder(
            self.project,
            self.name,
            self.pyname,
            instance=self.primary,
            in_hierarchy=in_hierarchy and self.is_method(),
        )
        if self.others:
            name, pyname = self.others
            constructor_finder = occurrences.create_finder(
                self.project, name, pyname, only_calls=True
            )
            finder = _MultipleFinders([finder, constructor_finder])
        for file in resources:
            job_set.started_job(file.path)
            change_calls = _ChangeCallsInModule(
                self.project, finder, file, call_changer
            )
            changed_file = change_calls.get_changed_module()
            if changed_file is not None:
                changes.add_change(ChangeContents(file, changed_file))
            job_set.finished_job()
        return changes

    def get_args(self):
        """Get function arguments.

        Return a list of ``(name, default)`` tuples for all but star
        and double star arguments.  For arguments that don't have a
        default, `None` will be used.
        """
        return self._definfo().args_with_defaults

    def is_method(self):
        pyfunction = self.pyname.get_object()
        return isinstance(pyfunction.parent, pyobjects.PyClass)

    @utils.deprecated("Use `ChangeSignature.get_args()` instead")
    def get_definition_info(self):
        return self._definfo()

    def _definfo(self):
        return functionutils.DefinitionInfo.read(self.pyname.get_object())

    @utils.deprecated()
    def normalize(self):
        changer = _FunctionChangers(
            self.pyname.get_object(), self.get_definition_info(), [ArgumentNormalizer()]
        )
        return self._change_calls(changer)

    @utils.deprecated()
    def remove(self, index):
        changer = _FunctionChangers(
            self.pyname.get_object(),
            self.get_definition_info(),
            [ArgumentRemover(index)],
        )
        return self._change_calls(changer)

    @utils.deprecated()
    def add(self, index, name, default=None, value=None):
        changer = _FunctionChangers(
            self.pyname.get_object(),
            self.get_definition_info(),
            [ArgumentAdder(index, name, default, value)],
        )
        return self._change_calls(changer)

    @utils.deprecated()
    def inline_default(self, index):
        changer = _FunctionChangers(
            self.pyname.get_object(),
            self.get_definition_info(),
            [ArgumentDefaultInliner(index)],
        )
        return self._change_calls(changer)

    @utils.deprecated()
    def reorder(self, new_ordering):
        changer = _FunctionChangers(
            self.pyname.get_object(),
            self.get_definition_info(),
            [ArgumentReorderer(new_ordering)],
        )
        return self._change_calls(changer)

    def get_changes(
        self,
        changers,
        in_hierarchy=False,
        resources=None,
        task_handle=taskhandle.NullTaskHandle(),
    ):
        """Get changes caused by this refactoring

        `changers` is a list of `_ArgumentChanger`.  If `in_hierarchy`
        is `True` the changers are applyed to all matching methods in
        the class hierarchy.
        `resources` can be a list of `rope.base.resource.File` that
        should be searched for occurrences; if `None` all python files
        in the project are searched.

        """
        function_changer = _FunctionChangers(
            self.pyname.get_object(), self._definfo(), changers
        )
        return self._change_calls(
            function_changer, in_hierarchy, resources, task_handle
        )


class _FunctionChangers(object):
    def __init__(self, pyfunction, definition_info, changers=None):
        self.pyfunction = pyfunction
        self.definition_info = definition_info
        self.changers = changers
        self.changed_definition_infos = self._get_changed_definition_infos()

    def _get_changed_definition_infos(self):
        result = []
        definition_info = self.definition_info
        result.append(definition_info)
        for changer in self.changers:
            definition_info = copy.deepcopy(definition_info)
            changer.change_definition_info(definition_info)
            result.append(definition_info)
        return result

    def change_definition(self, call):
        return self.changed_definition_infos[-1].to_string()

    def change_call(self, primary, pyname, call):
        call_info = functionutils.CallInfo.read(
            primary, pyname, self.definition_info, call
        )
        mapping = functionutils.ArgumentMapping(self.definition_info, call_info)

        for definition_info, changer in zip(
            self.changed_definition_infos, self.changers
        ):
            changer.change_argument_mapping(definition_info, mapping)

        return mapping.to_call_info(self.changed_definition_infos[-1]).to_string()


class _ArgumentChanger(object):
    def change_definition_info(self, definition_info):
        pass

    def change_argument_mapping(self, definition_info, argument_mapping):
        pass


class ArgumentNormalizer(_ArgumentChanger):
    pass


class ArgumentRemover(_ArgumentChanger):
    def __init__(self, index):
        self.index = index

    def change_definition_info(self, call_info):
        if self.index < len(call_info.args_with_defaults):
            del call_info.args_with_defaults[self.index]
        elif (
            self.index == len(call_info.args_with_defaults)
            and call_info.args_arg is not None
        ):
            call_info.args_arg = None
        elif (
            self.index == len(call_info.args_with_defaults)
            and call_info.args_arg is None
            and call_info.keywords_arg is not None
        ) or (
            self.index == len(call_info.args_with_defaults) + 1
            and call_info.args_arg is not None
            and call_info.keywords_arg is not None
        ):
            call_info.keywords_arg = None

    def change_argument_mapping(self, definition_info, mapping):
        if self.index < len(definition_info.args_with_defaults):
            name = definition_info.args_with_defaults[0]
            if name in mapping.param_dict:
                del mapping.param_dict[name]


class ArgumentAdder(_ArgumentChanger):
    def __init__(self, index, name, default=None, value=None):
        self.index = index
        self.name = name
        self.default = default
        self.value = value

    def change_definition_info(self, definition_info):
        for pair in definition_info.args_with_defaults:
            if pair[0] == self.name:
                raise rope.base.exceptions.RefactoringError(
                    "Adding duplicate parameter: <%s>." % self.name
                )
        definition_info.args_with_defaults.insert(self.index, (self.name, self.default))

    def change_argument_mapping(self, definition_info, mapping):
        if self.value is not None:
            mapping.param_dict[self.name] = self.value


class ArgumentDefaultInliner(_ArgumentChanger):
    def __init__(self, index):
        self.index = index
        self.remove = False

    def change_definition_info(self, definition_info):
        if self.remove:
            definition_info.args_with_defaults[self.index] = (
                definition_info.args_with_defaults[self.index][0],
                None,
            )

    def change_argument_mapping(self, definition_info, mapping):
        default = definition_info.args_with_defaults[self.index][1]
        name = definition_info.args_with_defaults[self.index][0]
        if default is not None and name not in mapping.param_dict:
            mapping.param_dict[name] = default


class ArgumentReorderer(_ArgumentChanger):
    def __init__(self, new_order, autodef=None):
        """Construct an `ArgumentReorderer`

        Note that the `new_order` is a list containing the new
        position of parameters; not the position each parameter
        is going to be moved to. (changed in ``0.5m4``)

        For example changing ``f(a, b, c)`` to ``f(c, a, b)``
        requires passing ``[2, 0, 1]`` and *not* ``[1, 2, 0]``.

        The `autodef` (automatic default) argument, forces rope to use
        it as a default if a default is needed after the change.  That
        happens when an argument without default is moved after
        another that has a default value.  Note that `autodef` should
        be a string or `None`; the latter disables adding automatic
        default.

        """
        self.new_order = new_order
        self.autodef = autodef

    def change_definition_info(self, definition_info):
        new_args = list(definition_info.args_with_defaults)
        for new_index, index in enumerate(self.new_order):
            new_args[new_index] = definition_info.args_with_defaults[index]
        seen_default = False
        for index, (arg, default) in enumerate(list(new_args)):
            if default is not None:
                seen_default = True
            if seen_default and default is None and self.autodef is not None:
                new_args[index] = (arg, self.autodef)
        definition_info.args_with_defaults = new_args


class _ChangeCallsInModule(object):
    def __init__(self, project, occurrence_finder, resource, call_changer):
        self.project = project
        self.occurrence_finder = occurrence_finder
        self.resource = resource
        self.call_changer = call_changer

    def get_changed_module(self):
        word_finder = worder.Worder(self.source)
        change_collector = codeanalyze.ChangeCollector(self.source)
        for occurrence in self.occurrence_finder.find_occurrences(self.resource):
            if not occurrence.is_called() and not occurrence.is_defined():
                continue
            start, end = occurrence.get_primary_range()
            begin_parens, end_parens = word_finder.get_word_parens_range(end - 1)
            if occurrence.is_called():
                primary, pyname = occurrence.get_primary_and_pyname()
                changed_call = self.call_changer.change_call(
                    primary, pyname, self.source[start:end_parens]
                )
            else:
                changed_call = self.call_changer.change_definition(
                    self.source[start:end_parens]
                )
            if changed_call is not None:
                change_collector.add_change(start, end_parens, changed_call)
        return change_collector.get_changed()

    @property
    @utils.saveit
    def pymodule(self):
        return self.project.get_pymodule(self.resource)

    @property
    @utils.saveit
    def source(self):
        if self.resource is not None:
            return self.resource.read()
        else:
            return self.pymodule.source_code

    @property
    @utils.saveit
    def lines(self):
        return self.pymodule.lines


class _MultipleFinders(object):
    def __init__(self, finders):
        self.finders = finders

    def find_occurrences(self, resource=None, pymodule=None):
        all_occurrences = []
        for finder in self.finders:
            all_occurrences.extend(finder.find_occurrences(resource, pymodule))
        all_occurrences.sort(key=lambda x: x.get_primary_range())
        return all_occurrences
