"use strict";
/*!
 * 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 });
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.addConstraintsToTransaction = exports.requireSchema = exports.getCheckout = exports.SchematizingSimpleTreeView = exports.ViewSlot = void 0;
const client_utils_1 = require("@fluid-internal/client-utils");
const internal_1 = require("@fluidframework/core-utils/internal");
const internal_2 = require("@fluidframework/telemetry-utils/internal");
const index_js_1 = require("../core/index.js");
const index_js_2 = require("../feature-libraries/index.js");
const index_js_3 = require("../simple-tree/index.js");
const index_js_4 = require("../util/index.js");
const checkoutFlexTreeView_js_1 = require("./checkoutFlexTreeView.js");
const schematizeTree_js_1 = require("./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.
 */
exports.ViewSlot = (0, index_js_1.anchorSlot)();
/**
 * Implementation of TreeView wrapping a FlexTreeView.
 */
let SchematizingSimpleTreeView = (() => {
    let _classDecorators = [index_js_4.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 = (0, client_utils_1.createEmitter)();
            /**
             * 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(exports.ViewSlot)) {
                throw new internal_2.UsageError("Cannot create a second tree view from the same checkout");
            }
            checkout.forest.anchors.slots.set(exports.ViewSlot, this);
            this.rootFieldSchema = (0, index_js_3.normalizeFieldSchema)(config.schema);
            this.schemaPolicy = {
                ...index_js_2.defaultSchemaPolicy,
                validateSchema: config.enableSchemaValidation,
                allowUnknownOptionalFields: (0, index_js_3.createUnknownOptionalFieldPolicy)(this.rootFieldSchema),
            };
            this.viewSchema = new index_js_3.SchemaCompatibilityTester(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 (0, index_js_3.areImplicitFieldSchemaEqual)(this.rootFieldSchema, schema);
        }
        get schema() {
            return this.config.schema;
        }
        initialize(content) {
            this.ensureUndisposed();
            const compatibility = this.compatibility;
            if (!compatibility.canInitialize) {
                throw new internal_2.UsageError("Tree cannot be initialized more than once.");
            }
            this.runSchemaEdit(() => {
                const schema = this.viewSchema.viewSchemaAsStored;
                const mapTree = (0, index_js_3.prepareForInsertionContextless)(content, this.rootFieldSchema, {
                    schema,
                    policy: this.schemaPolicy,
                }, this);
                (0, schematizeTree_js_1.initialize)(this.checkout, {
                    schema,
                    initialTree: mapTree === undefined ? undefined : (0, index_js_2.cursorForMapTreeNode)(mapTree),
                });
            });
        }
        upgradeSchema() {
            this.ensureUndisposed();
            const compatibility = this.compatibility;
            if (compatibility.isEquivalent) {
                // No-op
                return;
            }
            if (!compatibility.canUpgrade) {
                throw new internal_2.UsageError("Existing stored schema can not be upgraded (see TreeView.compatibility.canUpgrade).");
            }
            this.runSchemaEdit(() => {
                const result = (0, schematizeTree_js_1.ensureSchema)(this.viewSchema, this.checkout);
                (0, internal_1.assert)(result, 0x8bf /* Schema upgrade should always work if canUpgrade is set. */);
            });
        }
        /**
         * Gets the view. Throws when disposed.
         */
        getView() {
            this.ensureUndisposed();
            (0, internal_1.assert)(this.view !== undefined, 0x8c0 /* unexpected getViewOrError */);
            return this.view;
        }
        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 internal_2.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 = this.viewSchema.checkCompatibility(this.checkout.storedSchema);
            let lastRoot = this.compatibility.canView && this.view !== undefined ? this.root : undefined;
            this.currentCompatibility = {
                ...compatibility,
                canInitialize: (0, schematizeTree_js_1.canInitialize)(this.checkout),
            };
            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;
                (0, internal_1.assert)(!this.checkout.forest.anchors.slots.has(index_js_3.SimpleContextSlot), 0xa47 /* extra simple tree context */);
                this.checkout.forest.anchors.slots.set(index_js_3.SimpleContextSlot, new index_js_3.HydratedContext((0, index_js_3.normalizeFieldSchema)(this.rootFieldSchema).annotatedAllowedTypesNormalized, view.context));
                const unregister = this.checkout.storedSchema.events.on("afterSchemaChange", () => {
                    unregister();
                    this.unregisterCallbacks.delete(unregister);
                    view[index_js_4.disposeSymbol]();
                });
                this.unregisterCallbacks.add(unregister);
            }
            else {
                this.view = undefined;
                this.checkout.forest.anchors.slots.delete(index_js_3.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[index_js_4.disposeSymbol]();
                this.view = undefined;
                this.checkout.forest.anchors.slots.delete(index_js_3.SimpleContextSlot);
                this.unregisterCallbacks.forEach((unregister) => unregister());
            }
            this.checkout.forest.anchors.slots.delete(index_js_3.SimpleContextSlot);
        }
        get compatibility() {
            if (!this.currentCompatibility) {
                this.failDisposed();
            }
            return this.currentCompatibility;
        }
        dispose() {
            this.disposed = true;
            this.disposeView();
            this.checkout.forest.anchors.slots.delete(exports.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 internal_2.UsageError("Document is out of schema. Check TreeView.compatibility before accessing TreeView.root.");
            }
            const view = this.getView();
            return (0, index_js_3.tryGetTreeNodeForField)(view.flexTree);
        }
        set root(newRoot) {
            this.breaker.use();
            if (!this.compatibility.canView) {
                throw new internal_2.UsageError("Document is out of schema. Check TreeView.compatibility before accessing TreeView.root.");
            }
            const view = this.getView();
            (0, index_js_3.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;
})();
exports.SchematizingSimpleTreeView = 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.
 */
