"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.EditManager = exports.minimumPossibleSequenceNumber = void 0;
const sorted_btree_1 = __importDefault(require("sorted-btree"));
const common_utils_1 = require("@fluidframework/common-utils");
const util_1 = require("../util");
const core_1 = require("../core");
exports.minimumPossibleSequenceNumber = (0, util_1.brand)(Number.MIN_SAFE_INTEGER);
const nullRevisionTag = (0, core_1.assertIsRevisionTag)("00000000-0000-4000-8000-000000000000");
/**
 * Represents a local branch of a document and interprets the effect on the document of adding sequenced changes,
 * which were based on a given session's branch, to the document history
 */
// TODO: Try to reduce this to a single type parameter
class EditManager extends core_1.SimpleDependee {
    /**
     * @param localBranchUndoRedoManager - the {@link UndoRedoManager} associated with the local branch.
     * localBranchUndoRedoManager.getHead can not be called within this constructor.
     * @param trunkUndoRedoManager - the {@link UndoRedoManager} associated with the trunk.
     * trunkUndoRedoManager.getHead can not be called within this constructor.
     */
    constructor(changeFamily, 
    // TODO: Change this type to be the Session ID type provided by the IdCompressor when available.
    localSessionId, localBranchUndoRedoManager, trunkUndoRedoManager, anchors) {
        super("EditManager");
        this.changeFamily = changeFamily;
        this.localSessionId = localSessionId;
        this.localBranchUndoRedoManager = localBranchUndoRedoManager;
        this.trunkUndoRedoManager = trunkUndoRedoManager;
        this.anchors = anchors;
        /**
         * Branches are maintained to represent the local change list that the issuing client had
         * at the time of submitting the latest known edit on the branch.
         * This means the head commit of each branch is always in its original (non-rebased) form.
         */
        this.peerLocalBranches = new Map();
        /**
         * A map of local commit revisions to their undo redo manager commit types. This is stored so that
         * the trunk undo redo manager can properly track commits when they become sequenced.
         */
        this.localCommitTypes = new Map();
        /**
         * Tracks where on the trunk all registered branches are based. Each key is the sequence number of a commit on
         * the trunk, and the value is the set of all branches who have that commit as their common ancestor with the trunk.
         */
        this.trunkBranches = new sorted_btree_1.default();
        /**
         * The sequence number of the newest commit on the trunk that has been received by all peers
         */
        this.minimumSequenceNumber = (0, util_1.brand)(-1);
        /**
         * A map from a sequence number to the commit in the trunk which has that sequence number
         */
        this.sequenceMap = new sorted_btree_1.default();
        this.trunkBase = {
            revision: nullRevisionTag,
            sessionId: "",
            change: changeFamily.rebaser.compose([]),
            sequenceNumber: exports.minimumPossibleSequenceNumber,
        };
        this.trunk = this.trunkBase;
        this.localBranch = this.trunk;
    }
    /**
     * Make the given branch known to the `EditManager`. The `EditManager` will ensure that all registered
     * branches remain usable even as the minimum sequence number advances.
     * @param branch - the branch to register. All branches that fork from this branch, directly or transitively,
     * will also be registered.
     */
    registerBranch(branch) {
        const trackBranch = (b) => {
            const trunkCommit = (0, core_1.findCommonAncestor)(this.trunk, b.getHead());
            (0, common_utils_1.assert)(isTrunkCommit(trunkCommit), 0x66f /* Expected commit to be on the trunk branch */);
            const branches = (0, util_1.getOrCreate)(this.trunkBranches, trunkCommit.sequenceNumber, () => new Set());
            (0, common_utils_1.assert)(!branches.has(b), 0x670 /* Branch was registered more than once */);
            branches.add(b);
            return trunkCommit.sequenceNumber;
        };
        const untrackBranch = (b, sequenceNumber) => {
            var _a;
            const branches = (_a = this.trunkBranches.get(sequenceNumber)) !== null && _a !== void 0 ? _a : (0, util_1.fail)("Expected branch to be tracked");
            (0, common_utils_1.assert)(branches.delete(b), 0x671 /* Expected branch to be tracked */);
            if (branches.size === 0) {
                this.trunkBranches.delete(sequenceNumber);
            }
        };
        // Record the sequence number of the branch's base commit on the trunk
        const trunkBase = { sequenceNumber: trackBranch(branch) };
        // Whenever the branch forks, register the new fork
        const offFork = branch.on("fork", (f) => this.registerBranch(f));
        // Whenever the branch is rebased, update our record of its base trunk commit
        const offRebase = branch.on("change", ({ type }) => {
            if (type === "rebase") {
                untrackBranch(branch, trunkBase.sequenceNumber);
                trunkBase.sequenceNumber = trackBranch(branch);
            }
        });
        // When the branch is disposed, update our branch set and trim the trunk
        const offDispose = branch.on("dispose", () => {
            untrackBranch(branch, trunkBase.sequenceNumber);
            this.trimTrunk();
            offFork();
            offRebase();
            offDispose();
        });
    }
    /**
     * Advances the minimum sequence number, and removes all commits from the trunk which lie outside the collaboration window.
     * @param minimumSequenceNumber - the sequence number of the newest commit that all peers (including this one) have received and applied to their trunks
     */
    advanceMinimumSequenceNumber(minimumSequenceNumber) {
        if (minimumSequenceNumber === this.minimumSequenceNumber) {
            return;
        }
        (0, common_utils_1.assert)(minimumSequenceNumber > this.minimumSequenceNumber, 0x476 /* number must be larger or equal to current minimumSequenceNumber. */);
        this.minimumSequenceNumber = minimumSequenceNumber;
        this.trimTrunk();
    }
    /**
     * Examines the latest known minimum sequence number and the trunk bases of any registered branches to determine
     * if any commits on the trunk are unreferenced and unneeded for future computation; those found are evicted from the trunk.
     * @returns the number of commits that were removed from the trunk
     */
    trimTrunk() {
        let deleted = 0;
        /** The sequence number of the oldest commit on the trunk that will be retained */
        let trunkTailSearchKey = this.minimumSequenceNumber;
        // If there are any outstanding registered branches, get the one that is the oldest (has the "most behind" trunk base)
        const minimumBranchBaseSequenceNumber = this.trunkBranches.minKey();
        if (minimumBranchBaseSequenceNumber !== undefined) {
            // If that branch is behind the minimum sequence number, we only want to evict commits older than it,
            // even if those commits are behind the minimum sequence number
            trunkTailSearchKey = (0, util_1.brand)(Math.min(trunkTailSearchKey, minimumBranchBaseSequenceNumber));
        }
        // The new tail of the trunk is the commit at or just past the new minimum trunk sequence number
        const searchResult = this.sequenceMap.getPairOrNextHigher(trunkTailSearchKey);
        if (searchResult !== undefined) {
            const [_, newTrunkTail] = searchResult;
            // Don't do any work if the commit found by the search is already the tail of the trunk
            if (newTrunkTail.parent !== this.trunkBase) {
                // This is dangerous. Commits ought to be immutable, but if they are then changing the trunk tail requires
                // regenerating the entire commit graph. It is, in general, safe to chop off the tail like this if we know
                // that there are no outstanding references to any of the commits being removed. For example, there must be
                // no existing branches that are based off of any of the commits being removed.
                newTrunkTail.parent = this.trunkBase;
                deleted = this.sequenceMap.deleteRange(exports.minimumPossibleSequenceNumber, newTrunkTail.sequenceNumber, false);
                for (const [sessionId, branch] of this.peerLocalBranches) {
                    // If a session branch falls behind the min sequence number, then we know that its session has been abandoned by Fluid
                    // (because otherwise, it would have already been updated) and we expect not to receive any more updates for it.
                    if ((0, core_1.findCommonAncestor)(branch, newTrunkTail) === undefined) {
                        this.peerLocalBranches.delete(sessionId);
                    }
                }
            }
        }
        else {
            // If no trunk commit is found, it means that all trunk commits are below the search key, so evict them all
            (0, common_utils_1.assert)(this.trunkBranches.isEmpty, 0x672 /* Expected no registered branches when clearing trunk */);
            this.trunk = this.trunkBase;
            this.sequenceMap.clear();
            this.peerLocalBranches.clear();
        }
        return deleted;
    }
    isEmpty() {
        return (this.trunk === this.trunkBase &&
            this.peerLocalBranches.size === 0 &&
            this.localBranch === this.trunk &&
            this.minimumSequenceNumber === -1);
    }
    getSummaryData() {
        // The assert below is acceptable at present because summarization only ever occurs on a client with no
        // local/in-flight changes.
        // In the future we may wish to relax this constraint. For that to work, the current implementation of
        // `EditManager` would have to be amended in one of two ways:
        // A) Changes made by the local session should be represented by a branch in `EditManager.branches`.
        // B) The contents of such a branch should be computed on demand based on the trunk.
        // Note that option (A) would be a simple change to `addSequencedChange` whereas (B) would likely require
        // rebasing trunk changes over the inverse of trunk changes.
        (0, common_utils_1.assert)(this.localBranch === this.trunk, 0x428 /* Clients with local changes cannot be used to generate summaries */);
        const trunkPath = getPathFromBase(this.trunk, this.trunkBase);
        (0, common_utils_1.assert)(this.sequenceMap.size === trunkPath.length, 0x572 /* Expected sequence map to be the same size as the trunk */);
        const trunk = Array.from((0, util_1.mapIterable)((0, util_1.zipIterables)(this.sequenceMap.keys(), trunkPath), ([sequenceNumber, commit]) => (Object.assign(Object.assign({}, commit), { sequenceNumber }))));
        const branches = new Map((0, util_1.mapIterable)(this.peerLocalBranches.entries(), ([sessionId, branch]) => {
            var _a;
            const branchPath = [];
            const ancestor = (_a = (0, core_1.findCommonAncestor)([branch, branchPath], this.trunk)) !== null && _a !== void 0 ? _a : (0, util_1.fail)("Expected branch to be based on trunk");
            return [
                sessionId,
                {
                    base: ancestor.revision,
                    commits: branchPath,
                },
            ];
        }));
        return { trunk, branches };
    }
    loadSummaryData(data) {
        var _a;
        this.sequenceMap.clear();
        this.trunk = data.trunk.reduce((base, c) => {
            const commit = mintTrunkCommit(base, c);
            this.trunkUndoRedoManager.repairDataStoreProvider.applyDelta(this.changeFamily.intoDelta(commit.change));
            this.sequenceMap.set(c.sequenceNumber, commit);
            return commit;
        }, this.trunkBase);
        this.localBranch = this.trunk;
        this.peerLocalBranches.clear();
        for (const [sessionId, branch] of data.branches) {
            const commit = (_a = (0, core_1.findAncestor)(this.trunk, (r) => r.revision === branch.base)) !== null && _a !== void 0 ? _a : (0, util_1.fail)("Expected summary branch to be based off of a revision in the trunk");
            this.peerLocalBranches.set(sessionId, branch.commits.reduce(core_1.mintCommit, commit));
        }
    }
    getTrunk() {
        return getPathFromBase(this.trunk, this.trunkBase);
    }
    getLastSequencedChange() {
        var _a;
        return ((_a = this.getTrunkHead()) !== null && _a !== void 0 ? _a : (0, util_1.fail)("No sequenced changes")).change;
    }
    getTrunkHead() {
        return this.trunk;
    }
    /**
     * @returns the head commit of the local branch
     */
    getLocalBranchHead() {
        return this.localBranch;
    }
    getLocalChanges() {
        return getPathFromBase(this.localBranch, this.trunk).map((c) => c.change);
    }
    addSequencedChange(newCommit, sequenceNumber, referenceSequenceNumber) {
        var _a, _b, _c;
        if (newCommit.sessionId === this.localSessionId) {
            // `newCommit` should correspond to the oldest change in `localChanges`, so we move it into trunk.
            // `localChanges` are already rebased to the trunk, so we can use the stored change instead of rebasing the
            // change in the incoming commit.
            const localPath = getPathFromBase(this.localBranch, this.trunk);
            // Get the first revision in the local branch, and then remove it
            const { change } = (_a = localPath.shift()) !== null && _a !== void 0 ? _a : (0, util_1.fail)("Received a sequenced change from the local session despite having no local changes");
            this.pushToTrunk(sequenceNumber, Object.assign(Object.assign({}, newCommit), { change }), true);
            // TODO: Can this be optimized by simply mutating the localPath parent pointers? Is it safe to do that?
            this.localBranch = localPath.reduce(core_1.mintCommit, this.trunk);
            return core_1.emptyDelta;
        }
        // Get the revision that the remote change is based on
        const baseRevisionInTrunk = (_c = (_b = this.sequenceMap.getPairOrNextLower(referenceSequenceNumber)) === null || _b === void 0 ? void 0 : _b[1]) !== null && _c !== void 0 ? _c : this.trunkBase;
        // Rebase that branch over the part of the trunk up to the base revision
        // This will be a no-op if the sending client has not advanced since the last time we received an edit from it
        const [rebasedBranch] = (0, core_1.rebaseBranch)(this.changeFamily.rebaser, (0, util_1.getOrCreate)(this.peerLocalBranches, newCommit.sessionId, () => baseRevisionInTrunk), baseRevisionInTrunk, this.trunk);
        if (rebasedBranch === this.trunk) {
            // If the branch is fully caught up and empty after being rebased, then push to the trunk directly
            this.pushToTrunk(sequenceNumber, newCommit);
            this.peerLocalBranches.set(newCommit.sessionId, this.trunk);
        }
        else {
            const newChangeFullyRebased = (0, core_1.rebaseChange)(this.changeFamily.rebaser, newCommit.change, rebasedBranch, this.trunk);
            this.peerLocalBranches.set(newCommit.sessionId, (0, core_1.mintCommit)(rebasedBranch, newCommit));
            this.pushToTrunk(sequenceNumber, Object.assign(Object.assign({}, newCommit), { change: newChangeFullyRebased }));
        }
        const delta = this.rebaseLocalBranchOverTrunk();
        if (delta === undefined) {
            return core_1.emptyDelta;
        }
        return this.changeFamily.intoDelta(delta);
    }
    addLocalChange(revision, change, rebaseAnchors = true, undoRedoType = core_1.UndoRedoManagerCommitType.Undoable) {
        this.pushToLocalBranch(revision, change);
        this.localCommitTypes.set(revision, undoRedoType);
        if (rebaseAnchors && this.anchors !== undefined) {
            this.changeFamily.rebaser.rebaseAnchors(this.anchors, change);
        }
        return this.changeFamily.intoDelta(change);
    }
    /**
     * Given a revision on the local branch, replace all commits after it with a single commit containing
     * an equivalent composition of changes.
     * @returns the new commit (which is now the head of the local branch)
     */
    squashLocalChanges(startRevision) {
        const [squashStart, commits] = this.findLocalCommit(startRevision);
        // Anonymize the commits from this transaction by stripping their revision tags.
        // Otherwise, the change rebaser will record their tags and those tags no longer exist.
        const anonymousCommits = commits.map(({ change, revision }) => {
            this.localCommitTypes.delete(revision);
            return { change, revision: undefined };
        });
        {
            const change = this.changeFamily.rebaser.compose(anonymousCommits);
            const revision = (0, core_1.mintRevisionTag)();
            this.localBranch = (0, core_1.mintCommit)(squashStart, {
                revision,
                sessionId: this.localSessionId,
                change,
            });
            // A completed transaction is always undoable.
            this.localCommitTypes.set(revision, core_1.UndoRedoManagerCommitType.Undoable);
            return this.localBranch;
        }
    }
    /**
     * Given a revision on the local branch, remove all commits after it, and updates anchors accordingly.
     * @param startRevision - the revision on the local branch that will become the new head
     * @param repairStore - an optional repair data store to assist with generating inverses of the removed commits
     * @returns a delta that describes the change from rolling back all of the removed commits.
     */
    rollbackLocalChanges(startRevision, repairStore) {
        const [rollbackTo, commits] = this.findLocalCommit(startRevision);
        this.localBranch = rollbackTo;
        const inverses = [];
        for (let i = commits.length - 1; i >= 0; i--) {
            const { change, revision } = commits[i];
            const inverse = this.changeFamily.rebaser.invert((0, core_1.tagChange)(change, revision), false, repairStore);
            // We assign a revision tag to the inverse changesets so that the compose code can lookup the relative order
            // of individual changesets. These revisions don't actually make it to the document history.
            inverses.push((0, core_1.tagChange)(inverse, (0, core_1.mintRevisionTag)()));
        }
        const composedInverse = this.changeFamily.rebaser.compose(inverses);
        if (this.anchors !== undefined) {
            this.changeFamily.rebaser.rebaseAnchors(this.anchors, composedInverse);
        }
        return this.changeFamily.intoDelta(composedInverse);
    }
    findLocalCommit(revision) {
        const commits = [];
        const commit = (0, core_1.findAncestor)([this.localBranch, commits], (c) => c.revision === revision);
        (0, common_utils_1.assert)(commit !== undefined, 0x599 /* Expected local branch to contain revision */);
        return [commit, commits];
    }
    pushToTrunk(sequenceNumber, commit, local = false) {
        var _a;
        this.trunk = mintTrunkCommit(this.trunk, commit, sequenceNumber);
        if (local) {
            const type = (_a = this.localCommitTypes.get(this.trunk.revision)) !== null && _a !== void 0 ? _a : (0, util_1.fail)("Local commit types must be tracked until they are sequenced.");
            this.localCommitTypes.delete(this.trunk.revision);
            this.trunkUndoRedoManager.trackCommit(this.trunk, type);
        }
        this.trunkUndoRedoManager.repairDataStoreProvider.applyDelta(this.changeFamily.intoDelta(commit.change));
        this.sequenceMap.set(sequenceNumber, this.trunk);
    }
    pushToLocalBranch(revision, change) {
        this.localBranch = (0, core_1.mintCommit)(this.localBranch, {
            revision,
            sessionId: this.localSessionId,
            change,
        });
    }
    rebaseLocalBranchOverTrunk() {
        const [newLocalChanges, netChange] = (0, core_1.rebaseBranch)(this.changeFamily.rebaser, this.localBranch, this.trunk);
        this.localBranchUndoRedoManager.updateAfterRebase(newLocalChanges, this.trunkUndoRedoManager);
        this.localBranch = newLocalChanges;
        if (this.anchors !== undefined && netChange !== undefined) {
            this.changeFamily.rebaser.rebaseAnchors(this.anchors, netChange);
        }
        return netChange;
    }
}
exports.EditManager = EditManager;
/**
 * @returns the path from the base of a branch to its head
 */
function getPathFromBase(branchHead, baseBranchHead) {
    const path = [];
    (0, common_utils_1.assert)((0, core_1.findCommonAncestor)([branchHead, path], baseBranchHead) !== undefined, 0x573 /* Expected branches to be related */);
    return path;
}
function isTrunkCommit(commit) {
    return commit !== undefined && commit.sequenceNumber !== undefined;
}
function mintTrunkCommit(parent, commit, explicitSequenceNumber) {
    const sequenceNumber = explicitSequenceNumber !== null && explicitSequenceNumber !== void 0 ? explicitSequenceNumber : commit.sequenceNumber;
    return {
        revision: commit.revision,
        sessionId: commit.sessionId,
        change: commit.change,
        sequenceNumber,
        parent,
    };
}
//# sourceMappingURL=editManager.js.map