"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.markCommits = exports.UndoRedoManagerCommitType = exports.UndoRedoManager = void 0;
const common_utils_1 = require("@fluidframework/common-utils");
const rebase_1 = require("../rebase");
/**
 * Manages a branch of the undoable/redoable commit trees and repair data associated with the undoable and redoable commits.
 */
class UndoRedoManager {
    /**
     * @param repairDataStoryFactory - Factory for creating {@link RepairDataStore}s to create and store repair
     * data for {@link ReversibleCommit}s.
     * @param changeFamily - {@link ChangeFamily} used for inverting changes.
     * @param getHead - Function for retrieving the head commit of the branch associated with this undo redo manager.
     * @param headUndoableCommit - Optional commit to set as the initial undoable commit.
     * @param headRedoableCommit - Optional commit to set as the initial redoable commit.
     */
    constructor(repairDataStoreProvider, changeFamily, getHead, headUndoableCommit, headRedoableCommit) {
        this.repairDataStoreProvider = repairDataStoreProvider;
        this.changeFamily = changeFamily;
        this.getHead = getHead;
        this.headUndoableCommit = headUndoableCommit;
        this.headRedoableCommit = headRedoableCommit;
    }
    get headUndoable() {
        return this.headUndoableCommit;
    }
    get headRedoable() {
        return this.headRedoableCommit;
    }
    /**
     * Adds the provided commit to the undo or redo commit tree, depending on the type of commit it is.
     * Should be called for all commits on the relevant branch, including undo and redo commits.
     */
    trackCommit(commit, type) {
        var _a, _b;
        const repairData = this.repairDataStoreProvider.createRepairData();
        repairData.capture(this.changeFamily.intoDelta(commit.change), commit.revision);
        const parent = type === UndoRedoManagerCommitType.Undo || type === UndoRedoManagerCommitType.Redoable
            ? this.headRedoableCommit
            : this.headUndoableCommit;
        const undoableOrRedoable = {
            commit,
            parent,
            repairData,
        };
        switch (type) {
            // Both undo commits and redoable commits result in a new head redoable commit
            // being pushed to the redoable commit stack but only undo commits need to pop from the
            // undoable commit stack.
            case UndoRedoManagerCommitType.Undo:
                this.headUndoableCommit = (_a = this.headUndoableCommit) === null || _a === void 0 ? void 0 : _a.parent;
            case UndoRedoManagerCommitType.Redoable:
                this.headRedoableCommit = undoableOrRedoable;
                break;
            // Both redo commits and undoable commits result in a new head undoable commit
            // being pushed to the undoable commit stack but only redo commits need to pop from the
            // redoable commit stack.
            case UndoRedoManagerCommitType.Redo:
                this.headRedoableCommit = (_b = this.headRedoableCommit) === null || _b === void 0 ? void 0 : _b.parent;
            case UndoRedoManagerCommitType.Undoable:
                this.headUndoableCommit = undoableOrRedoable;
                break;
            default:
                (0, common_utils_1.unreachableCase)(type);
        }
    }
    /**
     * Inverts the head undoable commit and returns the inverted change.
     * This change can then be applied and tracked.
     */
    undo() {
        const undoableCommit = this.headUndoableCommit;
        if (undoableCommit === undefined) {
            // No undoable commits, exit early
            return undefined;
        }
        return this.createInvertedChange(undoableCommit);
    }
    /**
     * Inverts the head redoable commit and returns the inverted change.
     * This change can then be applied and tracked.
     */
    redo() {
        const redoableCommit = this.headRedoableCommit;
        if (redoableCommit === undefined) {
            // No undoable commits, exit early
            return undefined;
        }
        return this.createInvertedChange(redoableCommit);
    }
    createInvertedChange(undoableOrRedoable) {
        const { commit, repairData } = undoableOrRedoable;
        let change = this.changeFamily.rebaser.invert((0, rebase_1.tagChange)(commit.change, commit.revision), false, repairData);
        if (this.getHead !== undefined) {
            // Rebase the inverted change onto any commits that occurred after the undoable commits.
            const head = this.getHead();
            if (commit.revision !== head.revision) {
                const pathAfterUndoable = [];
                const ancestor = (0, rebase_1.findCommonAncestor)([commit], [head, pathAfterUndoable]);
                (0, common_utils_1.assert)(ancestor === commit, 0x677 /* The head commit should be based off the undoable commit. */);
                change = pathAfterUndoable.reduce((a, b) => this.changeFamily.rebaser.rebase(a, b), change);
            }
        }
        return change;
    }
    /**
     * Creates a copy of this `UndoRedoManager`.
     * @param getHead - Optional function to use for retrieving the head commit for the branch associated with the
     * the new {@link UndoRedoManager}. If one is not provided, the one from this {@link UndoRedoManager} will be used.
     * @param repairDataStoreProvider - Optional {@link IRepairDataStoreProvider} to use for the new {@link UndoRedoManager}.
     * If one is not provided, the `repairDataStoreProvider` of this {@link UndoRedoManager} will be cloned.
     * @param headUndoableCommit - Optional head undoable commit, if one is not provided the head undoable commit
     * of this {@link UndoRedoManager} will be used.
     */
    clone(getHead, repairDataStoreProvider, headUndoableCommit, headRedoableCommit) {
        return new UndoRedoManager(repairDataStoreProvider !== null && repairDataStoreProvider !== void 0 ? repairDataStoreProvider : this.repairDataStoreProvider.clone(), this.changeFamily, getHead !== null && getHead !== void 0 ? getHead : this.getHead, headUndoableCommit !== null && headUndoableCommit !== void 0 ? headUndoableCommit : this.headUndoableCommit, headRedoableCommit !== null && headRedoableCommit !== void 0 ? headRedoableCommit : this.headRedoableCommit);
    }
    /**
     * Updates the state of this {@link UndoRedoManager} to correctly reference commits that have been rebased after merging.
     * @param newHead - the head commit of the newly rebased branch.
     * @param mergedUndoRedoManager - the {@link UndoRedoManager} of the branch that was merged.
     */
    updateAfterMerge(newHead, mergedUndoRedoManager) {
        if (this.getHead !== undefined) {
            this.updateBasedOnNewCommits(this.getHead(), newHead, this, mergedUndoRedoManager);
        }
    }
    /**
     * Updates the state of this {@link UndoRedoManager} to correctly reference commits that have been rebased.
     * @param newHead - the head commit of the newly rebased branch.
     * @param baseUndoRedoManager - the {@link UndoRedoManager} of the branch that was rebased onto
     */
    updateAfterRebase(newHead, baseUndoRedoManager) {
        if (baseUndoRedoManager.getHead !== undefined) {
            const baseHead = baseUndoRedoManager.getHead();
            this.updateBasedOnNewCommits(baseHead, newHead, baseUndoRedoManager, this);
        }
    }
    updateBasedOnNewCommits(baseHead, rebasedHead, baseUndoRedoManager, originalUndoRedoManager) {
        if (originalUndoRedoManager.headUndoable === undefined &&
            originalUndoRedoManager.headRedoable === undefined) {
            // The branch that was rebased had no undoable or redoable edits so the new undo redo manager
            // should be a copy of the undo redo manager from the base branch.
            this.headUndoableCommit = baseUndoRedoManager.headUndoable;
            this.headRedoableCommit = baseUndoRedoManager.headRedoable;
            return;
        }
        const rebasedPath = [];
        const ancestor = (0, rebase_1.findCommonAncestor)([baseHead], [rebasedHead, rebasedPath]);
        (0, common_utils_1.assert)(ancestor === baseHead, 0x678 /* The rebased head should be based off of the base branch. */);
        if (rebasedPath.length === 0) {
            this.headUndoableCommit = baseUndoRedoManager.headUndoable;
            this.headRedoableCommit = baseUndoRedoManager.headRedoable;
            return;
        }
        const markedCommits = markCommits(rebasedPath, originalUndoRedoManager.headUndoable, originalUndoRedoManager.headRedoable);
        // Create a complete clone of the base undo redo manager for tracking the rebased path
        const undoRedoManager = baseUndoRedoManager.clone();
        for (const { commit, undoRedoManagerCommitType } of markedCommits) {
            if (undoRedoManagerCommitType !== undefined) {
                undoRedoManager.trackCommit(commit, undoRedoManagerCommitType);
            }
            undoRedoManager.repairDataStoreProvider.applyDelta(this.changeFamily.intoDelta(commit.change));
        }
        this.headUndoableCommit = undoRedoManager.headUndoable;
        this.headRedoableCommit = undoRedoManager.headRedoable;
    }
}
exports.UndoRedoManager = UndoRedoManager;
/**
 * The type of a commit in the context of undo/redo manager.
 */
