/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { assert } from "@fluidframework/core-utils";
import { AnchorSet, TreeStoredSchemaRepository, combineVisitors, visitDelta, makeDetachedFieldIndex, tagChange, } from "../core/index.js";
import { createEmitter } from "../events/index.js";
import { buildForest, intoDelta, jsonableTreeFromCursor, makeFieldBatchCodec, } from "../feature-libraries/index.js";
import { SharedTreeBranch, getChangeReplaceType } from "../shared-tree-core/index.js";
import { TransactionResult, fail } from "../util/index.js";
import { noopValidator } from "../codec/index.js";
import { SharedTreeChangeFamily } from "./sharedTreeChangeFamily.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, revisionTagCodec, args) {
    const forest = args?.forest ?? buildForest();
    const schema = args?.schema ?? new TreeStoredSchemaRepository();
    const defaultCodecOptions = { jsonValidator: noopValidator };
    const changeFamily = args?.changeFamily ??
        new SharedTreeChangeFamily(revisionTagCodec, args?.fieldBatchCodec ?? makeFieldBatchCodec(defaultCodecOptions), { jsonValidator: noopValidator }, args?.chunkCompressionStrategy);
    const branch = args?.branch ??
        new SharedTreeBranch({
            change: changeFamily.rebaser.compose([]),
            revision: "root",
        }, changeFamily, () => idCompressor.generateCompressedId());
    const events = args?.events ?? createEmitter();
    const transaction = new Transaction(branch);
    return new TreeCheckout(transaction, branch, changeFamily, schema, forest, events, revisionTagCodec, args?.removedRoots);
}
class Transaction {
    constructor(branch) {
        this.branch = branch;
    }
    start() {
        this.branch.startTransaction();
        this.branch.editor.enterTransaction();
    }
    commit() {
        this.branch.commitTransaction();
        this.branch.editor.exitTransaction();
        return TransactionResult.Commit;
    }
    abort() {
        this.branch.abortTransaction();
        this.branch.editor.exitTransaction();
        return TransactionResult.Abort;
    }
    inProgress() {
        return this.branch.isTransacting();
    }
}
/**
 * An implementation of {@link ITreeCheckoutFork}.
 */
