"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.SharedTreeBranch = exports.isTransactionCommitChange = exports.isRebaseChange = void 0;
const common_utils_1 = require("@fluidframework/common-utils");
const core_1 = require("../core");
const events_1 = require("../events");
const transactionStack_1 = require("./transactionStack");
/**
 * Returns true iff the given {@link SharedTreeBranchChange} is the result of a rebase operation.
 */
function isRebaseChange(change) {
    return !isTransactionCommitChange(change);
}
exports.isRebaseChange = isRebaseChange;
/**
 * Returns true iff the given {@link SharedTreeBranchChange} is the result of a transaction being committed.
 */
function isTransactionCommitChange(change) {
    // "replace" is emitted by transaction commits and by rebase operations. A commit operation will
    // always result in exactly one new commit. When a rebase results in exactly one new commit, we know
    // that there will also be no removed commits:
    //
    // A ─ B (branch X)
    // └─ (branch Y)
    //
    // Rebasing branch Y onto branch X results in:
    //
    // A ─ B (branch X)
    //     └─ (branch Y)
    //
    // newCommits: [B]
    // removedCommits: []
    return change.newCommits.length === 1 && change.removedCommits.length !== 0;
}
exports.isTransactionCommitChange = isTransactionCommitChange;
/**
 * A branch of changes that can be applied to a SharedTree.
 */
