/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { assert, unreachableCase } from "@fluidframework/core-utils";
import { UsageError } from "@fluidframework/telemetry-utils";
import { AllowedUpdateType, anchorSlot, Compatibility } from "../core/index.js";
import { defaultSchemaPolicy, ViewSchema, } from "../feature-libraries/index.js";
import { createEmitter } from "../events/index.js";
import { disposeSymbol } from "../util/index.js";
import { toFlexConfig, getProxyForField, } from "../simple-tree/index.js";
import { UpdateType, ensureSchema, evaluateUpdate } from "./schematizeTree.js";
import { CheckoutFlexTreeView } from "./treeView.js";
/**
 * Implementation of TreeView wrapping a FlexTreeView.
 */
export class SchematizingSimpleTreeView {
    constructor(checkout, config, nodeKeyManager, nodeKeyFieldKey) {
        this.checkout = checkout;
        this.config = config;
        this.nodeKeyManager = nodeKeyManager;
        this.nodeKeyFieldKey = nodeKeyFieldKey;
        this.events = createEmitter();
        this.disposed = false;
        this.flexConfig = toFlexConfig(config);
        this.viewSchema = new ViewSchema(defaultSchemaPolicy, {}, this.flexConfig.schema);
        this.update();
    }
    upgradeSchema() {
        // Errors if disposed.
        const error = this.error;
        // No-op non error state.
        if (error === undefined) {
            return;
        }
        if (this.error?.canUpgrade !== true) {
            throw new UsageError("Existing stored schema can not be upgraded (see TreeView.canUpgrade).");
        }
        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 is the stored schema is compatible with the view schema,
     * otherwise returns an error detailing the schema incompatibility.
     */
    getViewOrError() {
        if (this.disposed) {
            throw new UsageError("Accessed a disposed TreeView.");
        }
        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 = evaluateUpdate(this.viewSchema, 
        // eslint-disable-next-line no-bitwise
        AllowedUpdateType.SchemaCompatible | AllowedUpdateType.Initialize, this.checkout);
        this.disposeView();
        switch (compatibility) {
            case UpdateType.None: {
                // Remove event from checkout when view is disposed
                this.view = requireSchema(this.checkout, this.viewSchema, () => {
                    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 UpdateType.Incompatible:
            case UpdateType.Initialize:
            case UpdateType.SchemaCompatible: {
                this.view = new SchematizeError(compatibility);
                const unregister = this.checkout.storedSchema.on("afterSchemaChange", () => {
                    unregister();
                    this.update();
                });
                break;
            }
            default: {
                unreachableCase(compatibility);
            }
        }
        this.events.emit("rootChanged");
    }
    disposeView() {
        if (this.view !== undefined && !(this.view instanceof SchematizeError)) {
            this.view[disposeSymbol]();
            this.view = undefined;
        }
    }
    get error() {
        const view = this.getViewOrError();
        return view instanceof SchematizeError ? view : undefined;
    }
    [disposeSymbol]() {
        this.getViewOrError();
        this.disposed = true;
        this.disposeView();
    }
    get root() {
        const view = this.getViewOrError();
        if (view instanceof SchematizeError) {
            throw new UsageError("Document is out of schema. Check TreeView.error before accessing TreeView.root.");
        }
        return getProxyForField(view.flexTree);
    }
}
export class SchematizeError {
    constructor(updateType) {
        this.updateType = updateType;
    }
    get canUpgrade() {
        return (this.updateType === UpdateType.Initialize ||
            this.updateType === UpdateType.SchemaCompatible);
    }
    get canInitialize() {
        return this.updateType === UpdateType.Initialize;
    }
}
/**
 * Creating multiple flex tree contexts for the same branch, and thus with the same underlying AnchorSet does not work due to how TreeNode caching works.
 * This slot is used to detect if one already exists and error if creating a second.
 *
 * TODO:
 * 1. API docs need to reflect this limitation or the limitation has to be removed.
 */
const ViewSlot = anchorSlot();
/**
 * 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).
 */
export function requireSchema(checkout, viewSchema, onDispose, nodeKeyManager, nodeKeyFieldKey) {
    const slots = checkout.forest.anchors.slots;
    assert(!slots.has(ViewSlot), 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, nodeKeyFieldKey, () => {
        const deleted = slots.delete(ViewSlot);
        assert(deleted, 0x8c4 /* unexpected dispose */);
        onDispose();
    });
    assert(!slots.has(ViewSlot), 0x8c5 /* Cannot create second view from checkout */);
    slots.set(ViewSlot, view);
    const unregister = checkout.storedSchema.on("afterSchemaChange", () => {
        const compatibility = viewSchema.checkCompatibility(checkout.storedSchema);
        if (compatibility.write !== Compatibility.Compatible ||
            compatibility.read !== Compatibility.Compatible) {
            unregister();
            view[disposeSymbol]();
        }
    });
    return view;
}
//# sourceMappingURL=schematizingTreeView.js.map