"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.ensureSchema = exports.initialize = exports.canInitialize = exports.evaluateUpdate = exports.UpdateType = exports.initializeContent = void 0;
const internal_1 = require("@fluidframework/core-utils/internal");
const index_js_1 = require("../core/index.js");
const index_js_2 = require("../feature-libraries/index.js");
const index_js_3 = require("../util/index.js");
const index_js_4 = require("../simple-tree/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.
 */
function initializeContent(schemaRepository, newSchema, setInitialTree) {
    (0, internal_1.assert)((0, index_js_1.schemaDataIsEmpty)(schemaRepository.storedSchema), 0x743 /* cannot initialize after a schema is set */);
    const rootSchema = newSchema.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 === index_js_2.FieldKinds.sequence.identifier ||
        rootKind === index_js_2.FieldKinds.optional.identifier) {
        // These kinds are known to tolerate empty, so use the schema as is:
        incrementalSchemaUpdate = newSchema;
    }
    else {
        (0, internal_1.assert)(rootKind === index_js_2.FieldKinds.required.identifier, 0x5c8 /* Unexpected kind */);
        // Replace value kind with optional kind in root field schema:
        incrementalSchemaUpdate = {
            nodeSchema: newSchema.nodeSchema,
            rootFieldSchema: {
                kind: index_js_2.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",
    // );
    (0, internal_1.assert)((0, index_js_2.allowsRepoSuperset)(index_js_2.defaultSchemaPolicy, newSchema, 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 !== newSchema) {
        schemaRepository.updateSchema(newSchema);
    }
}
exports.initializeContent = initializeContent;
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 || (exports.UpdateType = UpdateType = {}));
function evaluateUpdate(viewSchema, allowedSchemaModifications, checkout) {
    const compatibility = viewSchema.checkCompatibility(checkout.storedSchema);
    if (compatibility.canUpgrade && compatibility.canView) {
        // Compatible as is
        return UpdateType.None;
    }
    // eslint-disable-next-line no-bitwise
    if (allowedSchemaModifications & index_js_1.AllowedUpdateType.Initialize && canInitialize(checkout)) {
        return UpdateType.Initialize;
    }
    if (!compatibility.canUpgrade) {
        // Existing stored schema permits trees which are incompatible with the view schema, so schema can not be updated
        return UpdateType.Incompatible;
    }
    (0, internal_1.assert)(!compatibility.canView, 0x8bd /* unexpected case */);
    (0, internal_1.assert)(compatibility.canUpgrade, 0x8be /* unexpected case */);
    // eslint-disable-next-line no-bitwise
    return allowedSchemaModifications & index_js_1.AllowedUpdateType.SchemaCompatible
        ? UpdateType.SchemaCompatible
        : UpdateType.Incompatible;
}
exports.evaluateUpdate = evaluateUpdate;
function canInitialize(checkout) {
    // Check for empty.
    return checkout.forest.isEmpty && (0, index_js_1.schemaDataIsEmpty)(checkout.storedSchema);
}
exports.canInitialize = canInitialize;
function normalizeNewFieldContent(content) {
    if (content === undefined) {
        return (0, index_js_2.cursorForMapTreeField)([]);
    }
    if ((0, index_js_3.isReadonlyArray)(content)) {
        return (0, index_js_2.cursorForMapTreeField)(content.map((c) => (0, index_js_2.mapTreeFromCursor)(c)));
    }
    if (content.mode === 1 /* CursorLocationType.Fields */) {
        return content;
    }
    return (0, index_js_2.cursorForMapTreeField)([(0, index_js_2.mapTreeFromCursor)(content)]);
}
/**
 * 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.
 */
function initialize(checkout, treeContent) {
    checkout.transaction.start();
    try {
        initializeContent(checkout, treeContent.schema, () => {
            const field = { field: index_js_1.rootFieldKey, parent: undefined };
            const content = normalizeNewFieldContent(treeContent.initialTree);
            switch (checkout.storedSchema.rootFieldSchema.kind) {
                case index_js_2.FieldKinds.optional.identifier: {
                    const fieldEditor = checkout.editor.optionalField(field);
                    (0, internal_1.assert)(content.getFieldLength() <= 1, 0x7f4 /* optional field content should normalize at most one item */);
                    fieldEditor.set(content.getFieldLength() === 0 ? undefined : content, true);
                    break;
                }
                case index_js_2.FieldKinds.sequence.identifier: {
                    const fieldEditor = checkout.editor.sequenceField(field);
                    // TODO: should do an idempotent edit here.
                    fieldEditor.insert(0, content);
                    break;
                }
                default: {
                    (0, index_js_3.fail)(0xac7 /* unexpected root field kind during initialize */);
                }
            }
        });
    }
    finally {
        checkout.transaction.commit();
    }
}
exports.initialize = initialize;
/**
 * 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`.
 */
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 &= ~index_js_1.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((0, index_js_4.toStoredSchema)(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: {
            (0, internal_1.unreachableCase)(updatedNeeded);
        }
    }
}
exports.ensureSchema = ensureSchema;
//# sourceMappingURL=schematizeTree.js.map