import rope.base.exceptions
import rope.base.pyobjects
from rope.base import libutils
from rope.base import taskhandle, evaluate
from rope.base.change import (ChangeSet, ChangeContents)
from rope.refactor import rename, occurrences, sourceutils, importutils


class IntroduceFactory(object):

    def __init__(self, project, resource, offset):
        self.project = project
        self.offset = offset

        this_pymodule = self.project.get_pymodule(resource)
        self.old_pyname = evaluate.eval_location(this_pymodule, offset)
        if self.old_pyname is None or \
                not isinstance(self.old_pyname.get_object(),
                               rope.base.pyobjects.PyClass):
            raise rope.base.exceptions.RefactoringError(
                'Introduce factory should be performed on a class.')
        self.old_name = self.old_pyname.get_object().get_name()
        self.pymodule = self.old_pyname.get_object().get_module()
        self.resource = self.pymodule.get_resource()

    def get_changes(self, factory_name, global_factory=False, resources=None,
                    task_handle=taskhandle.NullTaskHandle()):
        """Get the changes this refactoring makes

        `factory_name` indicates the name of the factory function to
        be added.  If `global_factory` is `True` the factory will be
        global otherwise a static method is added to the class.

        `resources` can be a list of `rope.base.resource.File`\s that
        this refactoring should be applied on; if `None` all python
        files in the project are searched.

        """
        if resources is None:
            resources = self.project.get_python_files()
        changes = ChangeSet('Introduce factory method <%s>' % factory_name)
        job_set = task_handle.create_jobset('Collecting Changes',
                                            len(resources))
        self._change_module(resources, changes, factory_name,
                            global_factory, job_set)
        return changes

    def get_name(self):
        """Return the name of the class"""
        return self.old_name

    def _change_module(self, resources, changes,
                       factory_name, global_, job_set):
        if global_:
            replacement = '__rope_factory_%s_' % factory_name
        else:
            replacement = self._new_function_name(factory_name, global_)

        for file_ in resources:
            job_set.started_job(file_.path)
            if file_ == self.resource:
                self._change_resource(changes, factory_name, global_)
                job_set.finished_job()
                continue
            changed_code = self._rename_occurrences(file_, replacement,
                                                    global_)
            if changed_code is not None:
                if global_:
                    new_pymodule = libutils.get_string_module(
                        self.project, changed_code, self.resource)
                    modname = libutils.modname(self.resource)
                    changed_code, imported = importutils.add_import(
                        self.project, new_pymodule, modname, factory_name)
                    changed_code = changed_code.replace(replacement, imported)
                changes.add_change(ChangeContents(file_, changed_code))
            job_set.finished_job()

    def _change_resource(self, changes, factory_name, global_):
        class_scope = self.old_pyname.get_object().get_scope()
        source_code = self._rename_occurrences(
            self.resource, self._new_function_name(factory_name,
                                                   global_), global_)
        if source_code is None:
            source_code = self.pymodule.source_code
        else:
            self.pymodule = libutils.get_string_module(
                self.project, source_code, resource=self.resource)
        lines = self.pymodule.lines
        start = self._get_insertion_offset(class_scope, lines)
        result = source_code[:start]
        result += self._get_factory_method(lines, class_scope,
                                           factory_name, global_)
        result += source_code[start:]
        changes.add_change(ChangeContents(self.resource, result))

    def _get_insertion_offset(self, class_scope, lines):
        start_line = class_scope.get_end()
        if class_scope.get_scopes():
            start_line = class_scope.get_scopes()[-1].get_end()
        start = lines.get_line_end(start_line) + 1
        return start

    def _get_factory_method(self, lines, class_scope,
                            factory_name, global_):
        unit_indents = ' ' * sourceutils.get_indent(self.project)
        if global_:
            if self._get_scope_indents(lines, class_scope) > 0:
                raise rope.base.exceptions.RefactoringError(
                    'Cannot make global factory method for nested classes.')
            return ('\ndef %s(*args, **kwds):\n%sreturn %s(*args, **kwds)\n' %
                    (factory_name, unit_indents, self.old_name))
        unindented_factory = \
            ('@staticmethod\ndef %s(*args, **kwds):\n' % factory_name +
             '%sreturn %s(*args, **kwds)\n' % (unit_indents, self.old_name))
        indents = self._get_scope_indents(lines, class_scope) + \
            sourceutils.get_indent(self.project)
        return '\n' + sourceutils.indent_lines(unindented_factory, indents)

    def _get_scope_indents(self, lines, scope):
        return sourceutils.get_indents(lines, scope.get_start())

    def _new_function_name(self, factory_name, global_):
        if global_:
            return factory_name
        else:
            return self.old_name + '.' + factory_name

    def _rename_occurrences(self, file_, changed_name, global_factory):
        finder = occurrences.create_finder(self.project, self.old_name,
                                           self.old_pyname, only_calls=True)
        result = rename.rename_in_module(finder, changed_name, resource=file_,
                                         replace_primary=global_factory)
        return result

IntroduceFactoryRefactoring = IntroduceFactory
