import re

import rope.base.change
import rope.contrib.generate
import rope.refactor.change_signature
import rope.refactor.extract
import rope.refactor.inline
import rope.refactor.introduce_factory
import rope.refactor.method_object
import rope.refactor.move
import rope.refactor.rename
import rope.refactor.restructure
import rope.refactor.usefunction
from rope.base import taskhandle
import rope.base.exceptions

from ropemode import dialog, filter


class Refactoring(object):
    key = None
    confs = {}
    optionals = {}
    saveall = True

    def __init__(self, interface, env):
        self.interface = interface
        self.env = env

    def show(self, initial_asking=True):
        self.interface._check_project()
        self.interface._save_buffers(only_current=not self.saveall)
        self._create_refactoring()
        action, result = dialog.show_dialog(
            self.interface._askdata, ['perform', 'preview', 'cancel'],
            self._get_confs(), self._get_optionals(),
            initial_asking=initial_asking)
        if action == 'cancel':
            self.env.message('Cancelled!')
            return
        def calculate(handle):
            return self._calculate_changes(result, handle)
        name = 'Calculating %s changes' % self.name
        changes = runtask(self.env, calculate, name=name)
        if action == 'perform':
            self._perform(changes)
        if action == 'preview':
            if changes is not None:
                diffs = changes.get_description()
                if self.env.preview_changes(diffs):
                    self._perform(changes)
                else:
                    self.env.message('Thrown away!')
            else:
                self.env.message('No changes!')

    @property
    def project(self):
        return self.interface.project

    @property
    def resource(self):
        return self.interface._get_resource()

    @property
    def offset(self):
        return self.env.get_offset()

    @property
    def region(self):
        return self.env.get_region()

    @property
    def name(self):
        return refactoring_name(self.__class__)

    def _calculate_changes(self, option_values, task_handle):
        pass

    def _create_refactoring(self):
        pass

    def _done(self):
        pass

    def _perform(self, changes):
        if changes is None:
            self.env.message('No changes!')
            return
        def perform(handle, self=self, changes=changes):
            self.project.do(changes, task_handle=handle)
            self.interface._reload_buffers(changes)
            self._done()
        runtask(self.env, perform, 'Making %s changes' % self.name,
                interrupts=False)
        self.env.message(str(changes.description) + ' finished')

    def _get_confs(self):
        return self.confs

    def _get_optionals(self):
        return self.optionals

    @property
    def resources_option(self):
        return dialog.Data('Files to apply this refactoring on: ',
                           decode=self._decode_resources)

    def _decode_resources(self, value):
        return _resources(self.project, value)


class Rename(Refactoring):
    key = 'r'

    saveall = True

    def _create_refactoring(self):
        self.renamer = rope.refactor.rename.Rename(
            self.project, self.resource, self.offset)

    def _calculate_changes(self, values, task_handle):
        return self.renamer.get_changes(task_handle=task_handle, **values)

    def _get_optionals(self):
        opts = {}
        opts['docs'] = dialog.Boolean('Search comments and docs: ', True)
        if self.renamer.is_method():
            opts['in_hierarchy'] = dialog.Boolean('Rename methods in '
                                                  'class hierarchy: ')
        opts['resources'] = self.resources_option
        opts['unsure'] = dialog.Data('Unsure occurrences: ',
                                     decode=self._decode_unsure,
                                     values=['ignore', 'match'],
                                     default='ignore')
        return opts

    def _get_confs(self):
        oldname = str(self.renamer.get_old_name())
        return {'new_name': dialog.Data('New name: ', default=oldname)}

    def _decode_unsure(self, value):
        unsure = value == 'match'
        return lambda occurrence: unsure


class RenameCurrentModule(Rename):
    key = '1 r'
    offset = None


class Restructure(Refactoring):
    key = 'x'
    confs = {'pattern': dialog.Data('Restructuring pattern: '),
             'goal': dialog.Data('Restructuring goal: ')}

    def _calculate_changes(self, values, task_handle):
        restructuring = rope.refactor.restructure.Restructure(
            self.project, values['pattern'], values['goal'],
            args=values['args'], imports=values['imports'])
        return restructuring.get_changes(resources=values['resources'],
                                         task_handle=task_handle)

    def _get_optionals(self):
        return {
            'args': dialog.Data('Arguments: ', decode=self._decode_args),
            'imports': dialog.Data('Imports: ', decode=self._decode_imports),
            'resources': self.resources_option}

    def _decode_args(self, value):
        if value:
            args = {}
            for raw_check in value.split('\n'):
                if raw_check:
                    key, value = raw_check.split(':', 1)
                    args[key.strip()] = value.strip()
            return args

    def _decode_imports(self, value):
        if value:
            return [line.strip() for line in value.split('\n')]


