/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
    if (kind === "m") throw new TypeError("Private method is not writable");
    if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
    if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
    return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
};
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
    if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
    if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
    return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
};
var _TreeCheckout_events, _TreeCheckout_transaction, _TreeCheckout_validatedCommits;
import { assert, unreachableCase, fail } from "@fluidframework/core-utils/internal";
import { createEmitter } from "@fluid-internal/client-utils";
import { UsageError, } from "@fluidframework/telemetry-utils/internal";
import { FluidClientVersion, FormatValidatorNoOp } from "../codec/index.js";
import { AnchorSet, CommitKind, RevertibleStatus, TreeStoredSchemaRepository, combineVisitors, makeDetachedFieldIndex, rebaseChange, rootFieldKey, tagChange, visitDelta, isAncestor, moveToDetachedField, LeafNodeStoredSchema, diffHistories, } from "../core/index.js";
import { allowsRepoSuperset, buildForest, createNodeIdentifierManager, defaultSchemaPolicy, intoDelta, jsonableTreeFromCursor, makeFieldBatchCodec, } from "../feature-libraries/index.js";
import { SquashingTransactionStack, SharedTreeBranch, TransactionResult, onForkTransitive, } from "../shared-tree-core/index.js";
import { Breakable, disposeSymbol, getOrCreate } from "../util/index.js";
import { SharedTreeChangeFamily, hasSchemaChange } from "./sharedTreeChangeFamily.js";
import { customFromCursorStored, } from "../simple-tree/index.js";
import { getCheckout, SchematizingSimpleTreeView } from "./schematizingTreeView.js";
/**
 * Creates a {@link TreeCheckout}.
 * @param args - an object containing optional components that will be used to build the view.
 * Any components not provided will be created by default.
 * @remarks This does not create a {@link SharedTree}, but rather a view with the minimal state
 * and functionality required to implement {@link ITreeCheckout}.
 */
export function createTreeCheckout(idCompressor, mintRevisionTag, revisionTagCodec, args) {
    const breaker = args?.breaker ?? new Breakable("TreeCheckout");
    const schema = args?.schema ?? new TreeStoredSchemaRepository();
    const forest = args?.forest ?? buildForest(breaker, schema);
    const defaultCodecOptions = {
        jsonValidator: FormatValidatorNoOp,
        minVersionForCollab: FluidClientVersion.v2_0,
    };
    const changeFamily = args?.changeFamily ??
        new SharedTreeChangeFamily(revisionTagCodec, args?.fieldBatchCodec ?? makeFieldBatchCodec(defaultCodecOptions), defaultCodecOptions, args?.chunkCompressionStrategy, idCompressor);
    // TODO: If a branch is passed in, is it supposed to be local main?
    // Otherwise shouldn't we set `isSharedBranch` to false?
    const branch = args?.branch ??
        new SharedTreeBranch({
            change: changeFamily.rebaser.compose([]),
            revision: "root",
        }, changeFamily, () => idCompressor.generateCompressedId());
    return new TreeCheckout(branch, true, changeFamily, schema, forest, mintRevisionTag, revisionTagCodec, idCompressor, args?.removedRoots, args?.logger, breaker, args?.disposeForksAfterTransaction);
}
/**
 * An implementation of {@link ITreeCheckoutFork}.
 */
