"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.DefaultEditBuilder = exports.relevantRemovedRoots = exports.intoDelta = exports.DefaultChangeFamily = 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("../../util/index.js");
const index_js_3 = require("../modular-schema/index.js");
const index_js_4 = require("../optional-field/index.js");
const index_js_5 = require("../sequence-field/index.js");
const defaultFieldKinds_js_1 = require("./defaultFieldKinds.js");
/**
 * Implementation of {@link ChangeFamily} based on the default set of supported field kinds.
 *
 * @sealed
 */
class DefaultChangeFamily {
    constructor(codecs, codecOptions) {
        this.modularFamily = new index_js_3.ModularChangeFamily(defaultFieldKinds_js_1.fieldKinds, codecs, codecOptions);
    }
    get rebaser() {
        return this.modularFamily.rebaser;
    }
    get codecs() {
        return this.modularFamily.codecs;
    }
    buildEditor(mintRevisionTag, changeReceiver) {
        return new DefaultEditBuilder(this, mintRevisionTag, changeReceiver, this.modularFamily.codecOptions);
    }
}
exports.DefaultChangeFamily = DefaultChangeFamily;
/**
 * @param change - The change to convert into a delta.
 */
function intoDelta(taggedChange) {
    return (0, index_js_3.intoDelta)(taggedChange, defaultFieldKinds_js_1.fieldKinds);
}
exports.intoDelta = intoDelta;
/**
 * Returns the set of removed roots that should be in memory for the given change to be applied.
 * A removed root is relevant if any of the following is true:
 * - It is being inserted
 * - It is being restored
 * - It is being edited
 * - The ID it is associated with is being changed
 *
 * May be conservative by returning more removed roots than strictly necessary.
 *
 * Will never return IDs for non-root trees, even if they are removed.
 *
 * @param change - The change to be applied.
 */
function relevantRemovedRoots(change) {
    return (0, index_js_3.relevantRemovedRoots)(change, defaultFieldKinds_js_1.fieldKinds);
}
exports.relevantRemovedRoots = relevantRemovedRoots;
/**
 * Implementation of {@link IDefaultEditBuilder} based on the default set of supported field kinds.
 * @sealed
 */
