#! /usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Copyright 2021, Nils Hilbricht, Germany ( https://www.hilbricht.net )

This file is part of the Laborejo Software Suite ( https://www.laborejo.org ),

This application is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
"""

import logging; logger = logging.getLogger(__name__); logger.info("import")


from contextlib import contextmanager

class History(object):
    """Keep track of changes in the session. From file load to quit.

    All functions that need undo and redo follow the same idiom:

    A fuzzy and vague user function starts the process. It may depend
    on the context, like the cursor pitch or the current key signature.

    This function itself, e.g. Note.sharpen, saves its exact old values
    and instances and returns a lambda: undo function with these exact
    values already inserted.

    The undo function must be able to undo itself.
    Redo is undo for undo, it uses the same system. Each undo function
    needs to register an undo itself.

    In other words: The user function, like "sharpen", is only called
    once. After that an internal setter function, without context and
    state, handles undo and redo by using exact values on the exact
    instances (for example note instances).

    We still set and reset the cursor position in undo, but that doesn't
    mean getting the right context or track-state back.
    It is only to enable the user to edit from the same place again
    and also to find the right track to instruct the callbacks to update.


    The history does not know, and subsequently does not differentiate,
    between apply-to-selection or single-item commands. Each history
    function does a set of operations. The history itself is agnostic
    about them.
    """
    def __init__(self):
        self.commandStack = [] # elements are (description, listOfFunctions). listOfFunctions is a real python list.
        self.redoStack = [] #same as self.commandStack
        self.doNotRegisterRightNow = False #prevents self.undo from going into an infinite loop. For example if you undo api.delete it will register insertItem again (which has a register itself with delete as undo function, which will...). There is only one history in the session so this attribute is shared by all undo functions.
        self.duringRedo = False

        self.apiCallback_historySequenceStarted = None
        self.apiCallback_historySequenceStopped = None

    @contextmanager
    def sequence(self, descriptionString):
        """Convenience function. All registrations from now on will be undone at once.
        call stopSequence when done.
        This is meant for high level scripts and not for user-interaction.

        This context can also be used to force a custom name for the undo description, even if it is
        only one step.
        """

        self.apiCallback_historySequenceStarted()


        def sequenceRegister(registeredUndoFunction, descriptionString, listOfFunctions):
            """Modifies a list in place"""
            listOfFunctions.append(registeredUndoFunction)

        listOfFunctions = []
        originalRegister = self.register
        self.register = lambda registeredUndoFunction, descriptionString, l=listOfFunctions: sequenceRegister(registeredUndoFunction, descriptionString, l)

        yield

        self._register(descriptionString, listOfFunctions)
        self.register = originalRegister
        self.apiCallback_historySequenceStopped()


    def register(self, registeredUndoFunction, descriptionString):
        """Register a single undo function but use the same syntax as many functions"""
        self._register(descriptionString, [registeredUndoFunction,])

    def _register(self, descriptionString, listOfFunctions):
        assert type(listOfFunctions) is list, (type(listOfFunctions), listOfFunctions)
        if self.doNotRegisterRightNow:
            self.redoStack.append((descriptionString, listOfFunctions))
        else:
            self.commandStack.append((descriptionString, listOfFunctions))
            if not self.duringRedo:
                self.redoStack = [] #no redo after new commands.

    def clear(self):
        """User-scripts which do not implement their own undo must
        call this to not disturb the history.  Better no history
        than a wrong history."""
        self.commandStack = []
        self.redoStack = []

    def undo(self):
        if self.commandStack:
            self.doNotRegisterRightNow = True

            descriptionString, listOfFunctions = self.commandStack.pop()

            with self.sequence(descriptionString):
                for registeredUndoFunction in reversed(listOfFunctions):
                    registeredUndoFunction()

            self.doNotRegisterRightNow = False
        #print ("pop", len(self.commandStack))

    def redo(self):
        if self.redoStack:
            self.duringRedo = True

            descriptionString, listOfFunctions = self.redoStack.pop()
            with self.sequence(descriptionString):
                for registeredUndoFunction in reversed(listOfFunctions):
                    registeredUndoFunction()

            self.duringRedo = False

    def asList(self):
        return [descr for descr, listOfFunctions in self.commandStack], [descr for descr, listOfFunctions  in self.redoStack]


    def setterWithUndo(self, object, attribute, value, descriptionString, callback):
        oldValue = getattr(object, attribute)
        setattr(object, attribute, value)
        undoFunction = lambda oldValue=oldValue: self.setterWithUndo(object, attribute, oldValue, descriptionString, callback)
        self.register(undoFunction, descriptionString = descriptionString)
        callback()
