"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");
const events_1 = require("../events");
const branch_1 = require("./branch");
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 changeFamily - the change family of changes on the trunk and local branch
     * @param localSessionId - the id of the local session that will be used for local commits
     * @param repairDataStoreProvider - used for undoing/redoing the local branch
     * @param anchors - an optional set of anchors to be rebased by the local branch when it changes
     */
    constructor(changeFamily, 
    // TODO: Change this type to be the Session ID type provided by the IdCompressor when available.
    localSessionId, repairDataStoreProvider, anchors) {
        super("EditManager");
        this.changeFamily = changeFamily;
        this.localSessionId = localSessionId;
        /** Records extra data associated with trunk commits */
        this.trunkMetadata = new Map();
        /** A map from a sequence number to the commit in the trunk which has that sequence number */
        this.sequenceMap = new sorted_btree_1.default();
        /**
         * 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();
        /**
         * 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);
        this.events = (0, events_1.createEmitter)();
        this.trunkBase = {
            revision: nullRevisionTag,
            change: changeFamily.rebaser.compose([]),
        };
        this.localBranchUndoRedoManager = core_1.UndoRedoManager.create(repairDataStoreProvider, changeFamily);
        this.trunkUndoRedoManager = this.localBranchUndoRedoManager.clone();
        this.trunk = new branch_1.SharedTreeBranch(this.trunkBase, changeFamily, this.trunkUndoRedoManager);
        this.localBranch = new branch_1.SharedTreeBranch(this.trunk.getHead(), changeFamily, this.localBranchUndoRedoManager, anchors);
        // This registers each fork of the local branch, rather than registering the local branch directly.
        // Registering the local branch _almost_ works the same, except that it means the trunk retains an
        // additional commit - because the local branch is always rebased off of the trunk's head, we can
        // never evict the head. That sounds like a good thing, but because the trunk has a privileged
        // relationship with the local branch (the local branch doesn't undergo normal rebasing), we can
        // actually safely evict that last commit (assuming there are no other outstanding branches).
        // TODO: Fiddle with the local case of addSequencedChange some more, and see if you can make it actually do a rebase.
        this.localBranch.on("fork", (fork) => this.registerBranch(fork));
    }
    on(eventName, listener) {
        return this.events.on(eventName, listener);
    }
    /**
     * 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) => {
            var _a, _b, _c;
            const trunkCommit = (_a = (0, core_1.findCommonAncestor)(this.trunk.getHead(), b.getHead())) !== null && _a !== void 0 ? _a : (0, util_1.fail)("Expected branch to be related to trunk");
            const sequenceNumber = (_c = (_b = this.trunkMetadata.get(trunkCommit.revision)) === null || _b === void 0 ? void 0 : _b.sequenceNumber) !== null && _c !== void 0 ? _c : exports.minimumPossibleSequenceNumber;
            const branches = (0, util_1.getOrCreate)(this.trunkBranches, sequenceNumber, () => new Set());
            (0, common_utils_1.assert)(!branches.has(b), 0x670 /* Branch was registered more than once */);
            branches.add(b);
            return 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", (args) => {
            if (args.type === "replace" && (0, branch_1.isRebaseChange)(args)) {
                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() {
        var _a, _b;
        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;
                const sequenceNumber = (_b = (_a = this.trunkMetadata.get(newTrunkTail.revision)) === null || _a === void 0 ? void 0 : _a.sequenceNumber) !== null && _b !== void 0 ? _b : exports.minimumPossibleSequenceNumber;
                this.sequenceMap.forRange(exports.minimumPossibleSequenceNumber, sequenceNumber, false, (_seq, { revision }) => {
                    this.trunkMetadata.delete(revision);
                    this.localBranchUndoRedoManager.untrackCommitType(revision);
                });
                deleted = this.sequenceMap.deleteRange(exports.minimumPossibleSequenceNumber, 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.setHead(this.trunkBase);
            this.sequenceMap.clear();
            this.peerLocalBranches.clear();
        }
        return deleted;
    }
    isEmpty() {
        return (this.trunk.getHead() === this.trunkBase &&
            this.peerLocalBranches.size === 0 &&
            this.localBranch.getHead() === this.trunk.getHead() &&
            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.getHead() === this.trunk.getHead(), 0x428 /* Clients with local changes cannot be used to generate summaries */);
        const trunk = getPathFromBase(this.trunk.getHead(), this.trunkBase).map((c) => {
            var _a;
            return (Object.assign(Object.assign({}, c), ((_a = this.trunkMetadata.get(c.revision)) !== null && _a !== void 0 ? _a : (0, util_1.fail)("Expected metadata for trunk commit"))));
        });
        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.getHead())) !== null && _a !== void 0 ? _a : (0, util_1.fail)("Expected branch to be based on trunk");
            return [
                sessionId,
                {
                    base: ancestor.revision,
                    commits: branchPath.map((c) => (Object.assign(Object.assign({}, c), { sessionId }))),
                },
            ];
        }));
        return { trunk, branches };
    }
    loadSummaryData(data) {
        var _a;
        (0, common_utils_1.assert)(this.isEmpty(), 0x68a /* Attempted to load from summary after edit manager was already mutated */);
        this.sequenceMap.clear();
        this.trunk.setHead(data.trunk.reduce((base, c) => {
            const commit = (0, core_1.mintCommit)(base, c);
            this.trunkUndoRedoManager.repairDataStoreProvider.applyDelta(this.changeFamily.intoDelta(commit.change));
            this.sequenceMap.set(c.sequenceNumber, commit);
            this.trunkMetadata.set(c.revision, {
                sequenceNumber: c.sequenceNumber,
                sessionId: c.sessionId,
            });
            return commit;
        }, this.trunkBase));
        this.localBranch.setHead(this.trunk.getHead());
        for (const [sessionId, branch] of data.branches) {
            const commit = (_a = (0, core_1.findAncestor)(this.trunk.getHead(), (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));
        }
    }
    getTrunkChanges() {
        return getPathFromBase(this.trunk.getHead(), this.trunkBase).map((c) => c.change);
    }
    getTrunkHead() {
        return this.trunk.getHead();
    }
    getLocalChanges() {
        return getPathFromBase(this.localBranch.getHead(), this.trunk.getHead()).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.getHead(), this.trunk.getHead());
            // 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: Replace the line below with this line when UndoRedoManager is better optimized.
                // this.localBranch.rebaseOnto(this.trunk, this.trunkUndoRedoManager);
                this.localBranch.setHead(localPath.reduce(core_1.mintCommit, this.trunk.getHead()));
            }
            return;
        }
        // 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.getHead());
        if (rebasedBranch === this.trunk.getHead()) {
            // 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.getHead());
        }
        else {
            const newChangeFullyRebased = (0, core_1.rebaseChange)(this.changeFamily.rebaser, newCommit.change, rebasedBranch, this.trunk.getHead());
            this.peerLocalBranches.set(newCommit.sessionId, (0, core_1.mintCommit)(rebasedBranch, newCommit));
            this.pushToTrunk(sequenceNumber, Object.assign(Object.assign({}, newCommit), { change: newChangeFullyRebased }));
        }
        this.localBranch.rebaseOnto(this.trunk);
    }
    findLocalCommit(revision) {
        const commits = [];
        const commit = (0, core_1.findAncestor)([this.localBranch.getHead(), 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.setHead((0, core_1.mintCommit)(this.trunk.getHead(), commit));
        const trunkHead = this.trunk.getHead();
        if (local) {
            const type = (_a = this.localBranchUndoRedoManager.getCommitType(trunkHead.revision)) !== null && _a !== void 0 ? _a : (0, util_1.fail)("Local commit types must be tracked until they are sequenced.");
            this.trunkUndoRedoManager.trackCommit(trunkHead, type);
        }
        this.trunkUndoRedoManager.repairDataStoreProvider.applyDelta(this.changeFamily.intoDelta(commit.change));
        this.sequenceMap.set(sequenceNumber, trunkHead);
        this.trunkMetadata.set(trunkHead.revision, { sequenceNumber, sessionId: commit.sessionId });
        this.events.emit("newTrunkHead", trunkHead);
    }
}
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;
}
//# sourceMappingURL=editManager.js.map