/*!
 * 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 } from "@fluidframework/core-utils/internal";
import { UsageError } from "@fluidframework/telemetry-utils/internal";
import { anchorSlot, rootFieldKey } from "../core/index.js";
import { defaultSchemaPolicy, cursorForMapTreeField, Context, combineChunks, FieldKinds, } from "../feature-libraries/index.js";
import { tryGetTreeNodeForField, setField, normalizeFieldSchema, SchemaCompatibilityTester, 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);
            }));
        }
        isBranch() {
            return true;
        }
        applyChange(change) {
            this.checkout.applySerializedChange(change);
        }
        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);
                // This has to be the contextless version, since when "initialize" is called (right after this),
                // it will do a schema change which would dispose of the current context (see inside `update`).
                // Thus using the current context (if any) would hydrate nodes then
                // immediately dispose them instead of having them actually be useable after initialize.
                // For this to work,
                // the hydration must be deferred until after the content is inserted into the tree and the final schema change is done (for required roots),
                // but before any user event could could run.
                const mapTree = prepareForInsertionContextless(content, this.rootFieldSchema, {
                    schema,
                    policy: defaultSchemaPolicy,
                }, this, schema.rootFieldSchema, (batches, doHydration) => {
                    assert(this.pendingHydration === undefined, 0xc74 /* pendingHydration already set */);
                    this.pendingHydration = () => {
                        assert(batches.length <= 1, 0xc75 /* initialize should at most one hydration batch */);
                        for (const batch of batches) {
                            doHydration(batch, {
                                parent: undefined,
                                parentField: rootFieldKey,
                                parentIndex: 0,
                            });
                        }
                    };
                });
                this.runTransaction(() => {
                    initialize(this.checkout, schema, initializerFromChunk(this.checkout, () => {
                        // This must be done after initial schema is set!
                        return combineChunks(this.checkout.forest.chunkField(cursorForMapTreeField(mapTree === undefined ? [] : [mapTree])));
                    }));
                });
            });
        }
        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) {
            this.ensureUndisposed();
            return this.checkout.runTransaction(transaction, params);
        }
        async runTransactionAsync(transaction, params) {
            this.ensureUndisposed();
            if (this.checkout.transaction.size > 0) {
                // breaker.break() sets brokenBy synchronously before throwing.
                // A plain `throw` inside an async function would be captured as a rejected Promise
                // before @breakingClass could set brokenBy. By setting it here first, the
                // subsequent call to unmountTransaction (also @breakingClass-wrapped) will see the
                // broken state and throw synchronously, propagating out of the outer runTransaction
                // to its caller.
                this.breaker.break(new UsageError("An asynchronous transaction cannot be started while another transaction is already in progress."));
            }
            return this.checkout.runTransactionAsync(transaction, params);
        }
        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.allowedTypesFull.evaluate())));
                // 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.
                    // This uses the flex tree root to avoid demanding the simple-tree TreeNode when it might not be hydrated yet.
                    let lastRoot = this.flexTreeContext.root.content;
                    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.flexRoot.content) {
                            lastRoot = this.flexRoot.content;
                            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) {
                assert(this.pendingHydration === undefined, 0xc76 /* no nodes should be pending hydration when triggering events that could access nodes */);
                this.events.emit("schemaChanged");
                this.events.emit("rootChanged");
            }
        }
        runSchemaEdit(edit) {
            this.midUpgrade = true;
            try {
                edit();
            }
            finally {
                this.midUpgrade = false;
            }
            // Ensure hydration is flushed before events run which could access nodes.
            this.pendingHydration?.();
            this.pendingHydration = undefined;
            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;
            }
            for (const unregister of this.flexTreeViewUnregisterCallbacks) {
                unregister();
            }
            this.flexTreeViewUnregisterCallbacks.clear();
            anchors.slots.delete(SimpleContextSlot);
        }
        get compatibility() {
            if (!this.currentCompatibility) {
                this.failDisposed();
            }
            return this.currentCompatibility;
        }
        dispose() {
            this.disposed = true;
            this.disposeFlexView();
            for (const unregister of this.unregisterCallbacks) {
                unregister();
            }
            this.checkout.forest.anchors.slots.delete(ViewSlot);
            this.currentCompatibility = undefined;
            this.onDispose?.();
            if (!this.checkout.isSharedBranch && !this.checkout.disposed) {
                // All non-shared 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 flexRoot() {
            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();
            assert(view.root.is(FieldKinds.optional) ||
                view.root.is(FieldKinds.required) ||
                view.root.is(FieldKinds.identifier), 0xc77 /* unexpected root field kind */);
            return view.root;
        }
        get root() {
            return tryGetTreeNodeForField(this.flexRoot);
        }
        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.fork().viewWith(this.config);
        }
        merge(context, disposeMerged = true) {
            this.checkout.merge(context, disposeMerged);
        }
        rebaseOnto(context) {
            this.checkout.rebaseOnto(context);
        }
    };
    __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 };
//# sourceMappingURL=schematizingTreeView.js.map