/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { assert, unreachableCase } from "@fluidframework/core-utils/internal";
import { AllowedUpdateType, Compatibility, rootFieldKey, schemaDataIsEmpty, } from "../core/index.js";
import { FieldKinds, allowsRepoSuperset, defaultSchemaPolicy, intoStoredSchema, normalizeNewFieldContent, } from "../feature-libraries/index.js";
import { fail } from "../util/index.js";
/**
 * Modify `storedSchema` and invoke `setInitialTree` when it's time to set the tree content.
 *
 * Requires `storedSchema` to be in its default/empty state.
 *
 * This is done in such a way that if the content (implicitly assumed to start empty)
 * is never out of schema.
 * This means that if the root field of the new schema requires content (like a value field),
 * a temporary intermediate schema is used so the initial empty state is not out of schema.
 *
 * Since this makes multiple changes, callers may want to wrap it in a transaction.
 */
export function initializeContent(schemaRepository, newSchema, setInitialTree) {
    assert(schemaDataIsEmpty(schemaRepository.storedSchema), 0x743 /* cannot initialize after a schema is set */);
    const schema = intoStoredSchema(newSchema);
    const rootSchema = schema.rootFieldSchema;
    const rootKind = rootSchema.kind;
    // To keep the data in schema during the update, first define a schema that tolerates the current (empty) tree as well as the final (initial) tree.
    let incrementalSchemaUpdate;
    if (rootKind === FieldKinds.sequence.identifier ||
        rootKind === FieldKinds.optional.identifier) {
        // These kinds are known to tolerate empty, so use the schema as is:
        incrementalSchemaUpdate = schema;
    }
    else {
        assert(rootKind === FieldKinds.required.identifier, 0x5c8 /* Unexpected kind */);
        // Replace value kind with optional kind in root field schema:
        incrementalSchemaUpdate = {
            nodeSchema: schema.nodeSchema,
            rootFieldSchema: {
                kind: FieldKinds.optional.identifier,
                types: rootSchema.types,
            },
        };
    }
    // TODO: fix issues with schema comparison and enable this.
    // assert(
    // 	allowsRepoSuperset(defaultSchemaPolicy, tree.storedSchema, incrementalSchemaUpdate),
    // 	"Incremental Schema update should support the existing empty tree",
    // );
    assert(allowsRepoSuperset(defaultSchemaPolicy, schema, incrementalSchemaUpdate), 0x5c9 /* Incremental Schema during update should be a allow a superset of the final schema */);
    // Update to intermediate schema
    schemaRepository.updateSchema(incrementalSchemaUpdate);
    // Insert initial tree
    setInitialTree();
    // If intermediate schema is not final desired schema, update to the final schema:
    if (incrementalSchemaUpdate !== schema) {
        schemaRepository.updateSchema(schema);
    }
}
export var UpdateType;
(function (UpdateType) {
    /**
     * Already compatible, no update needed.
     */
    UpdateType[UpdateType["None"] = 0] = "None";
    /**
     * Empty: needs initializing.
     */
    UpdateType[UpdateType["Initialize"] = 1] = "Initialize";
    /**
     * Schema can be upgraded leaving tree as is.
     */
    UpdateType[UpdateType["SchemaCompatible"] = 2] = "SchemaCompatible";
    /**
     * No update currently supported.
     */
    UpdateType[UpdateType["Incompatible"] = 3] = "Incompatible";
})(UpdateType || (UpdateType = {}));
export function evaluateUpdate(viewSchema, allowedSchemaModifications, checkout) {
    const compatibility = viewSchema.checkCompatibility(checkout.storedSchema);
    if (compatibility.read === Compatibility.Compatible &&
        compatibility.write === Compatibility.Compatible) {
        // Compatible as is
        return UpdateType.None;
    }
    // eslint-disable-next-line no-bitwise
    if (allowedSchemaModifications & AllowedUpdateType.Initialize && canInitialize(checkout)) {
        return UpdateType.Initialize;
    }
    if (compatibility.read !== Compatibility.Compatible) {
        // Existing stored schema permits trees which are incompatible with the view schema, so schema can not be updated
        return UpdateType.Incompatible;
    }
    assert(compatibility.write === Compatibility.Incompatible, 0x8bd /* unexpected case */);
    assert(compatibility.read === Compatibility.Compatible, 0x8be /* unexpected case */);
    // eslint-disable-next-line no-bitwise
    return allowedSchemaModifications & AllowedUpdateType.SchemaCompatible
        ? UpdateType.SchemaCompatible
        : UpdateType.Incompatible;
}
export function canInitialize(checkout) {
    // Check for empty.
    return checkout.forest.isEmpty && schemaDataIsEmpty(checkout.storedSchema);
}
/**
 * Initialize a checkout with a schema and tree content.
 * This function should only be called when the tree is uninitialized (no schema or content).
 * @remarks
 *
 * If the proposed schema (from `treeContent`) is not compatible with the emptry tree, this function handles using an intermediate schema
 * which supports the empty tree as well as the final tree content.
 */
