"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.requireSchema = exports.SchematizeError = exports.SchematizingSimpleTreeView = void 0;
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("../events/index.js");
const index_js_3 = require("../feature-libraries/index.js");
const index_js_4 = require("../simple-tree/index.js");
const index_js_5 = require("../util/index.js");
const schematizeTree_js_1 = require("./schematizeTree.js");
const treeView_js_1 = require("./treeView.js");
/**
 * Implementation of TreeView wrapping a FlexTreeView.
 */
class SchematizingSimpleTreeView {
    constructor(checkout, config, nodeKeyManager, nodeKeyFieldKey) {
        this.checkout = checkout;
        this.config = config;
        this.nodeKeyManager = nodeKeyManager;
        this.nodeKeyFieldKey = nodeKeyFieldKey;
        this.events = (0, index_js_2.createEmitter)();
        this.unregisterCallbacks = new Set();
        this.disposed = false;
        this.rootFieldSchema = (0, index_js_4.normalizeFieldSchema)(config.schema);
        this.flexConfig = (0, index_js_4.toFlexConfig)(config);
        this.viewSchema = new index_js_3.ViewSchema(index_js_3.defaultSchemaPolicy, {}, this.flexConfig.schema);
        this.update();
        this.unregisterCallbacks.add(this.checkout.events.on("commitApplied", (data, getRevertible) => this.events.emit("commitApplied", data, getRevertible)));
    }
    upgradeSchema() {
        // Errors if disposed.
        const error = this.error;
        // No-op non error state.
        if (error === undefined) {
            return;
        }
        if (this.error?.canUpgrade !== true) {
            throw new internal_2.UsageError("Existing stored schema can not be upgraded (see TreeView.canUpgrade).");
        }
        const result = (0, schematizeTree_js_1.ensureSchema)(this.viewSchema, 
        // eslint-disable-next-line no-bitwise
        index_js_1.AllowedUpdateType.SchemaCompatible | index_js_1.AllowedUpdateType.Initialize, this.checkout, this.flexConfig);
        (0, internal_1.assert)(result, 0x8bf /* Schema upgrade should always work if canUpgrade is set. */);
    }
    /**
     * Gets the view is the stored schema is compatible with the view schema,
     * otherwise returns an error detailing the schema incompatibility.
     */
    getViewOrError() {
        if (this.disposed) {
            throw new internal_2.UsageError("Accessed a disposed TreeView.");
        }
        (0, internal_1.assert)(this.view !== undefined, 0x8c0 /* unexpected getViewOrError */);
        return this.view;
    }
    /**
     * Updates `this.view`.
     * Invoked during initialization and when `this.view` needs to be replaced due to stored schema changes.
     * Handled 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 its really needed before calling `update`.
     */
    update() {
        // 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.
        const compatibility = (0, schematizeTree_js_1.evaluateUpdate)(this.viewSchema, 
        // eslint-disable-next-line no-bitwise
        index_js_1.AllowedUpdateType.SchemaCompatible | index_js_1.AllowedUpdateType.Initialize, this.checkout);
        this.disposeView();
        switch (compatibility) {
            case schematizeTree_js_1.UpdateType.None: {
                // Remove event from checkout when view is disposed
                this.view = requireSchema(this.checkout, this.viewSchema, () => {
                    (0, internal_1.assert)(cleanupCheckOutEvents !== undefined, 0x8c1 /* missing cleanup */);
                    cleanupCheckOutEvents();
                    this.view = undefined;
                    if (!this.disposed) {
                        this.update();
                    }
                }, this.nodeKeyManager, this.nodeKeyFieldKey);
                // 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(????)
                let lastRoot = this.root;
                const cleanupCheckOutEvents = this.checkout.events.on("afterBatch", () => {
                    if (lastRoot !== this.root) {
                        lastRoot = this.root;
                        this.events.emit("rootChanged");
                    }
                });
                break;
            }
            case schematizeTree_js_1.UpdateType.Incompatible:
            case schematizeTree_js_1.UpdateType.Initialize:
            case schematizeTree_js_1.UpdateType.SchemaCompatible: {
                this.view = new SchematizeError(compatibility);
                const unregister = this.checkout.storedSchema.on("afterSchemaChange", () => {
                    unregister();
                    this.unregisterCallbacks.delete(unregister);
                    this.update();
                });
                this.unregisterCallbacks.add(unregister);
                break;
            }
            default: {
                (0, internal_1.unreachableCase)(compatibility);
            }
        }
        this.events.emit("rootChanged");
    }
    disposeView() {
        if (this.view !== undefined && !(this.view instanceof SchematizeError)) {
            this.view[index_js_5.disposeSymbol]();
            this.view = undefined;
            this.unregisterCallbacks.forEach((unregister) => unregister());
        }
    }
    get error() {
        const view = this.getViewOrError();
        return view instanceof SchematizeError ? view : undefined;
    }
    [index_js_5.disposeSymbol]() {
        this.getViewOrError();
        this.disposed = true;
        this.disposeView();
    }
    get root() {
        const view = this.getViewOrError();
        if (view instanceof SchematizeError) {
            throw new internal_2.UsageError("Document is out of schema. Check TreeView.error before accessing TreeView.root.");
        }
        return (0, index_js_4.getProxyForField)(view.flexTree);
    }
    set root(newRoot) {
        const view = this.getViewOrError();
        if (view instanceof SchematizeError) {
            throw new internal_2.UsageError("Document is out of schema. Check TreeView.error before accessing TreeView.root.");
        }
        (0, index_js_4.setField)(view.context.root, this.rootFieldSchema, newRoot);
    }
}
exports.SchematizingSimpleTreeView = SchematizingSimpleTreeView;
class SchematizeError {
    constructor(updateType) {
        this.updateType = updateType;
    }
    get canUpgrade() {
        return (this.updateType === schematizeTree_js_1.UpdateType.Initialize ||
            this.updateType === schematizeTree_js_1.UpdateType.SchemaCompatible);
    }
    get canInitialize() {
        return this.updateType === schematizeTree_js_1.UpdateType.Initialize;
    }
}
exports.SchematizeError = SchematizeError;
/**
 * Flex-Tree schematizing layer.
 * Creates a view that self-disposes when stored schema becomes incompatible.
 * This may only be called when the schema is already known to be compatible (typically via ensureSchema).
 */