function getCheckout(context) {
    if (context instanceof SchematizingSimpleTreeView) {
        return context.checkout;
    }
    throw new internal_2.UsageError("Unsupported context implementation");
}
exports.getCheckout = getCheckout;
/**
 * Creates a view that self-disposes whenever the stored schema changes.
 * This may only be called when the schema is already known to be compatible (typically via ensureSchema).
 */
function requireSchema(checkout, viewSchema, onDispose, nodeKeyManager, schemaPolicy) {
    const slots = checkout.forest.anchors.slots;
    (0, internal_1.assert)(!slots.has(index_js_2.ContextSlot), 0x8c2 /* Cannot create second view from checkout */);
    {
        const compatibility = viewSchema.checkCompatibility(checkout.storedSchema);
        (0, internal_1.assert)(compatibility.canView, 0x8c3 /* requireSchema invoked with incompatible schema */);
    }
    const view = new checkoutFlexTreeView_js_1.CheckoutFlexTreeView(checkout, schemaPolicy, nodeKeyManager, onDispose);
    (0, internal_1.assert)(slots.has(index_js_2.ContextSlot), 0x90d /* Context should be tracked in slot */);
    return view;
}
exports.requireSchema = requireSchema;
/**
 * 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}.
 */
function addConstraintsToTransaction(checkout, constraintsOnRevert, constraints = []) {
    for (const constraint of constraints) {
        switch (constraint.type) {
            case "nodeInDocument": {
                const node = (0, index_js_3.getOrCreateInnerNode)(constraint.node);
                const nodeStatus = (0, index_js_3.getKernel)(constraint.node).getStatus();
                if (nodeStatus !== index_js_2.TreeStatus.InDocument) {
                    const revertText = constraintsOnRevert ? " on revert" : "";
                    throw new internal_2.UsageError(`Attempted to add a "nodeInDocument" constraint${revertText}, but the node is not currently in the document. Node status: ${nodeStatus}`);
                }
                (0, internal_1.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:
                (0, internal_1.unreachableCase)(constraint.type);
        }
    }
}
exports.addConstraintsToTransaction = addConstraintsToTransaction;
//# sourceMappingURL=schematizingTreeView.js.map