export class TreeCheckout {
    constructor(transaction, branch, changeFamily, storedSchema, forest, events, revisionTagCodec, removedRoots = makeDetachedFieldIndex("repair", revisionTagCodec)) {
        this.transaction = transaction;
        this.branch = branch;
        this.changeFamily = changeFamily;
        this.storedSchema = storedSchema;
        this.forest = forest;
        this.events = events;
        this.revisionTagCodec = revisionTagCodec;
        this.removedRoots = removedRoots;
        // We subscribe to `beforeChange` rather than `afterChange` here because it's possible that the change is invalid WRT our forest.
        // For example, a bug in the editor might produce a malformed change object and thus applying the change to the forest will throw an error.
        // In such a case we will crash here, preventing the change from being added to the commit graph, and preventing `afterChange` from firing.
        // One important consequence of this is that we will not submit the op containing the invalid change, since op submissions happens in response to `afterChange`.
        branch.on("beforeChange", (event) => {
            if (event.change !== undefined) {
                // Conflicts due to schema will be empty and thus are not applied.
                for (const change of event.change.change.changes) {
                    if (change.type === "data") {
                        const delta = intoDelta(tagChange(change.innerChange, event.change.revision));
                        this.withCombinedVisitor((visitor) => {
                            visitDelta(delta, visitor, this.removedRoots);
                        });
                    }
                    else if (change.type === "schema") {
                        // We purge all removed content because the schema change may render that repair data invalid.
                        // This happens on all peers that receive the schema change.
                        // Note that while the originator of the schema change could theoretically validate/update the
                        // repair data that it has, so that is it guaranteed to be valid with the new schema, we cannot
                        // guarantee that the originator has a superset of the repair data that other clients have.
                        // This means the originator cannot guarantee that the repair data on all peers is valid for
                        // the new schema.
                        this.purgeRemovedRoots();
                        storedSchema.apply(change.innerChange.schema.new);
                    }
                    else {
                        fail("Unknown Shared Tree change type.");
                    }
                }
                this.events.emit("afterBatch");
            }
            if (event.type === "replace" && getChangeReplaceType(event) === "transactionCommit") {
                const transactionRevision = event.newCommits[0].revision;
                for (const transactionStep of event.removedCommits) {
                    this.removedRoots.updateMajor(transactionStep.revision, transactionRevision);
                }
            }
        });
        branch.on("newRevertible", (revertible) => {
            this.events.emit("newRevertible", revertible);
        });
        branch.on("revertibleDisposed", (revertible, revision) => {
            // We do not expose the revision in this API
            this.events.emit("revertibleDisposed", revertible);
        });
    }
    withCombinedVisitor(fn) {
        const anchorVisitor = this.forest.anchors.acquireVisitor();
        const combinedVisitor = combineVisitors([this.forest.acquireVisitor(), anchorVisitor], [anchorVisitor]);
        fn(combinedVisitor);
        combinedVisitor.free();
    }
    purgeRemovedRoots() {
        // Revertibles are susceptible to use repair data so we purge them.
        this.branch.purgeRevertibles();
        this.withCombinedVisitor((visitor) => {
            for (const { root } of this.removedRoots.entries()) {
                const field = this.removedRoots.toFieldKey(root);
                // TODO:AD5509 Handle arbitrary-length fields once the storage of removed roots is no longer atomized.
                visitor.destroy(field, 1);
            }
        });
        this.removedRoots.purge();
    }
    get rootEvents() {
        return this.forest.anchors;
    }
    get editor() {
        return this.branch.editor;
    }
    locate(anchor) {
        return this.forest.anchors.locate(anchor);
    }
    fork() {
        const anchors = new AnchorSet();
        const branch = this.branch.fork();
        const storedSchema = this.storedSchema.clone();
        const forest = this.forest.clone(storedSchema, anchors);
        const transaction = new Transaction(branch);
        return new TreeCheckout(transaction, branch, this.changeFamily, storedSchema, forest, createEmitter(), this.revisionTagCodec, this.removedRoots.clone());
    }
    rebase(view) {
        view.branch.rebaseOnto(this.branch);
    }
    rebaseOnto(view) {
        view.rebase(this);
    }
    merge(view, disposeView = true) {
        assert(!this.transaction.inProgress() || disposeView, 0x710 /* A view that is merged into an in-progress transaction must be disposed */);
        while (view.transaction.inProgress()) {
            view.transaction.commit();
        }
        this.branch.merge(view.branch);
        if (disposeView) {
            view.dispose();
        }
    }
    updateSchema(newSchema) {
        this.editor.schema.setStoredSchema(this.storedSchema.clone(), newSchema);
    }
    /**
     * Dispose this view, freezing its state and allowing the SharedTree to release resources required by it.
     * Attempts to further mutate or dispose this view will error.
     */
    dispose() {
        this.branch.dispose();
    }
    getRemovedRoots() {
        const trees = [];
        const cursor = this.forest.allocateCursor();
        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);
            if (tree !== undefined) {
                // 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;
    }
}
/**
 * Run a synchronous transaction on the given shared tree view.
 * This is a convenience helper around the {@link SharedTreeFork#transaction} APIs.
 * @param view - the view on which to run the transaction
 * @param transaction - the transaction function. This will be executed immediately. It is passed `view` as an argument for convenience.
 * If this function returns an `Abort` result then the transaction will be aborted. Otherwise, it will be committed.
 * @returns whether or not the transaction was committed or aborted
 * @internal
 */
export function runSynchronous(view, transaction) {
    view.transaction.start();
    const result = transaction(view);
    return result === TransactionResult.Abort
        ? view.transaction.abort()
        : view.transaction.commit();
}
//# sourceMappingURL=treeCheckout.js.map