export class TreeCheckout {
    constructor(branch, 
    /** True if and only if this checkout is for a branch which is persisted and shared with other clients. */
    isSharedBranch, changeFamily, storedSchema, forest, mintRevisionTag, revisionTagCodec, idCompressor, removedRoots = makeDetachedFieldIndex("repair", revisionTagCodec, idCompressor), 
    /** Optional logger for telemetry. */
    logger, breaker = new Breakable("TreeCheckout"), disposeForksAfterTransaction = true) {
        this.isSharedBranch = isSharedBranch;
        this.changeFamily = changeFamily;
        this.storedSchema = storedSchema;
        this.forest = forest;
        this.mintRevisionTag = mintRevisionTag;
        this.revisionTagCodec = revisionTagCodec;
        this.idCompressor = idCompressor;
        this.removedRoots = removedRoots;
        this.logger = logger;
        this.breaker = breaker;
        this.disposeForksAfterTransaction = disposeForksAfterTransaction;
        this.disposed = false;
        this.views = new Set();
        /**
         * Set of revertibles maintained for automatic disposal
         */
        this.revertibles = new Set();
        /**
         * Each branch's head commit corresponds to a revertible commit.
         * Maintaining a whole branch ensures the commit graph is not pruned in a way that would prevent the commit from
         * being reverted.
         */
        this.revertibleCommitBranches = new Map();
        _TreeCheckout_events.set(this, createEmitter());
        this.events = __classPrivateFieldGet(this, _TreeCheckout_events, "f");
        this.onAfterBranchChange = (event) => {
            // The following logic allows revertibles to be generated for the change.
            // Currently only appends (including merges and transaction commits) are supported.
            if (event.type === "append") {
                // TODO:#20949: When the SharedTree is detached, these commits will already have been garbage collected.
                //       Figure out a way to generate revertibles before the commits are garbage collected.
                for (const commit of event.newCommits) {
                    const kind = event.type === "append" ? event.kind : CommitKind.Default;
                    const { change, revision } = commit;
                    const getRevertible = hasSchemaChange(change)
                        ? undefined
                        : (onRevertibleDisposed) => {
                            if (!withinEventContext) {
                                throw new UsageError("Cannot get a revertible outside of the context of a changed event.");
                            }
                            if (this.revertibleCommitBranches.get(revision) !== undefined) {
                                throw new UsageError("Cannot generate the same revertible more than once. Note that this can happen when multiple changed event listeners are registered.");
                            }
                            const revertible = this.createRevertible(revision, kind, this, onRevertibleDisposed);
                            this.revertibleCommitBranches.set(revision, __classPrivateFieldGet(this, _TreeCheckout_transaction, "f").activeBranch.fork(commit));
                            this.revertibles.add(revertible);
                            return revertible;
                        };
                    let withinEventContext = true;
                    __classPrivateFieldGet(this, _TreeCheckout_events, "f").emit("changed", { isLocal: true, kind }, getRevertible);
                    withinEventContext = false;
                }
            }
            else if (this.isRemoteChangeEvent(event)) {
                // TODO: figure out how to plumb through commit kind info for remote changes
                __classPrivateFieldGet(this, _TreeCheckout_events, "f").emit("changed", { isLocal: false, kind: CommitKind.Default });
            }
        };
        this.onAfterChange = (event) => {
            this.editLock.lock();
            __classPrivateFieldGet(this, _TreeCheckout_events, "f").emit("beforeBatch", event);
            if (event.change !== undefined) {
                const revision = event.type === "rebase"
                    ? __classPrivateFieldGet(this, _TreeCheckout_transaction, "f").activeBranch.getHead().revision
                    : event.change.revision;
                this.applyChange(event.change.change, revision);
            }
            __classPrivateFieldGet(this, _TreeCheckout_events, "f").emit("afterBatch");
            this.editLock.unlock();
            if (event.type === "append") {
                event.newCommits.forEach((commit) => this.validateCommit(commit));
            }
        };
        this.onAncestryTrimmed = (revisions) => {
            // When the branch is trimmed, we can garbage collect any repair data whose latest relevant revision is one of the
            // trimmed revisions.
            this.withCombinedVisitor((visitor) => {
                revisions.forEach((revision) => {
                    // get all the roots last created or used by the revision
                    const roots = this.removedRoots.getRootsLastTouchedByRevision(revision);
                    // get the detached field for the root and delete it from the removed roots
                    for (const root of roots) {
                        visitor.destroy(this.removedRoots.toFieldKey(root), 1);
                    }
                    this.removedRoots.deleteRootsLastTouchedByRevision(revision);
                });
            });
        };
        /**
         * The {@link Transactor} for this checkout.
         * @remarks In the context of a checkout, transactions allow edits to be batched into atomic units.
         * Edits made during a transaction will update the local state of the tree immediately, but will be squashed into a single edit when the transaction is committed.
         * If the transaction is aborted, the local state will be reset to what it was before the transaction began.
         * Transactions may nest, meaning that a transaction may be started while a transaction is already ongoing.
         *
         * To avoid updating observers of the view state with intermediate results during a transaction,
         * use {@link ITreeCheckout#branch} and {@link ISharedTreeFork#merge}.
         */
        _TreeCheckout_transaction.set(this, void 0);
        // #region Commit Validation
        /** Used to maintain the contract of {@link onCommitValid}(). */
        _TreeCheckout_validatedCommits.set(this, new WeakMap());
        __classPrivateFieldSet(this, _TreeCheckout_transaction, this.createTransactionStack(branch), "f");
        this.editLock = new EditLock(__classPrivateFieldGet(this, _TreeCheckout_transaction, "f").activeBranchEditor);
        this.registerForBranchEvents();
    }
    registerForBranchEvents() {
        __classPrivateFieldGet(this, _TreeCheckout_transaction, "f").branch.events.on("afterChange", this.onAfterBranchChange);
        __classPrivateFieldGet(this, _TreeCheckout_transaction, "f").activeBranchEvents.on("afterChange", this.onAfterChange);
        __classPrivateFieldGet(this, _TreeCheckout_transaction, "f").activeBranchEvents.on("ancestryTrimmed", this.onAncestryTrimmed);
    }
    unregisterFromBranchEvents() {
        __classPrivateFieldGet(this, _TreeCheckout_transaction, "f").branch.events.off("afterChange", this.onAfterBranchChange);
        __classPrivateFieldGet(this, _TreeCheckout_transaction, "f").activeBranchEvents.off("afterChange", this.onAfterChange);
        __classPrivateFieldGet(this, _TreeCheckout_transaction, "f").activeBranchEvents.off("ancestryTrimmed", this.onAncestryTrimmed);
    }
    createTransactionStack(branch) {
        return new SquashingTransactionStack(branch, (commits) => {
            const revision = this.mintRevisionTag();
            for (const transactionStep of commits) {
                this.removedRoots.updateMajor(transactionStep.revision, revision);
            }
            const squashedChange = this.changeFamily.rebaser.compose(commits);
            const change = this.changeFamily.rebaser.changeRevision(squashedChange, revision);
            return tagChange(change, revision);
        }, () => {
            const disposeForks = this.disposeForksAfterTransaction
                ? trackForksForDisposal(this)
                : undefined;
            // When each transaction is started, take a snapshot of the current state of removed roots
            const removedRootsSnapshot = this.removedRoots.clone();
            return (result) => {
                switch (result) {
                    case TransactionResult.Abort:
                        this.removedRoots = removedRootsSnapshot;
                        break;
                    case TransactionResult.Commit:
                        if (!this.transaction.isInProgress()) {
                            // The changes in a transaction squash commit have already applied to the checkout and are known to be valid, so we can validate the squash commit automatically.
                            this.validateCommit(__classPrivateFieldGet(this, _TreeCheckout_transaction, "f").branch.getHead());
                        }
                        break;
                    default:
                        unreachableCase(result);
                }
                disposeForks?.();
            };
        });
    }
    exportVerbose() {
        const cursor = this.forest.allocateCursor("contentSnapshot");
        try {
            moveToDetachedField(this.forest, cursor);
            const length = cursor.getFieldLength();
            if (length === 0) {
                return undefined;
            }
            else if (length === 1) {
                cursor.enterNode(0);
                return verboseFromCursor(cursor, this.storedSchema.nodeSchema);
            }
            else {
                fail(0xac8 /* Invalid document root length */);
            }
        }
        finally {
            cursor.free();
        }
    }
    // Revision is the revision of the commit, if any, which caused this change.
    applyChange(change, revision) {
        // Conflicts due to schema will be empty and thus are not applied.
        for (const innerChange of change.changes) {
            if (innerChange.type === "data") {
                const delta = intoDelta(tagChange(innerChange.innerChange, revision));
                this.withCombinedVisitor((visitor) => {
                    visitDelta(delta, visitor, this.removedRoots, revision);
                });
            }
            else if (innerChange.type === "schema") {
                // Schema changes from a current to a new schema are expected to be backwards compatible.
                // This guarantees that all data in the forest (which is valid before the schema change)
                // is also valid under the new schema.
                // Note however, that such schema changes may in some cases be rolled back:
                // Case 1: A transaction with a schema change may be aborted.
                // The transaction may have made some data changes that would render some trees invalid
                // under the old schema, but these changes will also be rolled back, thereby putting the forest
                // back in the state before the transaction, which is valid under the original (reinstated) schema.
                // Case 2: A branch with a schema change may be rebased such that the schema change (because
                // of a constraint) is no longer applied.
                // Such a branch may contain data changes that would render some trees invalid under the
                // original schema. These data changes may not necessarily be rolled back.
                // They will however be rebased over the rollback of the schema change. This rebasing will
                // ensure that these data changes are muted if they would render some trees invalid under the
                // original (reinstated) schema.
                this.storedSchema.apply(innerChange.innerChange.schema.new);
            }
            else {
                fail(0xad1 /* Unknown Shared Tree change type. */);
            }
        }
    }
    withCombinedVisitor(fn) {
        const anchorVisitor = this.forest.anchors.acquireVisitor();
        const combinedVisitor = combineVisitors([this.forest.acquireVisitor(), anchorVisitor]);
        fn(combinedVisitor);
        combinedVisitor.free();
    }
    checkNotDisposed(usageError) {
        this.breaker.use();
        if (this.disposed) {
            if (usageError !== undefined) {
                throw new UsageError(usageError);
            }
            assert(false, 0x911 /* Invalid operation on a disposed TreeCheckout */);
        }
    }
    /**
     * Creates a {@link RevertibleAlpha} object that can undo a specific change in the tree's history.
     * Revision must exist in the given {@link TreeCheckout}'s branch.
     *
     * @param revision - The revision tag identifying the change to be made revertible.
     * @param kind - The {@link CommitKind} that produced this revertible (e.g., Default, Undo, Redo).
     * @param checkout - The {@link TreeCheckout} instance this revertible belongs to.
     * @param onRevertibleDisposed - Callback function that will be called when the revertible is disposed.
     * @returns A {@link RevertibleAlpha} object.
     */
    createRevertible(revision, kind, checkout, onRevertibleDisposed) {
        const commitBranches = checkout.revertibleCommitBranches;
        const revertible = {
            get status() {
                const revertibleCommit = commitBranches.get(revision);
                return revertibleCommit === undefined
                    ? RevertibleStatus.Disposed
                    : RevertibleStatus.Valid;
            },
            revert: (release = true) => {
                if (revertible.status === RevertibleStatus.Disposed) {
                    throw new UsageError("Unable to revert a revertible that has been disposed.");
                }
                const revertMetrics = checkout.revertRevertible(revision, kind);
                checkout.logger?.sendTelemetryEvent({
                    eventName: TreeCheckout.revertTelemetryEventName,
                    ...revertMetrics,
                });
                if (release) {
                    revertible.dispose();
                }
            },
            clone: (targetBranch) => {
                // TODO:#23442: When a revertible is cloned for a forked branch, optimize to create a fork of a revertible branch once per revision NOT once per revision per checkout.
                const targetCheckout = getCheckout(targetBranch);
                const revertibleBranch = this.revertibleCommitBranches.get(revision);
                if (revertibleBranch === undefined) {
                    throw new UsageError("Unable to clone a revertible that has been disposed.");
                }
                const commitToRevert = revertibleBranch.getHead();
                const activeBranchHead = __classPrivateFieldGet(targetCheckout, _TreeCheckout_transaction, "f").activeBranch.getHead();
                if (isAncestor(commitToRevert, activeBranchHead, true) === false) {
                    throw new UsageError("Cannot clone revertible for a commit that is not present on the given branch.");
                }
                targetCheckout.revertibleCommitBranches.set(revision, revertibleBranch.fork());
                return this.createRevertible(revision, kind, targetCheckout, onRevertibleDisposed);
            },
            dispose: () => {
                if (revertible.status === RevertibleStatus.Disposed) {
                    throw new UsageError("Unable to dispose a revertible that has already been disposed.");
                }
                checkout.disposeRevertible(revertible, revision);
                onRevertibleDisposed?.(revertible);
            },
        };
        return revertible;
    }
    viewWith(config) {
        const view = new SchematizingSimpleTreeView(this, config, createNodeIdentifierManager(this.idCompressor), () => {
            this.views.delete(view);
        });
        this.views.add(view);
        return view;
    }
    get rootEvents() {
        return this.forest.anchors.events;
    }
    get editor() {
        this.checkNotDisposed();
        return this.editLock.editor;
    }
    locate(anchor) {
        this.checkNotDisposed();
        return this.forest.anchors.locate(anchor);
    }
    get transaction() {
        return __classPrivateFieldGet(this, _TreeCheckout_transaction, "f");
    }
    branch() {
        this.checkNotDisposed("The parent branch has already been disposed and can no longer create new branches.");
        this.editLock.checkUnlocked("Branching");
        const anchors = new AnchorSet();
        const branch = __classPrivateFieldGet(this, _TreeCheckout_transaction, "f").activeBranch.fork();
        const storedSchema = this.storedSchema.clone();
        const forest = this.forest.clone(storedSchema, anchors);
        const checkout = new TreeCheckout(branch, false, this.changeFamily, storedSchema, forest, this.mintRevisionTag, this.revisionTagCodec, this.idCompressor, this.removedRoots.clone(), this.logger, this.breaker, this.disposeForksAfterTransaction);
        __classPrivateFieldGet(this, _TreeCheckout_events, "f").emit("fork", checkout);
        return checkout;
    }
    switchBranch(branch) {
        // TODO: Dispose old branch, if necessary
        assert(!__classPrivateFieldGet(this, _TreeCheckout_transaction, "f").isInProgress(), 0xc55 /* Cannot switch branches during a transaction */);
        const diff = diffHistories(this.changeFamily.rebaser, __classPrivateFieldGet(this, _TreeCheckout_transaction, "f").branch.getHead(), branch.getHead(), this.mintRevisionTag);
        this.unregisterFromBranchEvents();
        __classPrivateFieldSet(this, _TreeCheckout_transaction, this.createTransactionStack(branch), "f");
        this.editLock = new EditLock(__classPrivateFieldGet(this, _TreeCheckout_transaction, "f").activeBranchEditor);
        this.registerForBranchEvents();
        // TODO: Rework eventing
        this.applyChange(diff);
        __classPrivateFieldGet(this, _TreeCheckout_events, "f").emit("afterBatch");
    }
    rebase(checkout) {
        this.checkNotDisposed("The target of the branch rebase has been disposed and cannot be rebased.");
        checkout.checkNotDisposed("The source of the branch rebase has been disposed and cannot be rebased.");
        this.editLock.checkUnlocked("Rebasing");
        assert(!checkout.transaction.isInProgress(), 0x9af /* A view cannot be rebased while it has a pending transaction */);
        assert(!checkout.isSharedBranch, 0xa5d /* Shared branches cannot be rebased onto another branch. */);
        __classPrivateFieldGet(checkout, _TreeCheckout_transaction, "f").activeBranch.rebaseOnto(__classPrivateFieldGet(this, _TreeCheckout_transaction, "f").activeBranch);
    }
    rebaseOnto(checkout) {
        this.checkNotDisposed("The target of the branch rebase has been disposed and cannot be rebased.");
        checkout.rebase(this);
    }
    merge(checkout, disposeMerged = true) {
        this.checkNotDisposed("The target of the branch merge has been disposed and cannot be merged.");
        checkout.checkNotDisposed("The source of the branch merge has been disposed and cannot be merged.");
        this.editLock.checkUnlocked("Merging");
        assert(!this.transaction.isInProgress(), 0x9b0 /* Views cannot be merged into a view while it has a pending transaction */);
        while (checkout.transaction.isInProgress()) {
            checkout.transaction.commit();
        }
        __classPrivateFieldGet(this, _TreeCheckout_transaction, "f").activeBranch.merge(__classPrivateFieldGet(checkout, _TreeCheckout_transaction, "f").activeBranch);
        if (disposeMerged && !checkout.isSharedBranch) {
            // Dispose the merged checkout unless it is a shared branch.
            checkout[disposeSymbol]();
        }
    }
    updateSchema(newSchema, allowNonSupersetSchema) {
        this.checkNotDisposed();
        if (allowNonSupersetSchema !== true) {
            assert(allowsRepoSuperset(defaultSchemaPolicy, this.storedSchema.clone(), newSchema), 0xbe6 /* New schema must allow all documents allowed by old schema */);
        }
        this.editor.schema.setStoredSchema(this.storedSchema.clone(), newSchema);
    }
    dispose() {
        this.editLock.checkUnlocked("Disposing a view");
        this[disposeSymbol]();
    }
    [(_TreeCheckout_events = new WeakMap(), _TreeCheckout_transaction = new WeakMap(), _TreeCheckout_validatedCommits = new WeakMap(), disposeSymbol)]() {
        this.checkNotDisposed("The branch has already been disposed and cannot be disposed again.");
        this.disposed = true;
        __classPrivateFieldGet(this, _TreeCheckout_transaction, "f").branch.dispose();
        __classPrivateFieldGet(this, _TreeCheckout_transaction, "f").dispose();
        this.purgeRevertibles();
        for (const view of this.views) {
            view.dispose();
        }
        __classPrivateFieldGet(this, _TreeCheckout_events, "f").emit("dispose");
    }
    getRemovedRoots() {
        this.assertNoUntrackedRoots();
        const trees = [];
        const cursor = this.forest.allocateCursor("getRemovedRoots");
        for (const { id, root } of this.removedRoots.entries()) {
            const parentField = this.removedRoots.toFieldKey(root);
            this.forest.moveCursorToPath({ parent: undefined, parentField, parentIndex: 0 }, cursor);
            const tree = jsonableTreeFromCursor(cursor);
            // This method is used for tree consistency comparison.
            const { major, minor } = id;
            const finalizedMajor = major !== undefined ? this.revisionTagCodec.encode(major) : major;
            trees.push([finalizedMajor, minor, tree]);
        }
        cursor.free();
        return trees;
    }
    /**
     * This must be called on the root/main checkout after loading from a summary.
     * @remarks This pattern is necessary because the EditManager skips the normal process of applying commits to branches when loading a summary.
     * Instead, it simply {@link SharedTreeBranch#setHead | mutates} the branches directly which does not propagate the typical events throughout the rest of the system.
     */
    load() {
        // Set the tip revision as the latest relevant revision for any removed roots that are loaded from a summary - this allows them to be garbage collected later.
        // When a load happens, the head of the trunk and the head of the local/main branch must be the same (this is enforced by SharedTree).
        this.removedRoots.setRevisionsForLoadedData(__classPrivateFieldGet(this, _TreeCheckout_transaction, "f").branch.getHead().revision);
        // The content of the checkout (e.g. the forest) has (maybe) changed, so fire an afterBatch event.
        __classPrivateFieldGet(this, _TreeCheckout_events, "f").emit("afterBatch");
    }
    purgeRevertibles() {
        for (const revertible of this.revertibles) {
            revertible.dispose();
        }
    }
    disposeRevertible(revertible, revision) {
        this.revertibleCommitBranches.get(revision)?.dispose();
        this.revertibleCommitBranches.delete(revision);
        this.revertibles.delete(revertible);
    }
    revertRevertible(revision, kind) {
        this.editLock.checkUnlocked("Reverting a commit");
        if (this.transaction.isInProgress()) {
            throw new UsageError("Undo is not yet supported during transactions.");
        }
        const revertibleBranch = this.revertibleCommitBranches.get(revision);
        assert(revertibleBranch !== undefined, 0x7cc /* expected to find a revertible commit */);
        const commitToRevert = revertibleBranch.getHead();
        const revisionForInvert = this.mintRevisionTag();
        let change = tagChange(this.changeFamily.rebaser.invert(commitToRevert, false, revisionForInvert), revisionForInvert);
        const headCommit = __classPrivateFieldGet(this, _TreeCheckout_transaction, "f").activeBranch.getHead();
        // Rebase the inverted change onto any commits that occurred after the undoable commits.
        if (commitToRevert !== headCommit) {
            change = tagChange(rebaseChange(this.changeFamily.rebaser, change, commitToRevert, headCommit, this.mintRevisionTag).change, revisionForInvert);
        }
        __classPrivateFieldGet(this, _TreeCheckout_transaction, "f").activeBranch.apply(change, kind === CommitKind.Default || kind === CommitKind.Redo
            ? CommitKind.Undo
            : CommitKind.Redo);
        // Derive some stats about the reversion to return to the caller.
        let revertAge = 0;
        let currentCommit = headCommit;
        while (commitToRevert.revision !== currentCommit.revision) {
            revertAge++;
            const parentCommit = currentCommit.parent;
            assert(parentCommit !== undefined, 0x9a9 /* expected to find a parent commit */);
            currentCommit = parentCommit;
        }
        return { age: revertAge };
    }
    assertNoUntrackedRoots() {
        const cursor = this.forest.getCursorAboveDetachedFields();
        const rootFields = new Set([rootFieldKey]);
        for (const { root } of this.removedRoots.entries()) {
            rootFields.add(this.removedRoots.toFieldKey(root));
        }
        if (!cursor.firstField()) {
            return;
        }
        do {
            const field = cursor.getFieldKey();
            assert(rootFields.has(field), 0xa22 /* Forest has a root field which is unknown to the detached field index */);
            rootFields.delete(field);
        } while (cursor.nextField());
    }
    /**
     * `true` iff the given branch change event is due to a remote change
     */
    isRemoteChangeEvent(event) {
        return (
        // Remote changes are only ever applied to shared branches
        this.isSharedBranch &&
            // Remote changes are applied to the branch by rebasing it onto the trunk.
            // No other rebases are allowed on shared branches, so we can use this to detect remote changes.
            event.type === "rebase");
    }
    /**
     * Registers a function to be called when the given commit is validated.
     * @remarks A commit is validated by the checkout after it has been applied to the checkout's state (e.g. it has an effect on the forest).
     * If the commit applies successfully (i.e. it does not raise any unexpected errors), the commit is considered valid and the registered function is called.
     * If the commit does not apply successfully (because it causes an unexpected error), the function is not called (and the checkout will left in an error state).
     *
     * If the commit has already been validated when this function is called, the function is called immediately and this function returns `true`.
     * Otherwise, the function is registered to be called later and this function returns `false`.
     */
    onCommitValid(commit, fn) {
        const validated = getOrCreate(__classPrivateFieldGet(this, _TreeCheckout_validatedCommits, "f"), commit, () => []);
        if (validated === true) {
            fn(commit);
            return true;
        }
        validated.push(fn);
        return false;
    }
    /** Mark the given commit as "validated" according to the contract of {@link onCommitValid}(). */
    validateCommit(commit) {
        const validated = getOrCreate(__classPrivateFieldGet(this, _TreeCheckout_validatedCommits, "f"), commit, () => []);
        if (validated !== true) {
            validated.forEach((fn) => fn(commit));
            __classPrivateFieldGet(this, _TreeCheckout_validatedCommits, "f").set(commit, true);
        }
    }
}
/**
 * The name of the telemetry event logged for calls to {@link TreeCheckout.revertRevertible}.
 * @privateRemarks Exposed for testing purposes.
 */
