package theorycrafter.utils


/**
 * A queue of actions that can be performed and reverted.
 */
class UndoRedoQueue<C>(
    val contextProvider: () -> C
) {


    /**
     * The list of actions.
     */
    private val actions: MutableList<UndoRedoAction<C>> = mutableListOf()


    /**
     * The index of the next action to perform (in [actions]).
     */
    private var index = 0


    /**
     * Performs the given action and appends it to the undo queue.
     */
    suspend fun performAndAppend(action: UndoRedoAction<C>) {
        with(contextProvider()) {
            action.perform()
        }
        append(action)
    }


    /**
     * Replaces the last action in the queue, or appends a new one.
     *
     * [merge] receives the last action in the queue and returns an action to be performed and whether it should replace
     * it in the queue (if `false`, it's appended to the queue). This allows multiple similar actions to be merged into
     * one action that can be undone as one. For example, when the action is controlled by a slider, this allows the
     * whole drag gesture to be registered as one change.
     *
     * Note that if the last action in the queue is `null`, it cannot be replaced.
     */
    suspend fun performAndMergeOrAppend(
        merge: (lastAction: UndoRedoAction<C>?) -> Pair<UndoRedoAction<C>, Boolean>
    ) {
        val (mergedAction, replace) = merge(actions.getOrNull(index - 1))

        if (replace) {
            if (index == 0)
                throw IllegalStateException("No last action; it can't be replaced")
            index -= 1
        }

        performAndAppend(mergedAction)
    }


    /**
     * Appends the given action to the undo queue without performing it.
     */
    fun append(action: UndoRedoAction<C>) {
        // Clear following actions and append the new one
        actions.removeTailFrom(index)
        actions.add(action)
        index += 1
    }


    /**
     * Undo the last performed action; return whether there was an action to undo.
     */
    suspend fun undo(): Boolean {
        if (index == 0)
            return false

        with(contextProvider()) {
            actions[index-1].revert()
        }
        index -= 1
        return true
    }


    /**
     * Redo the last undone action; return whether there was an action to redo.
     */
    suspend fun redo(): Boolean {
        if (index == actions.size)
            return false

        with(contextProvider()) {
            actions[index].perform()
        }
        index += 1
        return true
    }


    /**
     * Returns whether this queue is empty.
     */
    fun isEmpty() = actions.isEmpty()


    /**
     * Removes all existing actions from the queue, resetting it to its initial state.
     */
    fun clear() {
        actions.clear()
        index = 0
    }


    /**
     * A scope for building a composite [UndoRedoAction] step-by-step.
     */
    interface CompositeUndoRedoScope<C> {

        /**
         * Performs the action and adds it to the list of actions to undo within the scope of a single composite action.
         */
        suspend fun performAndAddStep(action: UndoRedoAction<C>)

    }


    /**
     * Performs and appends an atomic [UndoRedoAction] by performing a series of step actions.
     *
     * The steps will first be performed as they are being added (via [CompositeUndoRedoScope.performAndAddStep]).
     * Afterward, the steps are considered as one atomic [UndoRedoAction] and will be performed and reverted together.
     * When reverting, they will be run in reverse order.
     *
     * The purpose of this function is to allow building an atomic [UndoRedoAction] from a series of steps where each
     * step must be performed before the next step can be built. This can be useful when the next step can only be built
     * once the previous step has been completed.
     */
    suspend fun performAndAppendComposite(block: suspend CompositeUndoRedoScope<C>.() -> Unit) {
        val steps = mutableListOf<UndoRedoAction<C>>()
        val scope = object: CompositeUndoRedoScope<C> {
            override suspend fun performAndAddStep(action: UndoRedoAction<C>) {
                with(contextProvider()) {
                    action.perform()
                }
                steps.add(action)
            }
        }

        scope.block()

        if (steps.isEmpty())
            return

        append(
            object : UndoRedoAction<C> {

                context(C)
                override suspend fun perform() {
                    for (action in steps)
                        action.perform()
                }

                context(C)
                override suspend fun revert() {
                    for (action in steps.reversed())
                        action.revert()
                }

            }
        )
    }


}


/**
 * The interface for an action that can be performed and reverted.
 */
interface UndoRedoAction<in C> {


    /**
     * Performs the action.
     */
    context(C)
    suspend fun perform()


    /**
     * Perform the inverse of the action.
     */
    context(C)
    suspend fun revert()


}


/**
 * An [UndoRedoAction] that doesn't require a context.
 */
abstract class PlainUndoRedoAction: UndoRedoAction<Any?> {

    context(Any?)
    override suspend fun perform() {
        plainPerform()
    }


    abstract suspend fun plainPerform()


    context(Any?)
    override suspend fun revert() {
        plainRevert()
    }

    abstract suspend fun plainRevert()

}



/**
 * Returns an [UndoRedoAction] that performs and reverts the given actions in the same order.
 */
fun <C> undoRedoTogether(
    action1: UndoRedoAction<C>?,
    action2: UndoRedoAction<C>?,
    action3: UndoRedoAction<C>? = null,
): UndoRedoAction<C> = object: UndoRedoAction<C> {

    context(C)
    override suspend fun perform() {
        action1?.perform()
        action2?.perform()
        action3?.perform()
    }

    context(C)
    override suspend fun revert() {
        action1?.revert()
        action2?.revert()
        action3?.revert()
    }

}


/**
 * Returns an [UndoRedoAction] that performs the given list of actions, and undoes them in reverse order.
 */
fun <C> groupUndoRedoActions(actions: List<UndoRedoAction<C>>): UndoRedoAction<C> = object: UndoRedoAction<C> {

    context(C)
    override suspend fun perform() {
        for (action in actions)
            action.perform()
    }

    context(C)
    override suspend fun revert() {
        for (action in actions.asReversed())
            action.revert()
    }

}
