/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
    function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
    var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
    var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
    var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
    var _, done = false;
    for (var i = decorators.length - 1; i >= 0; i--) {
        var context = {};
        for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
        for (var p in contextIn.access) context.access[p] = contextIn.access[p];
        context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
        var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
        if (kind === "accessor") {
            if (result === void 0) continue;
            if (result === null || typeof result !== "object") throw new TypeError("Object expected");
            if (_ = accept(result.get)) descriptor.get = _;
            if (_ = accept(result.set)) descriptor.set = _;
            if (_ = accept(result.init)) initializers.unshift(_);
        }
        else if (_ = accept(result)) {
            if (kind === "field") initializers.unshift(_);
            else descriptor[key] = _;
        }
    }
    if (target) Object.defineProperty(target, contextIn.name, descriptor);
    done = true;
};
var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
    var useValue = arguments.length > 2;
    for (var i = 0; i < initializers.length; i++) {
        value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
    }
    return useValue ? value : void 0;
};
var __setFunctionName = (this && this.__setFunctionName) || function (f, name, prefix) {
    if (typeof name === "symbol") name = name.description ? "[".concat(name.description, "]") : "";
    return Object.defineProperty(f, "name", { configurable: true, value: prefix ? "".concat(prefix, " ", name) : name });
};
import { createEmitter } from "@fluid-internal/client-utils";
import { assert, unreachableCase } from "@fluidframework/core-utils/internal";
import { UsageError } from "@fluidframework/telemetry-utils/internal";
import { anchorSlot } from "../core/index.js";
import { defaultSchemaPolicy, cursorForMapTreeField, TreeStatus, Context, } from "../feature-libraries/index.js";
import { tryGetTreeNodeForField, setField, normalizeFieldSchema, SchemaCompatibilityTester, getOrCreateInnerNode, getKernel, HydratedContext, SimpleContextSlot, areImplicitFieldSchemaEqual, prepareForInsertionContextless, tryDisposeTreeNode, FieldSchemaAlpha, TreeViewConfigurationAlpha, toInitialSchema, toUpgradeSchema, } from "../simple-tree/index.js";
import { breakingClass, disposeSymbol, } from "../util/index.js";
import { canInitialize, initialize, initializerFromChunk } from "./schematizeTree.js";
/**
 * Creating multiple tree views from the same checkout is not supported. This slot is used to detect if one already
 * exists and error if creating a second.
 */
export const ViewSlot = anchorSlot();
/**
 * Implementation of TreeView wrapping a FlexTreeView.
 */