class UseFunction(Refactoring):
    key = 'u'

    def _create_refactoring(self):
        self.user = rope.refactor.usefunction.UseFunction(
            self.project, self.resource, self.offset)

    def _calculate_changes(self, values, task_handle):
        return self.user.get_changes(task_handle=task_handle, **values)

    def _get_optionals(self):
        return {'resources': self.resources_option}


class Move(Refactoring):
    key = 'v'

    def _create_refactoring(self):
        self.mover = rope.refactor.move.create_move(self.project,
                                                    self.resource,
                                                    self.offset)

    def _calculate_changes(self, values, task_handle):
        destination = values['destination']
        resources = values.get('resources', None)
        if isinstance(self.mover, rope.refactor.move.MoveGlobal):
            return self._move_global(destination, resources, task_handle)
        if isinstance(self.mover, rope.refactor.move.MoveModule):
            return self._move_module(destination, resources, task_handle)
        if isinstance(self.mover, rope.refactor.move.MoveMethod):
            return self._move_method(destination, resources, task_handle)

    def _move_global(self, dest, resources, handle):
        destination = self.project.pycore.find_module(dest)
        return self.mover.get_changes(
            destination, resources=resources, task_handle=handle)

    def _move_method(self, dest, resources, handle):
        return self.mover.get_changes(
            dest, self.mover.get_method_name(),
            resources=resources, task_handle=handle)

    def _move_module(self, dest, resources, handle):
        destination = self.project.pycore.find_module(dest)
        return self.mover.get_changes(
            destination, resources=resources, task_handle=handle)

    def _get_confs(self):
        if isinstance(self.mover, rope.refactor.move.MoveGlobal):
            prompt = 'Destination module: '
        if isinstance(self.mover, rope.refactor.move.MoveModule):
            prompt = 'Destination package: '
        if isinstance(self.mover, rope.refactor.move.MoveMethod):
            prompt = 'Destination attribute: '
        return {'destination': dialog.Data(prompt)}

    def _get_optionals(self):
        return {'resources': self.resources_option}


class MoveCurrentModule(Move):
    key = '1 v'
    offset = None


class ModuleToPackage(Refactoring):
    key = '1 p'
    saveall = False

    def _create_refactoring(self):
        self.packager = rope.refactor.ModuleToPackage(
            self.project, self.resource)

    def _calculate_changes(self, values, task_handle):
        return self.packager.get_changes()


class Inline(Refactoring):
    key = 'i'

    def _create_refactoring(self):
        self.inliner = rope.refactor.inline.create_inline(
            self.project, self.resource, self.offset)

    def _calculate_changes(self, values, task_handle):
        return self.inliner.get_changes(task_handle=task_handle, **values)

    def _get_optionals(self):
        opts = {'resources': self.resources_option}
        if self.inliner.get_kind() == 'parameter':
            opts['in_hierarchy'] = dialog.Boolean(
                'Apply on all matching methods in class hierarchy: ', False)
        else:
            opts['remove'] = dialog.Boolean('Remove the definition: ', True)
            opts['only_current'] = dialog.Boolean('Inline this '
                                                  'occurrence only: ')
        return opts


class _Extract(Refactoring):
    saveall = False
    optionals = {'similar': dialog.Boolean('Extract similar pieces: ', True),
                 'global_': dialog.Boolean('Make global: ')}
    kind = None
    constructor = None

    def _create_refactoring(self):
        start, end = self.region
        self.extractor = self.constructor(self.project,
                                          self.resource, start, end)

    def _calculate_changes(self, values, task_handle):
        similar = values.get('similar')
        global_ = values.get('global_')
        return self.extractor.get_changes(values['name'], similar=similar,
                                          global_=global_)

    def _get_confs(self):
        return {'name': dialog.Data('Extracted %s name: ' % self.kind)}


class ExtractVariable(_Extract):
    key = 'l'
    kind = 'variable'
    constructor = rope.refactor.extract.ExtractVariable


class ExtractMethod(_Extract):
    key = 'm'
    kind = 'method'
    constructor = rope.refactor.extract.ExtractMethod


class OrganizeImports(Refactoring):
    key = 'o'
    saveall = False

    def _create_refactoring(self):
        self.organizer = rope.refactor.ImportOrganizer(self.project)

    def _calculate_changes(self, values, task_handle):
        return self.organizer.organize_imports(self.resource)


class MethodObject(Refactoring):
    saveall = False
    confs = {'classname': dialog.Data('New class name: ',
                                      default='_ExtractedClass')}

    def _create_refactoring(self):
        self.objecter = rope.refactor.method_object.MethodObject(
            self.project, self.resource, self.offset)

    def _calculate_changes(self, values, task_handle):
        classname = values.get('classname')
        return self.objecter.get_changes(classname)