class DefaultEditBuilder {
    constructor(family, mintRevisionTag, changeReceiver, codecOptions) {
        this.mintRevisionTag = mintRevisionTag;
        this.modularBuilder = new index_js_3.ModularEditBuilder(family, defaultFieldKinds_js_1.fieldKinds, changeReceiver, codecOptions);
    }
    enterTransaction() {
        this.modularBuilder.enterTransaction();
    }
    exitTransaction() {
        this.modularBuilder.exitTransaction();
    }
    addNodeExistsConstraint(path) {
        this.modularBuilder.addNodeExistsConstraint(path, this.mintRevisionTag());
    }
    addNodeExistsConstraintOnRevert(path) {
        this.modularBuilder.addNodeExistsConstraintOnRevert(path, this.mintRevisionTag());
    }
    addNoChangeConstraint() {
        this.modularBuilder.addNoChangeConstraint(this.mintRevisionTag());
    }
    addNoChangeConstraintOnRevert() {
        this.modularBuilder.addNoChangeConstraintOnRevert(this.mintRevisionTag());
    }
    valueField(field) {
        return {
            set: (newContent) => {
                (0, internal_1.assert)(newContent.topLevelLength === 1, 0xc12 /* Value fields should have a single top level node */);
                const revision = this.mintRevisionTag();
                const fill = { localId: this.modularBuilder.generateId(), revision };
                const detach = { localId: this.modularBuilder.generateId(), revision };
                const build = this.modularBuilder.buildTrees(fill.localId, newContent, revision);
                const change = (0, index_js_2.brand)(index_js_4.required.changeHandler.editor.set({
                    fill,
                    detach,
                }));
                const edit = {
                    type: "field",
                    field,
                    fieldKind: index_js_4.required.identifier,
                    change,
                    revision,
                };
                this.modularBuilder.submitChanges([build, edit], revision);
            },
        };
    }
    optionalField(field) {
        return {
            set: (newContent, wasEmpty) => {
                // The choice to ban empty chunks here instead of treating them as a clear is a subjective choice made to err of the side of more explicitness and stricter validation.
                (0, internal_1.assert)(newContent === undefined || newContent.topLevelLength === 1, 0xc13 /* optional fields should have a single top level node, or undefined */);
                const edits = [];
                let optionalChange;
                const revision = this.mintRevisionTag();
                const detach = { localId: this.modularBuilder.generateId(), revision };
                if (newContent === undefined) {
                    optionalChange = index_js_4.optional.changeHandler.editor.clear(wasEmpty, detach);
                }
                else {
                    const fill = { localId: this.modularBuilder.generateId(), revision };
                    const build = this.modularBuilder.buildTrees(fill.localId, newContent, revision);
                    edits.push(build);
                    optionalChange = index_js_4.optional.changeHandler.editor.set(wasEmpty, {
                        fill,
                        detach,
                    });
                }
                const change = (0, index_js_2.brand)(optionalChange);
                const edit = {
                    type: "field",
                    field,
                    fieldKind: index_js_4.optional.identifier,
                    change,
                    revision,
                };
                edits.push(edit);
                this.modularBuilder.submitChanges(edits, revision);
            },
        };
    }
    move(sourceField, sourceIndex, count, destinationField, destIndex) {
        if (count === 0) {
            return;
        }
        else if (count < 0 || !Number.isSafeInteger(count)) {
            throw new internal_2.UsageError(`Expected non-negative integer count, got ${count}.`);
        }
        const revision = this.mintRevisionTag();
        const detachCellId = this.modularBuilder.generateId(count);
        const attachCellId = { localId: this.modularBuilder.generateId(count), revision };
        if ((0, index_js_1.compareFieldUpPaths)(sourceField, destinationField)) {
            // The source and destination fields are the same.
            const change = index_js_5.sequence.changeHandler.editor.move(sourceIndex, count, destIndex, detachCellId, attachCellId, revision);
            this.modularBuilder.submitChange(sourceField, index_js_5.sequence.identifier, (0, index_js_2.brand)(change), revision);
        }
        else {
            // The source and destination fields are different.
            const detachPath = (0, index_js_1.topDownPath)(sourceField.parent);
            const attachPath = (0, index_js_1.topDownPath)(destinationField.parent);
            /**
             * The number of elements, starting from the root, that both paths have in common.
             * This defines the lowest common ancestor node (LCA) of the source and destination fields.
             */
            const sharedDepth = getSharedPrefixLength(detachPath, attachPath);
            let adjustedAttachField = destinationField;
            // Check if the detach parent
            // (the node directly above the location of detach)
            // is along the attach path,
            // in which case the attach path might need to be adjusted.
            // This is synonymous with checking if the attach parent
            // (the node directly above the location of attach)
            // is either below or the same as the detach parent.
            // If not, then the move does not need to be adjusted
            // as the attach path will not be affected by the detach.
            if (sharedDepth === detachPath.length) {
                const fieldOnDetachPathUnderLCA = sourceField.field;
                const firstDifferentAttachAncestor = attachPath[sharedDepth];
                // Check if the detach location uses the same field as in
                // the attach path.
                // Note that when `firstDifferentAttachAncestor` is undefined,
                // that means the parents are the same. Since earlier call
                // to `compareFieldUpPaths` returned false, we know that the
                // fields must be different AND thus no adjustment is needed.
                if (fieldOnDetachPathUnderLCA === firstDifferentAttachAncestor?.parentField) {
                    // Now working at the level where detach happens along the
                    // attach path.
                    if (firstDifferentAttachAncestor.parentIndex < sourceIndex) {
                        // The attach path runs through a node located before the detached nodes.
                        // No need to adjust the attach path.
                    }
                    else if (sourceIndex + count <= firstDifferentAttachAncestor.parentIndex) {
                        // The attach path runs through a node located after the detached nodes.
                        // Adjust the index for the node at that depth of the path, so that it is interpreted correctly
                        // in the composition performed by `submitChanges`.
                        // Start with path to root that remains intact.
                        let parent = firstDifferentAttachAncestor.parent;
                        // Extend with index-adjusted parent.
                        parent = {
                            parent,
                            parentIndex: firstDifferentAttachAncestor.parentIndex - count,
                            parentField: firstDifferentAttachAncestor.parentField,
                        };
                        // Extend with the rest of the attach path, which is unaffected
                        // apart from parentage replacements.
                        for (let i = sharedDepth + 1; i < attachPath.length; i += 1) {
                            parent = {
                                ...(attachPath[i] ?? (0, internal_1.oob)()),
                                parent,
                            };
                        }
                        adjustedAttachField = { parent, field: destinationField.field };
                    }
                    else {
                        throw new internal_2.UsageError("Invalid move operation: the destination is located under one of the moved elements. Consider using the Tree.contains API to detect this.");
                    }
                }
            }
            const moveOut = index_js_5.sequence.changeHandler.editor.moveOut(sourceIndex, count, detachCellId, revision);
            const moveIn = index_js_5.sequence.changeHandler.editor.moveIn(destIndex, count, detachCellId, attachCellId, revision);
            this.modularBuilder.submitChanges([
                {
                    type: "field",
                    field: sourceField,
                    fieldKind: index_js_5.sequence.identifier,
                    change: (0, index_js_2.brand)(moveOut),
                    revision,
                },
                {
                    type: "field",
                    field: adjustedAttachField,
                    fieldKind: index_js_5.sequence.identifier,
                    change: (0, index_js_2.brand)(moveIn),
                    revision,
                },
            ], revision);
        }
    }
    sequenceField(field) {
        return {
            insert: (index, content) => {
                const length = content.topLevelLength;
                if (length === 0) {
                    return;
                }
                const revision = this.mintRevisionTag();
                const firstId = { localId: this.modularBuilder.generateId(length), revision };
                const build = this.modularBuilder.buildTrees(firstId.localId, content, revision);
                const change = (0, index_js_2.brand)(index_js_5.sequence.changeHandler.editor.insert(index, length, firstId, revision));
                const attach = {
                    type: "field",
                    field,
                    fieldKind: index_js_5.sequence.identifier,
                    change,
                    revision,
                };
                // The changes have to be submitted together, otherwise they will be assigned different revisions,
                // which will prevent the build ID and the insert ID from matching.
                this.modularBuilder.submitChanges([build, attach], revision);
            },
            remove: (index, count) => {
                if (count === 0) {
                    return;
                }
                const revision = this.mintRevisionTag();
                const id = this.modularBuilder.generateId(count);
                const change = (0, index_js_2.brand)(index_js_5.sequence.changeHandler.editor.remove(index, count, id, revision));
                this.modularBuilder.submitChange(field, index_js_5.sequence.identifier, change, revision);
            },
        };
    }
}
exports.DefaultEditBuilder = DefaultEditBuilder;
/**
 * Gets the number of path elements that both paths share, starting at index 0.
 */
function getSharedPrefixLength(pathA, pathB) {
    const minDepth = Math.min(pathA.length, pathB.length);
    let sharedDepth = 0;
    while (sharedDepth < minDepth) {
        const detachStep = pathA[sharedDepth] ?? (0, internal_1.oob)();
        const attachStep = pathB[sharedDepth] ?? (0, internal_1.oob)();
        if (detachStep !== attachStep &&
            (detachStep.parentField !== attachStep.parentField ||
                detachStep.parentIndex !== attachStep.parentIndex)) {
            break;
        }
        sharedDepth += 1;
    }
    return sharedDepth;
}
//# sourceMappingURL=defaultEditBuilder.js.map