export function initialize(checkout, treeContent) {
    checkout.transaction.start();
    try {
        initializeContent(checkout, treeContent.schema, () => {
            const field = { field: rootFieldKey, parent: undefined };
            const content = normalizeNewFieldContent({ schema: treeContent.schema }, treeContent.schema.rootFieldSchema, treeContent.initialTree);
            switch (checkout.storedSchema.rootFieldSchema.kind) {
                case FieldKinds.optional.identifier: {
                    const fieldEditor = checkout.editor.optionalField(field);
                    assert(content.getFieldLength() <= 1, 0x7f4 /* optional field content should normalize at most one item */);
                    fieldEditor.set(content.getFieldLength() === 0 ? undefined : content, true);
                    break;
                }
                case FieldKinds.sequence.identifier: {
                    const fieldEditor = checkout.editor.sequenceField(field);
                    // TODO: should do an idempotent edit here.
                    fieldEditor.insert(0, content);
                    break;
                }
                default: {
                    fail("unexpected root field kind during initialize");
                }
            }
        });
    }
    finally {
        checkout.transaction.commit();
    }
}
/**
 * Ensure a {@link ITreeCheckout} can be used with a given {@link ViewSchema}.
 *
 * @remarks
 * It is up to the caller to ensure that compatibility is reevaluated if the checkout's stored schema is edited in the future.
 *
 * @param viewSchema - View schema that `checkout` should be made compatible with.
 * @param allowedSchemaModifications - Flags enum describing the ways this is allowed to modify `checkout`.
 * @param checkout - To be modified as needed to be compatible with `viewSchema`.
 * @param treeContent - Content to be used to initialize `checkout`'s the tree if needed and allowed.
 * @returns true iff checkout now is compatible with `viewSchema`.
 */
export function ensureSchema(viewSchema, allowedSchemaModifications, checkout, treeContent) {
    let possibleModifications = allowedSchemaModifications;
    if (treeContent === undefined) {
        // Clear bit for Initialize if initial tree is not provided.
        // eslint-disable-next-line no-bitwise
        possibleModifications &= ~AllowedUpdateType.Initialize;
    }
    const updatedNeeded = evaluateUpdate(viewSchema, possibleModifications, checkout);
    switch (updatedNeeded) {
        case UpdateType.None: {
            return true;
        }
        case UpdateType.Incompatible: {
            return false;
        }
        case UpdateType.SchemaCompatible: {
            checkout.updateSchema(intoStoredSchema(viewSchema.schema));
            return true;
        }
        case UpdateType.Initialize: {
            if (treeContent === undefined) {
                return false;
            }
            // TODO:
            // When this becomes a more proper out of schema adapter, editing should be made lazy.
            // This will improve support for readonly documents, cross version collaboration and attribution.
            initialize(checkout, treeContent);
            return true;
        }
        default: {
            unreachableCase(updatedNeeded);
        }
    }
}
/**
 * Options used to initialize (if needed) and schematize a `SharedTree`.
 * @remarks
 * Using this builder improves type safety and error quality over just constructing the configuration as a object.
 * @internal
 */
export function buildTreeConfiguration(config) {
    return config;
}
//# sourceMappingURL=schematizeTree.js.map