/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { assert } from "@fluidframework/core-utils/internal";
import { UsageError } from "@fluidframework/telemetry-utils/internal";
import { AllowedUpdateType, Compatibility } from "../core/index.js";
import { createEmitter, } from "../events/index.js";
import { ViewSchema, defaultSchemaPolicy, ContextSlot, } from "../feature-libraries/index.js";
import { getProxyForField, toFlexConfig, setField, normalizeFieldSchema, cursorFromUnhydratedRoot, } from "../simple-tree/index.js";
import { disposeSymbol } from "../util/index.js";
import { canInitialize, ensureSchema, initialize, } from "./schematizeTree.js";
import { CheckoutFlexTreeView } from "./treeView.js";
/**
 * Implementation of TreeView wrapping a FlexTreeView.
 */
export class SchematizingSimpleTreeView {
    constructor(checkout, config, nodeKeyManager) {
        this.checkout = checkout;
        this.config = config;
        this.nodeKeyManager = nodeKeyManager;
        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;
        const policy = {
            ...defaultSchemaPolicy,
            validateSchema: config.enableSchemaValidation,
        };
        this.rootFieldSchema = normalizeFieldSchema(config.schema);
        this.flexConfig = toFlexConfig(config, nodeKeyManager, {
            schema: checkout.storedSchema,
            policy,
        });
        this.viewSchema = new ViewSchema(policy, {}, this.flexConfig.schema);
        // 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("commitApplied", (data, getRevertible) => this.events.emit("commitApplied", data, getRevertible)));
    }
    initialize(content) {
        this.ensureUndisposed();
        const compatibility = this.compatibility;
        if (!compatibility.canInitialize) {
            throw new UsageError("Tree cannot be initialized more than once.");
        }
        this.runSchemaEdit(() => {
            initialize(this.checkout, {
                schema: this.flexConfig.schema,
                initialTree: content === undefined
                    ? undefined
                    : cursorFromUnhydratedRoot(this.config.schema, content, this.nodeKeyManager),
            });
        });
    }
    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, 
            // eslint-disable-next-line no-bitwise
            AllowedUpdateType.SchemaCompatible | AllowedUpdateType.Initialize, this.checkout, this.flexConfig);
            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;
    }
    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 result = this.viewSchema.checkCompatibility(this.checkout.storedSchema);
        // TODO: AB#8121: Weaken this check to support viewing under additional circumstances.
        // In the near term, this should support viewing documents with additional optional fields in their schema on object types.
        // Longer-term (as demand arises), we could also add APIs to constructing view schema to allow for more flexibility
        // (e.g. out-of-schema content handlers could allow support for viewing docs which have extra allowed types in a particular field)
        const canView = result.write === Compatibility.Compatible && result.read === Compatibility.Compatible;
        const canUpgrade = result.read === Compatibility.Compatible;
        const isEquivalent = canView && canUpgrade;
        const compatibility = {
            canView,
            canUpgrade,
            isEquivalent,
            canInitialize: 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();
                }
            };
            this.view = requireSchema(this.checkout, this.viewSchema, onViewDispose, this.nodeKeyManager);
        }
        else {
            this.view = undefined;
            const unregister = this.checkout.storedSchema.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.unregisterCallbacks.forEach((unregister) => unregister());
        }
    }
    get compatibility() {
        if (!this.currentCompatibility) {
            this.failDisposed();
        }
        return this.currentCompatibility;
    }
    dispose() {
        this.disposed = true;
        this.disposeView();
        this.currentCompatibility = undefined;
    }
    get root() {
        if (!this.compatibility.canView) {
            throw new UsageError("Document is out of schema. Check TreeView.compatibility before accessing TreeView.root.");
        }
        const view = this.getView();
        return getProxyForField(view.flexTree);
    }
    set root(newRoot) {
        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);
    }
}
/**
 * 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) {
    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.write === Compatibility.Compatible &&
            compatibility.read === Compatibility.Compatible, 0x8c3 /* requireSchema invoked with incompatible schema */);
    }
    const view = new CheckoutFlexTreeView(checkout, viewSchema.schema, nodeKeyManager, onDispose);
    assert(slots.has(ContextSlot), 0x90d /* Context should be tracked in slot */);
    const unregister = checkout.storedSchema.on("afterSchemaChange", () => {
        unregister();
        view[disposeSymbol]();
    });
    return view;
}
//# sourceMappingURL=schematizingTreeView.js.map