function requireSchema(checkout, viewSchema, onDispose, nodeKeyManager, nodeKeyFieldKey) {
    const slots = checkout.forest.anchors.slots;
    (0, internal_1.assert)(!slots.has(index_js_3.ContextSlot), 0x8c2 /* Cannot create second view from checkout */);
    {
        const compatibility = viewSchema.checkCompatibility(checkout.storedSchema);
        (0, internal_1.assert)(compatibility.write === index_js_1.Compatibility.Compatible &&
            compatibility.read === index_js_1.Compatibility.Compatible, 0x8c3 /* requireSchema invoked with incompatible schema */);
    }
    const view = new treeView_js_1.CheckoutFlexTreeView(checkout, viewSchema.schema, nodeKeyManager, nodeKeyFieldKey, onDispose);
    (0, internal_1.assert)(slots.has(index_js_3.ContextSlot), 0x90d /* Context should be tracked in slot */);
    const unregister = checkout.storedSchema.on("afterSchemaChange", () => {
        const compatibility = viewSchema.checkCompatibility(checkout.storedSchema);
        if (compatibility.write !== index_js_1.Compatibility.Compatible ||
            compatibility.read !== index_js_1.Compatibility.Compatible) {
            unregister();
            view[index_js_5.disposeSymbol]();
        }
    });
    return view;
}
exports.requireSchema = requireSchema;
//# sourceMappingURL=schematizingTreeView.js.map