TreeCheckout.revertTelemetryEventName = "RevertRevertible";
/**
 * A helper class that assists {@link TreeCheckout} in preventing functionality from being used while the tree is in the middle of being edited.
 */
class EditLock {
    /**
     * @param editor - an editor which will be used to create a new editor that is monitored to determine if any changes are happening to the tree.
     * Use {@link EditLock.editor} in place of the original editor to ensure that changes are monitored.
     */
    constructor(editor) {
        this.locked = false;
        const checkLock = () => this.checkUnlocked("Editing the tree");
        this.editor = {
            get schema() {
                return editor.schema;
            },
            valueField(...fieldArgs) {
                const valueField = editor.valueField(...fieldArgs);
                return {
                    set(...editArgs) {
                        checkLock();
                        valueField.set(...editArgs);
                    },
                };
            },
            optionalField(...fieldArgs) {
                const optionalField = editor.optionalField(...fieldArgs);
                return {
                    set(...editArgs) {
                        checkLock();
                        optionalField.set(...editArgs);
                    },
                };
            },
            sequenceField(...fieldArgs) {
                const sequenceField = editor.sequenceField(...fieldArgs);
                return {
                    insert(...editArgs) {
                        checkLock();
                        sequenceField.insert(...editArgs);
                    },
                    remove(...editArgs) {
                        checkLock();
                        sequenceField.remove(...editArgs);
                    },
                };
            },
            move(...moveArgs) {
                checkLock();
                editor.move(...moveArgs);
            },
            addNodeExistsConstraint(path) {
                editor.addNodeExistsConstraint(path);
            },
            addNodeExistsConstraintOnRevert(path) {
                editor.addNodeExistsConstraintOnRevert(path);
            },
        };
    }
    /**
     * Prevent further changes from being made to {@link EditLock.editor} until {@link EditLock.unlock} is called.
     * @remarks May only be called when the lock is not already locked.
     */
    lock() {
        if (this.locked) {
            debugger;
        }
        assert(!this.locked, 0xaa7 /* Checkout has already been locked */);
        this.locked = true;
    }
    /**
     * Throws an error if the lock is currently locked.
     * @param action - The current action being performed by the user.
     * This must start with a capital letter, as it shows up as the first part of the error message and we want it to look nice.
     */
    checkUnlocked(action) {
        if (this.locked) {
            // These type assertions ensure that the event name strings used here match the actual event names
            const nodeChanged = "nodeChanged";
            const treeChanged = "treeChanged";
            throw new UsageError(`${action} is forbidden during a ${nodeChanged} or ${treeChanged} event`);
        }
    }
    /**
     * Allow changes to be made to {@link EditLock.editor} again.
     * @remarks May only be called when the lock is currently locked.
     */
    unlock() {
        assert(this.locked, 0xaa8 /* Checkout has not been locked */);
        this.locked = false;
    }
}
/**
 * Keeps track of all new forks created until the returned function is invoked, which will dispose all of those for.
 * The returned function may only be called once.
 *
 * @param checkout - The tree checkout for which you want to monitor forks for disposal.
 * @returns a function which can be called to dispose all of the tracked forks.
 */
function trackForksForDisposal(checkout) {
    const forks = new Set();
    const onDisposeUnSubscribes = [];
    const onForkUnSubscribe = onForkTransitive(checkout, (fork) => {
        forks.add(fork);
        onDisposeUnSubscribes.push(fork.events.on("dispose", () => forks.delete(fork)));
    });
    let disposed = false;
    return () => {
        assert(!disposed, 0xaa9 /* Forks may only be disposed once */);
        forks.forEach((fork) => fork.dispose());
        onDisposeUnSubscribes.forEach((unsubscribe) => unsubscribe());
        onForkUnSubscribe();
        disposed = true;
    };
}
function verboseFromCursor(reader, schema) {
    const fields = customFromCursorStored(reader, schema, verboseFromCursor);
    const nodeSchema = schema.get(reader.type) ?? fail(0xac9 /* missing schema for type in cursor */);
    if (nodeSchema instanceof LeafNodeStoredSchema) {
        return fields;
    }
    return {
        type: reader.type,
        fields: fields,
    };
}
//# sourceMappingURL=treeCheckout.js.map