/*!
 * 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 { AllowedUpdateType, anchorSlot } from "../core/index.js";
import { defaultSchemaPolicy, ContextSlot, cursorForMapTreeNode, TreeStatus, } from "../feature-libraries/index.js";
import { getTreeNodeForField, setField, normalizeFieldSchema, ViewSchema, mapTreeFromNodeData, prepareContentForHydration, comparePersistedSchemaInternal, toStoredSchema, getOrCreateInnerNode, getKernel, HydratedContext, SimpleContextSlot, areImplicitFieldSchemaEqual, createUnknownOptionalFieldPolicy, } from "../simple-tree/index.js";
import { breakingClass, disposeSymbol, } from "../util/index.js";
import { canInitialize, ensureSchema, initialize } from "./schematizeTree.js";
import { CheckoutFlexTreeView } from "./checkoutFlexTreeView.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();
            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);
            this.schemaPolicy = {
                ...defaultSchemaPolicy,
                validateSchema: config.enableSchemaValidation,
                allowUnknownOptionalFields: createUnknownOptionalFieldPolicy(this.rootFieldSchema),
            };
            this.viewSchema = new ViewSchema(this.schemaPolicy, {}, this.rootFieldSchema);
            // 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 mapTree = mapTreeFromNodeData(content, this.rootFieldSchema, this.nodeKeyManager, {
                    schema: this.checkout.storedSchema,
                    policy: this.schemaPolicy,
                });
                prepareContentForHydration(mapTree, this.checkout.forest);
                initialize(this.checkout, {
                    schema: toStoredSchema(this.viewSchema.schema),
                    initialTree: mapTree === undefined ? undefined : cursorForMapTreeNode(mapTree),
                });
            });
        }
        upgradeSchema() {
            this.ensureUndisposed();
            const compatibility = this.compatibility;
            if (compatibility.isEquivalent) {
                // No-op
                return;
            }
            if (!compatibility.canUpgrade) {
                throw new UsageError("Existing stored schema can not be upgraded (see TreeView.compatibility.canUpgrade).");
            }
            this.runSchemaEdit(() => {
                const result = ensureSchema(this.viewSchema, AllowedUpdateType.SchemaCompatible, this.checkout, {
                    schema: toStoredSchema(this.viewSchema.schema),
                    initialTree: undefined,
                });
                assert(result, 0x8bf /* Schema upgrade should always work if canUpgrade is set. */);
            });
        }
        /**
         * Gets the view. Throws when disposed.
         */
        getView() {
            this.ensureUndisposed();
            assert(this.view !== undefined, 0x8c0 /* unexpected getViewOrError */);
            return this.view;
        }
        runTransaction(transaction, params) {
            const addConstraints = (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}`);
                            }
                            if (constraintsOnRevert) {
                                this.checkout.editor.addNodeExistsConstraintOnRevert(node.anchorNode);
                            }
                            else {
                                this.checkout.editor.addNodeExistsConstraint(node.anchorNode);
                            }
                            break;
                        }
                        default:
                            unreachableCase(constraint.type);
                    }
                }
            };
            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.disposeView();
            const compatibility = comparePersistedSchemaInternal(this.checkout.storedSchema, this.viewSchema, canInitialize(this.checkout));
            let lastRoot = this.compatibility.canView && this.view !== undefined ? this.root : undefined;
            this.currentCompatibility = compatibility;
            if (compatibility.canView) {
                // Trigger "rootChanged" if the root changes in the future.
                // Currently there is no good way to do this as FlexTreeField has no events for changes.
                // this.view.flexTree.on(????)
                // As a workaround for the above, trigger "rootChanged" in "afterBatch"
                // which isn't the correct time since we normally do events during the batch when the forest is modified, but its better than nothing.
                // TODO: provide a better event: this.view.flexTree.on(????)
                const cleanupCheckOutEvents = 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");
                    }
                });
                const onViewDispose = () => {
                    cleanupCheckOutEvents();
                    this.view = undefined;
                    if (!this.disposed) {
                        this.update();
                    }
                };
                const view = requireSchema(this.checkout, this.viewSchema, onViewDispose, this.nodeKeyManager, this.schemaPolicy);
                this.view = view;
                assert(!this.checkout.forest.anchors.slots.has(SimpleContextSlot), 0xa47 /* extra simple tree context */);
                this.checkout.forest.anchors.slots.set(SimpleContextSlot, new HydratedContext(this.rootFieldSchema.allowedTypeSet, view.context));
                const unregister = this.checkout.storedSchema.events.on("afterSchemaChange", () => {
                    unregister();
                    this.unregisterCallbacks.delete(unregister);
                    view[disposeSymbol]();
                });
                this.unregisterCallbacks.add(unregister);
            }
            else {
                this.view = undefined;
                this.checkout.forest.anchors.slots.delete(SimpleContextSlot);
                const unregister = this.checkout.storedSchema.events.on("afterSchemaChange", () => {
                    unregister();
                    this.unregisterCallbacks.delete(unregister);
                    this.update();
                });
                this.unregisterCallbacks.add(unregister);
            }
            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");
        }
        disposeView() {
            if (this.view !== undefined) {
                this.view[disposeSymbol]();
                this.view = undefined;
                this.checkout.forest.anchors.slots.delete(SimpleContextSlot);
                this.unregisterCallbacks.forEach((unregister) => unregister());
            }
            this.checkout.forest.anchors.slots.delete(SimpleContextSlot);
        }
        get compatibility() {
            if (!this.currentCompatibility) {
                this.failDisposed();
            }
            return this.currentCompatibility;
        }
        dispose() {
            this.disposed = true;
            this.disposeView();
            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.getView();
            return getTreeNodeForField(view.flexTree);
        }
        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.getView();
            setField(view.context.root, this.rootFieldSchema, newRoot);
        }
        // #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");
}
/**
 * Creates a view that self-disposes whenenever the stored schema changes.
 * This may only be called when the schema is already known to be compatible (typically via ensureSchema).
 */
export function requireSchema(checkout, viewSchema, onDispose, nodeKeyManager, schemaPolicy) {
    const slots = checkout.forest.anchors.slots;
    assert(!slots.has(ContextSlot), 0x8c2 /* Cannot create second view from checkout */);
    {
        const compatibility = viewSchema.checkCompatibility(checkout.storedSchema);
        assert(compatibility.canView, 0x8c3 /* requireSchema invoked with incompatible schema */);
    }
    const view = new CheckoutFlexTreeView(checkout, schemaPolicy, nodeKeyManager, onDispose);
    assert(slots.has(ContextSlot), 0x90d /* Context should be tracked in slot */);
    return view;
}
//# sourceMappingURL=schematizingTreeView.js.map