/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { assert } from "@fluidframework/core-utils";
import { AnchorSet, InMemoryStoredSchemaRepository, assertIsRevisionTag, combineVisitors, visitDelta, makeDetachedFieldIndex, } from "../core";
import { createEmitter } from "../events";
import { buildForest, DefaultChangeFamily, getEditableTreeContext, getTreeContext, createNodeKeyManager, nodeKeyFieldKey as nodeKeyFieldKeyDefault, getProxyForField, } from "../feature-libraries";
import { SharedTreeBranch, getChangeReplaceType } from "../shared-tree-core";
import { TransactionResult, brand } from "../util";
import { noopValidator } from "../codec";
/**
 * Creates a {@link SharedTreeView}.
 * @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 ISharedTreeView}.
 */
export function createSharedTreeView(args) {
    const schema = args?.schema ?? new InMemoryStoredSchemaRepository();
    const forest = args?.forest ?? buildForest();
    const changeFamily = args?.changeFamily ?? new DefaultChangeFamily({ jsonValidator: noopValidator });
    const branch = args?.branch ??
        new SharedTreeBranch({
            change: changeFamily.rebaser.compose([]),
            revision: assertIsRevisionTag("00000000-0000-4000-8000-000000000000"),
        }, changeFamily);
    const context = getEditableTreeContext(forest, schema, branch.editor);
    const events = args?.events ?? createEmitter();
    const transaction = new Transaction(branch);
    return new SharedTreeView(transaction, branch, changeFamily, schema, forest, context, events, args?.removedTrees);
}
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 ISharedTreeBranchView}.
 */
export class SharedTreeView {
    constructor(transaction, branch, changeFamily, storedSchema, forest, context, events, removedTrees = makeDetachedFieldIndex("repair")) {
        this.transaction = transaction;
        this.branch = branch;
        this.changeFamily = changeFamily;
        this.storedSchema = storedSchema;
        this.forest = forest;
        this.context = context;
        this.events = events;
        this.removedTrees = removedTrees;
        branch.on("change", (event) => {
            if (event.change !== undefined) {
                const delta = this.changeFamily.intoDelta(event.change);
                const anchorVisitor = this.forest.anchors.acquireVisitor();
                const combinedVisitor = combineVisitors([this.forest.acquireVisitor(), anchorVisitor], [anchorVisitor]);
                visitDelta(delta, combinedVisitor, this.removedTrees);
                combinedVisitor.free();
                this.events.emit("afterBatch");
            }
            if (event.type === "replace" && getChangeReplaceType(event) === "transactionCommit") {
                const transactionRevision = event.newCommits[0].revision;
                for (const transactionStep of event.removedCommits) {
                    this.removedTrees.updateMajor(transactionStep.revision, transactionRevision);
                }
            }
        });
        branch.on("revertible", (revertible) => {
            // if there are no listeners, discard the revertible to avoid memory leaks
            if (!this.events.hasListeners("revertible")) {
                revertible.discard();
            }
            else {
                this.events.emit("revertible", revertible);
            }
        });
    }
    get rootEvents() {
        return this.forest.anchors;
    }
    get editor() {
        return this.branch.editor;
    }
    editableTree2(viewSchema, nodeKeyManager, nodeKeyFieldKey) {
        const context = getTreeContext(viewSchema, this.forest, this.branch.editor, nodeKeyManager ?? createNodeKeyManager(), nodeKeyFieldKey ?? brand(nodeKeyFieldKeyDefault));
        return context.root;
    }
    root2(viewSchema) {
        // TODO:
        // this allocates and leaks a new editable tree context (when used it will add content to the AnchorSet which refers back to the context).
        // Additionally its assumed there will be exactly one context per view and any TreeNodes cached on the AnchorSets will belong to that context.
        // Calling this more than once would violate that assumption, but currently does not error.
        // Therefore root2, like editableTree2 should really only be called once.
        // However, since getProxyForField returns an object that no longer reflects the root after the root is edited (unlike the root field in editableTree2)
        // users will need to call root2 again whenever that might have happened to get the new root.
        // This makes it impractical to use this efficiently and correctly at the same time.
        // This method is also undocumented which thus doesn't provide sufficient guidance to resolve this issue.
        const rootField = this.editableTree2(viewSchema);
        return getProxyForField(rootField);
    }
    locate(anchor) {
        return this.forest.anchors.locate(anchor);
    }
    fork() {
        const anchors = new AnchorSet();
        // TODO: ensure editing this clone of the schema does the right thing.
        const storedSchema = new InMemoryStoredSchemaRepository(this.storedSchema);
        const forest = this.forest.clone(storedSchema, anchors);
        const branch = this.branch.fork();
        const context = getEditableTreeContext(forest, storedSchema, branch.editor);
        const transaction = new Transaction(branch);
        return new SharedTreeView(transaction, branch, this.changeFamily, storedSchema, forest, context, createEmitter(), this.removedTrees.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();
        }
    }
    get root() {
        return this.context.unwrappedRoot;
    }
    setContent(data) {
        this.context.setContent(data);
    }
    /**
     * 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();
    }
}
/**
 * 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
 * @alpha
 */
export function runSynchronous(view, transaction) {
    view.transaction.start();
    const result = transaction(view);
    return result === TransactionResult.Abort
        ? view.transaction.abort()
        : view.transaction.commit();
}
//# sourceMappingURL=sharedTreeView.js.map