class IntroduceFactory(Refactoring):
    saveall = True
    key = 'f'

    def _create_refactoring(self):
        self.factory = rope.refactor.introduce_factory.IntroduceFactory(
            self.project, self.resource, self.offset)

    def _calculate_changes(self, values, task_handle):
        return self.factory.get_changes(task_handle=task_handle, **values)

    def _get_confs(self):
        default = 'create_%s' % self.factory.old_name.lower()
        return {'factory_name': dialog.Data('Factory name: ', default)}

    def _get_optionals(self):
        return {'global_factory': dialog.Boolean('Make global: ', True),
                'resources': self.resources_option}


class ChangeSignature(Refactoring):
    saveall = True
    key = 's'

    def _create_refactoring(self):
        self.changer = rope.refactor.change_signature.ChangeSignature(
            self.project, self.resource, self.offset)

    def _calculate_changes(self, values, task_handle):
        signature = values.get('signature')
        args = re.sub(r'[\s\(\)]+', '', signature).split(',')
        olds = [arg[0] for arg in self._get_args()]

        changers = []
        for arg in list(olds):
            if arg in args:
                continue
            changers.append(rope.refactor.change_signature.
                            ArgumentRemover(olds.index(arg)))
            olds.remove(arg)

        order = []
        for index, arg in enumerate(args):
            if arg not in olds:
                changers.append(rope.refactor.change_signature.
                                ArgumentAdder(index, arg))
                olds.insert(index, arg)
            order.append(olds.index(arg))
        changers.append(rope.refactor.change_signature.
                        ArgumentReorderer(order, autodef='None'))

        del values['signature']
        return self.changer.get_changes(changers, task_handle=task_handle,
                                        **values)

    def _get_args(self):
        if hasattr(self.changer, 'get_args'):
            return self.changer.get_args()
        return self.changer.get_definition_info().args_with_defaults

    def _get_confs(self):
        args = []
        for arg, default in self._get_args():
            args.append(arg)
        signature = '(' + ', '.join(args) + ')'
        return {'signature': dialog.Data('Change the signature: ',
                                         default=signature)}

    def _get_optionals(self):
        opts = {'resources': self.resources_option}
        if self.changer.is_method():
            opts['in_hierarchy'] = dialog.Boolean('Rename methods in '
                                                  'class hierarchy: ')
        return opts


class _GenerateElement(Refactoring):

    def _create_refactoring(self):
        kind = self.name.split('_')[-1]
        self.generator = rope.contrib.generate.create_generate(
            kind, self.project, self.resource, self.offset)

    def _calculate_changes(self, values, task_handle):
        return self.generator.get_changes()

    def _done(self):
        resource, lineno = self.generator.get_location()
        self.interface._goto_location(resource, lineno)


class GenerateVariable(_GenerateElement):
    key = 'n v'


class GenerateFunction(_GenerateElement):
    key = 'n f'


class GenerateClass(_GenerateElement):
    key = 'n c'

    def _get_optionals(self):
        return {"destination module": dialog.Data(
                    prompt="Destination module: "
    )}

    def _calculate_changes(self, values, task_handle):
        self.generator = rope.contrib.generate.create_generate(
            self._get_kind(), self.project, self.resource,
            self.offset, goal_resource=self._get_goal_resource(values))
        return self.generator.get_changes()

    def _get_kind(self):
        kind = self.name.split('_')[-1]
        return kind

    def _get_goal_resource(self, values):
        goal_resource = None
        if self._has_destination_module(values):
            goal_resource = self._get_destination_resource_or_raise_error(values, goal_resource)
        return goal_resource

    def _has_destination_module(self, values):
        return "destination module" in values and values["destination module"]

    def _get_destination_resource_or_raise_error(self, values, goal_resource):
        goal_resource = self.project.find_module(values["destination module"])
        if not goal_resource:
            raise rope.base.exceptions.RefactoringError("Module not found")
        return goal_resource


class GenerateModule(_GenerateElement):
    key = 'n m'


class GeneratePackage(_GenerateElement):
    key = 'n p'


def refactoring_name(refactoring):
    classname = refactoring.__name__
    result = []
    for c in classname:
        if result and c.isupper():
            result.append('_')
        result.append(c.lower())
    name = ''.join(result)
    return name

def _resources(project, text):
    if text is None or text.strip() == '':
        return None
    return filter.resources(project, text)


def runtask(env, command, name, interrupts=True):
    return RunTask(env, command, name, interrupts)()

class RunTask(object):

    def __init__(self, env, task, name, interrupts=True):
        self.env = env
        self.task = task
        self.name = name
        self.interrupts = interrupts

    def __call__(self):
        handle = taskhandle.TaskHandle(name=self.name)
        progress = self.env.create_progress(self.name)
        def update_progress():
            jobset = handle.current_jobset()
            if jobset:
                percent = jobset.get_percent_done()
                if percent is not None:
                    progress.update(percent)
        handle.add_observer(update_progress)
        result = self.task(handle)
        progress.done()
        return result