let SchematizingSimpleTreeView = (() => {
    let _classDecorators = [breakingClass];
    let _classDescriptor;
    let _classExtraInitializers = [];
    let _classThis;
    var SchematizingSimpleTreeView = _classThis = class {
        constructor(checkout, config, nodeKeyManager, onDispose) {
            this.checkout = checkout;
            this.config = config;
            this.nodeKeyManager = nodeKeyManager;
            this.onDispose = onDispose;
            this.events = createEmitter();
            /**
             * Events to unregister upon flex-tree view disposal.
             */
            this.flexTreeViewUnregisterCallbacks = new Set();
            /**
             * Events to unregister upon disposal.
             */
            this.unregisterCallbacks = new Set();
            this.disposed = false;
            /**
             * This is set to true while an edit impacting the document schema is in progress.
             * This allows suppressing extra rootChanged / schemaChanged events until the edit concludes.
             * This is useful especially for some initialization edits, since document initialization can involve transient schemas
             * which are implementation details and should not be exposed to the user.
             */
            this.midUpgrade = false;
            this.breaker = checkout.breaker;
            if (checkout.forest.anchors.slots.has(ViewSlot)) {
                throw new UsageError("Cannot create a second tree view from the same checkout");
            }
            checkout.forest.anchors.slots.set(ViewSlot, this);
            this.rootFieldSchema = normalizeFieldSchema(config.schema);
            const configAlpha = new TreeViewConfigurationAlpha({ schema: config.schema });
            this.viewSchema = new SchemaCompatibilityTester(configAlpha);
            // This must be initialized before `update` can be called.
            this.currentCompatibility = {
                canView: false,
                canUpgrade: true,
                isEquivalent: false,
                canInitialize: true,
            };
            this.update();
            this.unregisterCallbacks.add(this.checkout.events.on("changed", (data, getRevertible) => {
                this.events.emit("changed", data, getRevertible);
                this.events.emit("commitApplied", data, getRevertible);
            }));
        }
        hasRootSchema(schema) {
            return areImplicitFieldSchemaEqual(this.rootFieldSchema, schema);
        }
        get schema() {
            return this.config.schema;
        }
        initialize(content) {
            this.ensureUndisposed();
            const compatibility = this.compatibility;
            if (!compatibility.canInitialize) {
                throw new UsageError("Tree cannot be initialized more than once.");
            }
            this.runSchemaEdit(() => {
                const schema = toInitialSchema(this.config.schema);
                const mapTree = prepareForInsertionContextless(content, this.rootFieldSchema, {
                    schema,
                    policy: defaultSchemaPolicy,
                }, this, schema.rootFieldSchema);
                this.checkout.transaction.start();
                initialize(this.checkout, schema, initializerFromChunk(this.checkout, () => {
                    // This must be done after initial schema is set!
                    return this.checkout.forest.chunkField(cursorForMapTreeField(mapTree === undefined ? [] : [mapTree]));
                }));
                this.checkout.transaction.commit();
            });
        }
        upgradeSchema() {
            this.ensureUndisposed();
            const compatibility = this.compatibility;
            if (compatibility.isEquivalent) {
                // No-op
                return;
            }
            if (!compatibility.canUpgrade) {
                throw new UsageError("Existing stored schema cannot be upgraded (see TreeView.compatibility.canUpgrade).");
            }
            const newSchema = toUpgradeSchema(this.viewSchema.viewSchema.root);
            this.runSchemaEdit(() => this.checkout.updateSchema(newSchema));
        }
        /**
         * Gets the flex-tree context. Throws when disposed or out of schema.
         */
        getFlexTreeContext() {
            this.ensureUndisposed();
            assert(this.flexTreeContext !== undefined, 0x8c0 /* unexpected getViewOrError */);
            return this.flexTreeContext;
        }
        runTransaction(transaction, params) {
            const addConstraints = (constraintsOnRevert, constraints = []) => {
                addConstraintsToTransaction(this.checkout, constraintsOnRevert, constraints);
            };
            this.checkout.transaction.start();
            // Validate preconditions before running the transaction callback.
            addConstraints(false /* constraintsOnRevert */, params?.preconditions);
            const transactionCallbackStatus = transaction();
            const rollback = transactionCallbackStatus?.rollback;
            const value = transactionCallbackStatus?.value;
            if (rollback === true) {
                this.checkout.transaction.abort();
                return value !== undefined
                    ? { success: false, value: value }
                    : { success: false };
            }
            // Validate preconditions on revert after running the transaction callback and was successful.
            addConstraints(true /* constraintsOnRevert */, transactionCallbackStatus?.preconditionsOnRevert);
            this.checkout.transaction.commit();
            return value !== undefined
                ? { success: true, value: value }
                : { success: true };
        }
        ensureUndisposed() {
            if (this.disposed) {
                this.failDisposed();
            }
        }
        failDisposed() {
            throw new UsageError("Accessed a disposed TreeView.");
        }
        /**
         * Updates `this.view` and the current compatibility status.
         * Invoked during initialization and when `this.view` needs to be replaced due to stored schema changes.
         * Handles re-registering for events to call update in the future.
         * @remarks
         * This does not check if the view needs to be replaced, it replaces it unconditionally:
         * callers should do any checking to detect if it's really needed before calling `update`.
         * @privateRemarks
         * This implementation avoids making any edits, which prevents it from being invoked reentrantly.
         * If implicit initialization (or some other edit) is desired, it should be done outside of this method.
         */
        update() {
            this.disposeFlexView();
            const compatibility = this.viewSchema.checkCompatibility(this.checkout.storedSchema);
            this.currentCompatibility = {
                ...compatibility,
                canInitialize: canInitialize(this.checkout),
            };
            const anchors = this.checkout.forest.anchors;
            const slots = anchors.slots;
            if (compatibility.canView) {
                this.flexTreeContext = new Context(defaultSchemaPolicy, this.checkout, this.nodeKeyManager);
                assert(!slots.has(SimpleContextSlot), 0xa47 /* extra simple tree context */);
                assert(this.rootFieldSchema instanceof FieldSchemaAlpha, 0xbfa /* all field schema should be FieldSchemaAlpha */);
                slots.set(SimpleContextSlot, new HydratedContext(this.flexTreeContext, HydratedContext.schemaMapFromRootSchema(this.rootFieldSchema.annotatedAllowedTypesNormalized)));
                // Trigger "rootChanged" events if the root changes in the future.
                {
                    // Currently there is no good way to do this as FlexTreeField has no events for changes.
                    // this.root.on(????)
                    // As a workaround for the above, trigger "rootChanged" in "afterBatch".
                    // Ideally these events would be just events for changes within the root.
                    // TODO: provide a better event: this.view.flexTree.on(????) and/or integrate with with the normal event code paths.
                    // Track what the root was before to be able to detect changes.
                    let lastRoot = this.root;
                    this.flexTreeViewUnregisterCallbacks.add(this.checkout.events.on("afterBatch", () => {
                        // In the initialization flow, this event is raised before the correct compatibility w.r.t the new schema is calculated.
                        // Accessing `this.root` in that case can throw. It's OK to ignore this because:
                        // - The rootChanged event will already be raised at the end of the current upgrade
                        // - It doesn't matter that `lastRoot` isn't updated in this case, because `update` will be called again before the upgrade
                        //   completes (at which point this callback and the `lastRoot` captured here will be out of scope anyway)
                        if (!this.midUpgrade && lastRoot !== this.root) {
                            lastRoot = this.root;
                            this.events.emit("rootChanged");
                        }
                    }));
                }
            }
            this.flexTreeViewUnregisterCallbacks.add(
            // Will dispose the old view (if there is one) when its no longer valid, and create a new one if appropriate.
            this.checkout.storedSchema.events.on("afterSchemaChange", () => this.update()));
            if (!this.midUpgrade) {
                this.events.emit("schemaChanged");
                this.events.emit("rootChanged");
            }
        }
        runSchemaEdit(edit) {
            this.midUpgrade = true;
            try {
                edit();
            }
            finally {
                this.midUpgrade = false;
            }
            this.events.emit("schemaChanged");
            this.events.emit("rootChanged");
        }
        disposeFlexView() {
            const anchors = this.checkout.forest.anchors;
            if (this.flexTreeContext !== undefined) {
                // Cleanup any TreeNodes cached in the AnchorSet when disposing the flex-tree which they wrap.
                for (const anchorNode of anchors) {
                    tryDisposeTreeNode(anchorNode);
                }
                this.flexTreeContext[disposeSymbol]();
                this.flexTreeContext = undefined;
            }
            this.flexTreeViewUnregisterCallbacks.forEach((unregister) => unregister());
            this.flexTreeViewUnregisterCallbacks.clear();
            anchors.slots.delete(SimpleContextSlot);
        }
        get compatibility() {
            if (!this.currentCompatibility) {
                this.failDisposed();
            }
            return this.currentCompatibility;
        }
        dispose() {
            this.disposed = true;
            this.disposeFlexView();
            this.unregisterCallbacks.forEach((unregister) => unregister());
            this.checkout.forest.anchors.slots.delete(ViewSlot);
            this.currentCompatibility = undefined;
            this.onDispose?.();
            if (this.checkout.isBranch && !this.checkout.disposed) {
                // All (non-main) branches are 1:1 with views, so if a user manually disposes a view, we should also dispose the checkout/branch.
                this.checkout.dispose();
            }
        }
        get root() {
            this.breaker.use();
            if (!this.compatibility.canView) {
                throw new UsageError("Document is out of schema. Check TreeView.compatibility before accessing TreeView.root.");
            }
            const view = this.getFlexTreeContext();
            return tryGetTreeNodeForField(view.root);
        }
        set root(newRoot) {
            this.breaker.use();
            if (!this.compatibility.canView) {
                throw new UsageError("Document is out of schema. Check TreeView.compatibility before accessing TreeView.root.");
            }
            const view = this.getFlexTreeContext();
            setField(view.root, this.rootFieldSchema, newRoot, this.checkout.storedSchema.rootFieldSchema);
        }
        // #region Branching
        fork() {
            return this.checkout.branch().viewWith(this.config);
        }
        merge(context, disposeMerged = true) {
            this.checkout.merge(getCheckout(context), disposeMerged);
        }
        rebaseOnto(context) {
            getCheckout(context).rebase(this.checkout);
        }
    };
    __setFunctionName(_classThis, "SchematizingSimpleTreeView");
    (() => {
        const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
        __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
        SchematizingSimpleTreeView = _classThis = _classDescriptor.value;
        if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
        __runInitializers(_classThis, _classExtraInitializers);
    })();
    return SchematizingSimpleTreeView = _classThis;
})();
export { SchematizingSimpleTreeView };
/**
 * Get the {@link TreeCheckout} associated with a given {@link TreeBranch}.
 * @remarks Currently, all contexts are also {@link SchematizingSimpleTreeView}s.
 * Other checkout implementations (e.g. not associated with a view) may be supported in the future.
 */
