from rope.base import exceptions, change, taskhandle


class History(object):
    """A class that holds project history"""

    def __init__(self, project, maxundos=None):
        self.project = project
        self._undo_list = []
        self._redo_list = []
        self._maxundos = maxundos
        self._load_history()
        self.project.data_files.add_write_hook(self.write)
        self.current_change = None

    def _load_history(self):
        if self.save:
            result = self.project.data_files.read_data(
                'history', compress=self.compress, import_=True)
            if result is not None:
                to_change = change.DataToChange(self.project)
                for data in result[0]:
                    self._undo_list.append(to_change(data))
                for data in result[1]:
                    self._redo_list.append(to_change(data))

    def do(self, changes, task_handle=taskhandle.NullTaskHandle()):
        """Perform the change and add it to the `self.undo_list`

        Note that uninteresting changes (changes to ignored files)
        will not be appended to `self.undo_list`.

        """
        try:
            self.current_change = changes
            changes.do(change.create_job_set(task_handle, changes))
        finally:
            self.current_change = None
        if self._is_change_interesting(changes):
            self.undo_list.append(changes)
            self._remove_extra_items()
        del self.redo_list[:]

    def _remove_extra_items(self):
        if len(self.undo_list) > self.max_undos:
            del self.undo_list[0:len(self.undo_list) - self.max_undos]

    def _is_change_interesting(self, changes):
        for resource in changes.get_changed_resources():
            if not self.project.is_ignored(resource):
                return True
        return False

    def undo(self, change=None, drop=False,
             task_handle=taskhandle.NullTaskHandle()):
        """Redo done changes from the history

        When `change` is `None`, the last done change will be undone.
        If change is not `None` it should be an item from
        `self.undo_list`; this change and all changes that depend on
        it will be undone.  In both cases the list of undone changes
        will be returned.

        If `drop` is `True`, the undone change will not be appended to
        the redo list.

        """
        if not self._undo_list:
            raise exceptions.HistoryError('Undo list is empty')
        if change is None:
            change = self.undo_list[-1]
        dependencies = self._find_dependencies(self.undo_list, change)
        self._move_front(self.undo_list, dependencies)
        self._perform_undos(len(dependencies), task_handle)
        result = self.redo_list[-len(dependencies):]
        if drop:
            del self.redo_list[-len(dependencies):]
        return result

    def redo(self, change=None, task_handle=taskhandle.NullTaskHandle()):
        """Redo undone changes from the history

        When `change` is `None`, the last undone change will be
        redone.  If change is not `None` it should be an item from
        `self.redo_list`; this change and all changes that depend on
        it will be redone.  In both cases the list of redone changes
        will be returned.

        """
        if not self.redo_list:
            raise exceptions.HistoryError('Redo list is empty')
        if change is None:
            change = self.redo_list[-1]
        dependencies = self._find_dependencies(self.redo_list, change)
        self._move_front(self.redo_list, dependencies)
        self._perform_redos(len(dependencies), task_handle)
        return self.undo_list[-len(dependencies):]

    def _move_front(self, change_list, changes):
        for change in changes:
            change_list.remove(change)
            change_list.append(change)

    def _find_dependencies(self, change_list, change):
        index = change_list.index(change)
        return _FindChangeDependencies(change_list[index:])()

    def _perform_undos(self, count, task_handle):
        for i in range(count):
            self.current_change = self.undo_list[-1]
            try:
                job_set = change.create_job_set(task_handle,
                                                self.current_change)
                self.current_change.undo(job_set)
            finally:
                self.current_change = None
            self.redo_list.append(self.undo_list.pop())

    def _perform_redos(self, count, task_handle):
        for i in range(count):
            self.current_change = self.redo_list[-1]
            try:
                job_set = change.create_job_set(task_handle,
                                                self.current_change)
                self.current_change.do(job_set)
            finally:
                self.current_change = None
            self.undo_list.append(self.redo_list.pop())

    def contents_before_current_change(self, file):
        if self.current_change is None:
            return None
        result = self._search_for_change_contents([self.current_change], file)
        if result is not None:
            return result
        if file.exists() and not file.is_folder():
            return file.read()
        else:
            return None

    def _search_for_change_contents(self, change_list, file):
        for change_ in reversed(change_list):
            if isinstance(change_, change.ChangeSet):
                result = self._search_for_change_contents(change_.changes,
                                                          file)
                if result is not None:
                    return result
            if isinstance(change_, change.ChangeContents) and \
               change_.resource == file:
                return change_.old_contents

    def write(self):
        if self.save:
            data = []
            to_data = change.ChangeToData()
            self._remove_extra_items()
            data.append([to_data(change_) for change_ in self.undo_list])
            data.append([to_data(change_) for change_ in self.redo_list])
            self.project.data_files.write_data('history', data,
                                               compress=self.compress)

    def get_file_undo_list(self, resource):
        result = []
        for change in self.undo_list:
            if resource in change.get_changed_resources():
                result.append(change)
        return result

    def __str__(self):
        return 'History holds %s changes in memory' % \
               (len(self.undo_list) + len(self.redo_list))

    undo_list = property(lambda self: self._undo_list)
    redo_list = property(lambda self: self._redo_list)

    @property
    def tobe_undone(self):
        """The last done change if available, `None` otherwise"""
        if self.undo_list:
            return self.undo_list[-1]

    @property
    def tobe_redone(self):
        """The last undone change if available, `None` otherwise"""
        if self.redo_list:
            return self.redo_list[-1]

    @property
    def max_undos(self):
        if self._maxundos is None:
            return self.project.prefs.get('max_history_items', 100)
        else:
            return self._maxundos

    @property
    def save(self):
        return self.project.prefs.get('save_history', False)

    @property
    def compress(self):
        return self.project.prefs.get('compress_history', False)

    def clear(self):
        """Forget all undo and redo information"""
        del self.undo_list[:]
        del self.redo_list[:]


class _FindChangeDependencies(object):

    def __init__(self, change_list):
        self.change = change_list[0]
        self.change_list = change_list
        self.changed_resources = set(self.change.get_changed_resources())

    def __call__(self):
        result = [self.change]
        for change in self.change_list[1:]:
            if self._depends_on(change, result):
                result.append(change)
                self.changed_resources.update(change.get_changed_resources())
        return result

    def _depends_on(self, changes, result):
        for resource in changes.get_changed_resources():
            if resource is None:
                continue
            if resource in self.changed_resources:
                return True
            for changed in self.changed_resources:
                if resource.is_folder() and resource.contains(changed):
                    return True
                if changed.is_folder() and changed.contains(resource):
                    return True
        return False