var UndoRedoManagerCommitType;
(function (UndoRedoManagerCommitType) {
    UndoRedoManagerCommitType[UndoRedoManagerCommitType["Undoable"] = 0] = "Undoable";
    UndoRedoManagerCommitType[UndoRedoManagerCommitType["Redoable"] = 1] = "Redoable";
    UndoRedoManagerCommitType[UndoRedoManagerCommitType["Undo"] = 2] = "Undo";
    UndoRedoManagerCommitType[UndoRedoManagerCommitType["Redo"] = 3] = "Redo";
})(UndoRedoManagerCommitType = exports.UndoRedoManagerCommitType || (exports.UndoRedoManagerCommitType = {}));
/**
 * Marks the commits in the provided path as undoable or redoable.
 * @param path - the path of commits that may or may not be undoable or redoable.
 * @param headUndoableCommit - the head undoable commit of the undo commit tree that may contain the commits in the path.
 * @param headRedoableCommit - the head redoable commit of the redo commit tree that may contain the commits in the path.
 */
function markCommits(path, headUndoableCommit, headRedoableCommit) {
    let currentUndoable = headUndoableCommit;
    let currentRedoable = headRedoableCommit;
    if (currentUndoable === undefined && currentRedoable === undefined) {
        // If there are no undoable or redoable commits, none are marked
        return path.map((commit) => ({ commit }));
    }
    // Walk up the commit tree to figure out which commits are undoable or redoable
    return path
        .reverse()
        .map((commit) => {
        const markedCommit = { commit };
        if (commit.revision === (currentUndoable === null || currentUndoable === void 0 ? void 0 : currentUndoable.commit.revision)) {
            markedCommit.undoRedoManagerCommitType = UndoRedoManagerCommitType.Undoable;
            currentUndoable = currentUndoable === null || currentUndoable === void 0 ? void 0 : currentUndoable.parent;
        }
        else if (commit.revision === (currentRedoable === null || currentRedoable === void 0 ? void 0 : currentRedoable.commit.revision)) {
            markedCommit.undoRedoManagerCommitType = UndoRedoManagerCommitType.Redoable;
            currentRedoable = currentRedoable === null || currentRedoable === void 0 ? void 0 : currentRedoable.parent;
        }
        return markedCommit;
    })
        .reverse();
}
exports.markCommits = markCommits;
//# sourceMappingURL=undoRedoManager.js.map