export function getCheckout(context) {
    if (context instanceof SchematizingSimpleTreeView) {
        return context.checkout;
    }
    throw new UsageError("Unsupported context implementation");
}
/**
 * Adds constraints to a `checkout`'s pending transaction.
 *
 * @param checkout - The checkout's who's transaction will have the constraints added to it.
 * @param constraintsOnRevert - If true, use {@link ISharedTreeEditor.addNodeExistsConstraintOnRevert}.
 * @param constraints - The constraints to add to the transaction.
 *
 * @see {@link RunTransactionParams.preconditions}.
 */
export function addConstraintsToTransaction(checkout, constraintsOnRevert, constraints = []) {
    for (const constraint of constraints) {
        switch (constraint.type) {
            case "nodeInDocument": {
                const node = getOrCreateInnerNode(constraint.node);
                const nodeStatus = getKernel(constraint.node).getStatus();
                if (nodeStatus !== TreeStatus.InDocument) {
                    const revertText = constraintsOnRevert ? " on revert" : "";
                    throw new UsageError(`Attempted to add a "nodeInDocument" constraint${revertText}, but the node is not currently in the document. Node status: ${nodeStatus}`);
                }
                assert(node.isHydrated(), 0xbc2 /* In document node must be hydrated. */);
                if (constraintsOnRevert) {
                    checkout.editor.addNodeExistsConstraintOnRevert(node.anchorNode);
                }
                else {
                    checkout.editor.addNodeExistsConstraint(node.anchorNode);
                }
                break;
            }
            default:
                unreachableCase(constraint.type);
        }
    }
}
//# sourceMappingURL=schematizingTreeView.js.map