class SharedTreeBranch extends events_1.EventEmitter {
    /**
     * Construct a new branch.
     * @param head - the head of the branch
     * @param rebaser - the rebaser used for rebasing and merging commits across branches
     * @param changeFamily - determines the set of changes that this branch can commit
     * @param undoRedoManager - an optional {@link UndoRedoManager} to manage the undo/redo operations of this
     * branch. This must be provided in order to use the `undo` and `redo` methods of this branch.
     * @param anchors - an optional set of anchors that this branch will rebase whenever the branch head changes
     */
    constructor(head, changeFamily, undoRedoManager, anchors) {
        super();
        this.head = head;
        this.changeFamily = changeFamily;
        this.undoRedoManager = undoRedoManager;
        this.anchors = anchors;
        this.transactions = new transactionStack_1.TransactionStack();
        this.disposed = false;
        this.editor = this.changeFamily.buildEditor((change) => this.apply(change, (0, core_1.mintRevisionTag)()), new core_1.AnchorSet());
    }
    /**
     * Sets the head of this branch. Emits no change events.
     */
    setHead(head) {
        this.assertNotDisposed();
        (0, common_utils_1.assert)(!this.isTransacting(), 0x685 /* Cannot set head during a transaction */);
        this.head = head;
    }
    /**
     * Apply a change to this branch.
     * @param change - the change to apply
     * @param revision - the revision of the new head commit of the branch that contains `change`
     * @returns the change that was applied and the new head commit of the branch
     */
    apply(change, revision) {
        return this.applyChange(change, revision, core_1.UndoRedoManagerCommitType.Undoable);
    }
    applyChange(change, revision, undoRedoType) {
        var _a, _b;
        this.assertNotDisposed();
        this.head = (0, core_1.mintCommit)(this.head, {
            revision,
            change,
        });
        const delta = this.changeFamily.intoDelta(change);
        (_a = this.transactions.repairStore) === null || _a === void 0 ? void 0 : _a.capture(delta, this.head.revision);
        // If this is not part of a transaction, add it to the undo commit tree
        if (undoRedoType !== undefined && !this.isTransacting()) {
            (_b = this.undoRedoManager) === null || _b === void 0 ? void 0 : _b.trackCommit(this.head, undoRedoType);
        }
        this.emitAndRebaseAnchors({ type: "append", change, newCommits: [this.head] });
        return [change, this.head];
    }
    /**
     * @returns the commit at the head of this branch.
     */
    getHead() {
        return this.head;
    }
    /**
     * Begin a transaction on this branch. If the transaction is committed via {@link commitTransaction},
     * all commits made since this call will be squashed into a single head commit.
     * @param repairStore - the repair store associated with this transaction
     */
    startTransaction(repairStore) {
        var _a;
        this.assertNotDisposed();
        if (!this.isTransacting()) {
            // If this is the start of a transaction stack, freeze the undo redo manager's
            // repair data store provider so that repair data can be captured based on the
            // state of the branch at the start of the transaction.
            (_a = this.undoRedoManager) === null || _a === void 0 ? void 0 : _a.repairDataStoreProvider.freeze();
        }
        this.transactions.push(this.head.revision, repairStore);
        this.editor.enterTransaction();
    }
    /**
     * Commit the current transaction. There must be a transaction in progress that was begun via
     * {@link startTransaction}. All commits in the current transaction will be squashed into a new,
     * single head commit.
     * @returns the commits that were squashed, and the new squash commit
     */
    commitTransaction() {
        var _a, _b;
        this.assertNotDisposed();
        const [startCommit, commits] = this.popTransaction();
        this.editor.exitTransaction();
        if (commits.length === 0) {
            return undefined;
        }
        // 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 }) => ({ change, revision: undefined }));
        {
            // Squash the changes and make the squash commit the new head of this branch
            const change = this.changeFamily.rebaser.compose(anonymousCommits);
            this.head = (0, core_1.mintCommit)(startCommit, {
                revision: (0, core_1.mintRevisionTag)(),
                change,
            });
            // If this transaction is not nested, add it to the undo commit tree
            if (!this.isTransacting()) {
                (_a = this.undoRedoManager) === null || _a === void 0 ? void 0 : _a.trackCommit(this.head, core_1.UndoRedoManagerCommitType.Undoable);
            }
            // If there is still an ongoing transaction (because this transaction was nested inside of an outer transaction)
            // then update the repair data store for that transaction
            (_b = this.transactions.repairStore) === null || _b === void 0 ? void 0 : _b.capture(this.changeFamily.intoDelta(change), this.head.revision);
        }
        this.emitAndRebaseAnchors({
            type: "replace",
            change: undefined,
            removedCommits: commits,
            newCommits: [this.head],
        });
        return [commits, this.head];
    }
    /**
     * Cancel the current transaction. There must be a transaction in progress that was begun via
     * {@link startTransaction}. All commits made during the transaction will be removed.
     * @returns the change to this branch resulting in the removal of the commits, and a list of the
     * commits that were removed.
     */
    abortTransaction() {
        this.assertNotDisposed();
        const [startCommit, commits, repairStore] = this.popTransaction();
        this.editor.exitTransaction();
        this.head = startCommit;
        if (commits.length === 0) {
            return [undefined, []];
        }
        const inverses = [];
        for (let i = commits.length - 1; i >= 0; i--) {
            const inverse = this.changeFamily.rebaser.invert(commits[i], false, repairStore);
            inverses.push((0, core_1.tagChange)(inverse, (0, core_1.mintRevisionTag)()));
        }
        const change = inverses.length > 0 ? this.changeFamily.rebaser.compose(inverses) : undefined;
        this.emitAndRebaseAnchors({
            type: "remove",
            change,
            removedCommits: commits,
        });
        return [change, commits];
    }
    /**
     * True iff this branch is in the middle of a transaction that was begin via {@link startTransaction}
     */
    isTransacting() {
        return this.transactions.size !== 0;
    }
    popTransaction() {
        const { startRevision, repairStore } = this.transactions.pop();
        const commits = [];
        const startCommit = (0, core_1.findAncestor)([this.head, commits], (c) => c.revision === startRevision);
        (0, common_utils_1.assert)(startCommit !== undefined, 0x593 /* Expected branch to be ahead of transaction start revision */);
        return [startCommit, commits, repairStore];
    }
    /**
     * Undoes the last change made by the client. If there is no change to undo then this method has no effect.
     * This method will error if no {@link UndoRedoManager} was provided when this branch was constructed.
     * It is invalid to call this method while a transaction is open (this will be supported in the future).
     * @returns the change to this branch and the new head commit, or undefined if there was nothing to undo
     */
    undo() {
        var _a;
        (0, common_utils_1.assert)(this.undoRedoManager !== undefined, 0x686 /* Must construct branch with an `UndoRedoManager` in order to undo. */);
        // TODO: allow this once it becomes possible to compose the changesets created by edits made
        // within transactions and edits that represent completed transactions.
        (0, common_utils_1.assert)(!this.isTransacting(), 0x66a /* Undo is not yet supported during transactions */);
        const undoChange = (_a = this.undoRedoManager) === null || _a === void 0 ? void 0 : _a.undo(this.getHead());
        if (undoChange !== undefined) {
            return this.applyChange(undoChange, (0, core_1.mintRevisionTag)(), core_1.UndoRedoManagerCommitType.Undo);
        }
        return undefined;
    }
    /**
     * Redoes the last change made by the client. If there is no change to redo then this method has no effect.
     * This method will error if no {@link UndoRedoManager} was provided when this branch was constructed.
     * It is invalid to call this method while a transaction is open (this will be supported in the future).
     * @returns the change to this branch and the new head commit, or undefined if there was nothing to redo
     */
    redo() {
        var _a;
        (0, common_utils_1.assert)(this.undoRedoManager !== undefined, 0x687 /* Must construct branch with an `UndoRedoManager` in order to redo. */);
        // TODO: allow this once it becomes possible to compose the changesets created by edits made
        // within transactions and edits that represent completed transactions.
        (0, common_utils_1.assert)(!this.isTransacting(), 0x67e /* Redo is not yet supported during transactions */);
        const redoChange = (_a = this.undoRedoManager) === null || _a === void 0 ? void 0 : _a.redo(this.getHead());
        if (redoChange !== undefined) {
            return this.applyChange(redoChange, (0, core_1.mintRevisionTag)(), core_1.UndoRedoManagerCommitType.Redo);
        }
        return undefined;
    }
    /**
     * Spawn a new branch that is based off of the current state of this branch.
     * Changes made to the new branch will not be applied to this branch until the new branch is merged back in.
     * @param anchors - an optional set of anchors that the new branch is responsible for rebasing
     */
    fork(repairDataStoreProvider, anchors) {
        var _a;
        this.assertNotDisposed();
        const fork = new SharedTreeBranch(this.head, this.changeFamily, (_a = this.undoRedoManager) === null || _a === void 0 ? void 0 : _a.clone(repairDataStoreProvider), anchors);
        this.emit("fork", fork);
        return fork;
    }
    /**
     * Rebase the changes that have been applied to this branch over all the divergent changes in the given branch.
     * After this operation completes, this branch will be based off of `branch`.
     * @param branch - the branch to rebase onto
     * @returns the net change to this branch and the commits that were removed and added to this branch by the rebase,
     * or undefined if nothing changed
     */
    rebaseOnto(branch) {
        this.assertNotDisposed();
        // Rebase this branch onto the given branch
        const rebaseResult = this.rebaseBranch(this.head, branch.getHead());
        if (rebaseResult === undefined) {
            return undefined;
        }
        // The net change to this branch is provided by the `rebaseBranch` API
        const [newHead, change, { deletedSourceCommits, targetCommits, sourceCommits }] = rebaseResult;
        if (this.undoRedoManager !== undefined) {
            // TODO: We probably can rebase a revertible branch onto a non-revertible branch.
            (0, common_utils_1.assert)(branch.undoRedoManager !== undefined, 0x688 /* Cannot rebase a revertible branch onto a non-revertible branch */);
            this.undoRedoManager.updateAfterRebase(sourceCommits, branch.undoRedoManager);
        }
        this.head = newHead;
        const newCommits = targetCommits.concat(sourceCommits);
        this.emitAndRebaseAnchors({
            type: "replace",
            change,
            removedCommits: deletedSourceCommits,
            newCommits,
        });
        return [change, deletedSourceCommits, newCommits];
    }
    /**
     * Apply all the divergent changes on the given branch to this branch.
     * @returns the net change to this branch and the commits that were added to this branch by the merge,
     * or undefined if nothing changed
     */
    merge(branch) {
        this.assertNotDisposed();
        (0, common_utils_1.assert)(!branch.isTransacting(), 0x597 /* Branch may not be merged while transaction is in progress */);
        // Rebase the given branch onto this branch
        const rebaseResult = this.rebaseBranch(branch.head, this.head);
        if (rebaseResult === undefined) {
            return undefined;
        }
        // Compute the net change to this branch
        const [newHead, _, { sourceCommits }] = rebaseResult;
        if (this.undoRedoManager !== undefined) {
            // TODO: We probably can merge a non-revertible branch into a revertible branch.
            (0, common_utils_1.assert)(branch.undoRedoManager !== undefined, 0x689 /* Cannot merge a non-revertible branch into a revertible branch */);
            this.undoRedoManager.updateAfterMerge(sourceCommits, branch.undoRedoManager);
        }
        this.head = newHead;
        const change = this.changeFamily.rebaser.compose(sourceCommits);
        this.emitAndRebaseAnchors({
            type: "append",
            change,
            newCommits: sourceCommits,
        });
        return [change, sourceCommits];
    }
    /** Rebase `branchHead` onto `onto`, but return undefined if nothing changed */
    rebaseBranch(branchHead, onto) {
        if (branchHead === onto) {
            return undefined;
        }
        const rebaseResult = (0, core_1.rebaseBranch)(this.changeFamily.rebaser, branchHead, onto);
        const [rebasedHead] = rebaseResult;
        if (this.head === rebasedHead) {
            return undefined;
        }
        return rebaseResult;
    }
    /**
     * Dispose this branch, freezing its state.
     * Attempts to further mutate or dispose the branch will error.
     */
    dispose() {
        this.assertNotDisposed();
        this.disposed = true;
        this.emit("dispose");
    }
    emitAndRebaseAnchors(change) {
        if (this.anchors !== undefined && change.change !== undefined) {
            this.changeFamily.rebaser.rebaseAnchors(this.anchors, change.change);
        }
        this.emit("change", change);
    }
    assertNotDisposed() {
        (0, common_utils_1.assert)(!this.disposed, 0x66e /* Branch is disposed */);
    }
}
exports.SharedTreeBranch = SharedTreeBranch;
//# sourceMappingURL=branch.js.map