"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 core_utils_1 = require("@fluidframework/core-utils");
const core_1 = require("../../core");
const util_1 = require("../../util");
const modular_schema_1 = require("../modular-schema");
const defaultFieldKinds_1 = require("./defaultFieldKinds");
/**
 * Implementation of {@link ChangeFamily} based on the default set of supported field kinds.
 *
 * @sealed
 */
class DefaultChangeFamily {
    constructor(codecOptions) {
        this.modularFamily = new modular_schema_1.ModularChangeFamily(defaultFieldKinds_1.fieldKinds, codecOptions);
    }
    get rebaser() {
        return this.modularFamily.rebaser;
    }
    get codecs() {
        return this.modularFamily.codecs;
    }
    buildEditor(changeReceiver) {
        return new DefaultEditBuilder(this, changeReceiver);
    }
}
exports.DefaultChangeFamily = DefaultChangeFamily;
DefaultChangeFamily.emptyChange = modular_schema_1.ModularChangeFamily.emptyChange;
/**
 * @param change - The change to convert into a delta.
 */
function intoDelta(taggedChange) {
    return (0, modular_schema_1.intoDelta)(taggedChange, defaultFieldKinds_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(taggedChange) {
    return (0, modular_schema_1.relevantRemovedRoots)(taggedChange, defaultFieldKinds_1.fieldKinds);
}
exports.relevantRemovedRoots = relevantRemovedRoots;
/**
 * Implementation of {@link IDefaultEditBuilder} based on the default set of supported field kinds.
 * @sealed
 */
class DefaultEditBuilder {
    constructor(family, changeReceiver) {
        this.modularBuilder = new modular_schema_1.ModularEditBuilder(family, changeReceiver);
    }
    enterTransaction() {
        this.modularBuilder.enterTransaction();
    }
    exitTransaction() {
        this.modularBuilder.exitTransaction();
    }
    apply(change) {
        this.modularBuilder.apply(change);
    }
    addNodeExistsConstraint(path) {
        this.modularBuilder.addNodeExistsConstraint(path);
    }
    valueField(field) {
        return {
            set: (newContent) => {
                const fillId = this.modularBuilder.generateId();
                const build = this.modularBuilder.buildTrees(fillId, [newContent]);
                const change = (0, util_1.brand)(defaultFieldKinds_1.required.changeHandler.editor.set({
                    fill: fillId,
                    detach: this.modularBuilder.generateId(),
                }));
                const edit = {
                    type: "field",
                    field,
                    fieldKind: defaultFieldKinds_1.required.identifier,
                    change,
                };
                this.modularBuilder.submitChanges([build, edit]);
            },
        };
    }
    optionalField(field) {
        return {
            set: (newContent, wasEmpty) => {
                const detachId = this.modularBuilder.generateId();
                let fillId;
                const edits = [];
                let optionalChange;
                if (newContent !== undefined) {
                    fillId = this.modularBuilder.generateId();
                    const build = this.modularBuilder.buildTrees(fillId, [newContent]);
                    edits.push(build);
                    optionalChange = defaultFieldKinds_1.optional.changeHandler.editor.set(wasEmpty, {
                        fill: fillId,
                        detach: detachId,
                    });
                }
                else {
                    optionalChange = defaultFieldKinds_1.optional.changeHandler.editor.clear(wasEmpty, detachId);
                }
                const change = (0, util_1.brand)(optionalChange);
                const edit = {
                    type: "field",
                    field,
                    fieldKind: defaultFieldKinds_1.optional.identifier,
                    change,
                };
                edits.push(edit);
                this.modularBuilder.submitChanges(edits, newContent === undefined ? detachId : fillId);
            },
        };
    }
    move(sourceField, sourceIndex, count, destinationField, destIndex) {
        const moveId = this.modularBuilder.generateId(count);
        if ((0, core_1.compareFieldUpPaths)(sourceField, destinationField)) {
            const change = defaultFieldKinds_1.sequence.changeHandler.editor.move(sourceIndex, count, destIndex, moveId);
            this.modularBuilder.submitChange(sourceField, defaultFieldKinds_1.sequence.identifier, (0, util_1.brand)(change));
        }
        else {
            const detachPath = (0, core_1.topDownPath)(sourceField.parent);
            const attachPath = (0, core_1.topDownPath)(destinationField.parent);
            const sharedDepth = getSharedPrefixLength(detachPath, attachPath);
            let adjustedAttachField = destinationField;
            // After the above loop, `sharedDepth` is the number of elements, starting from the root,
            // that both paths have in common.
            if (sharedDepth === detachPath.length) {
                const attachField = attachPath[sharedDepth]?.parentField ?? destinationField.field;
                if (attachField === sourceField.field) {
                    // The detach occurs in an ancestor field of the field where the attach occurs.
                    let attachAncestorIndex = attachPath[sharedDepth]?.parentIndex ?? sourceIndex;
                    if (attachAncestorIndex < sourceIndex) {
                        // The attach path runs through a node located before the detached nodes.
                        // No need to adjust the attach path.
                    }
                    else {
                        (0, core_utils_1.assert)(sourceIndex + count <= attachAncestorIndex, 0x801 /* Invalid move: the destination is below one of the moved elements. */);
                        // 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`.
                        attachAncestorIndex -= count;
                        let parent = attachPath[sharedDepth - 1];
                        parent = {
                            parent,
                            parentIndex: attachAncestorIndex,
                            parentField: attachPath[sharedDepth].parentField,
                        };
                        for (let i = sharedDepth + 1; i < attachPath.length; i += 1) {
                            parent = {
                                ...attachPath[i],
                                parent,
                            };
                        }
                        adjustedAttachField = { parent, field: destinationField.field };
                    }
                }
            }
            const moveOut = defaultFieldKinds_1.sequence.changeHandler.editor.moveOut(sourceIndex, count, moveId);
            const moveIn = defaultFieldKinds_1.sequence.changeHandler.editor.moveIn(destIndex, count, moveId);
            this.modularBuilder.submitChanges([
                {
                    type: "field",
                    field: sourceField,
                    fieldKind: defaultFieldKinds_1.sequence.identifier,
                    change: (0, util_1.brand)(moveOut),
                },
                {
                    type: "field",
                    field: adjustedAttachField,
                    fieldKind: defaultFieldKinds_1.sequence.identifier,
                    change: (0, util_1.brand)(moveIn),
                },
            ], moveId);
        }
    }
    sequenceField(field, shapeInfo) {
        return {
            insert: (index, newContent) => {
                const content = (0, util_1.isReadonlyArray)(newContent) ? newContent : [newContent];
                const length = content.length;
                if (length === 0) {
                    return;
                }
                const firstId = this.modularBuilder.generateId(length);
                const build = this.modularBuilder.buildTrees(firstId, content, shapeInfo);
                const change = (0, util_1.brand)(defaultFieldKinds_1.sequence.changeHandler.editor.insert(index, length, firstId));
                const attach = {
                    type: "field",
                    field,
                    fieldKind: defaultFieldKinds_1.sequence.identifier,
                    change,
                };
                // 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], (0, util_1.brand)(firstId + length - 1));
            },
            delete: (index, count) => {
                if (count === 0) {
                    return;
                }
                const id = this.modularBuilder.generateId(count);
                const change = (0, util_1.brand)(defaultFieldKinds_1.sequence.changeHandler.editor.delete(index, count, id));
                this.modularBuilder.submitChange(field, defaultFieldKinds_1.sequence.identifier, change);
            },
            move: (sourceIndex, count, destIndex) => {
                const moveId = this.modularBuilder.generateId(count);
                const change = defaultFieldKinds_1.sequence.changeHandler.editor.move(sourceIndex, count, destIndex, moveId);
                this.modularBuilder.submitChange(field, defaultFieldKinds_1.sequence.identifier, (0, util_1.brand)(change));
            },
        };
    }
}
exports.DefaultEditBuilder = DefaultEditBuilder;
/**
 * @returns 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];
        const attachStep = pathB[sharedDepth];
        if (detachStep !== attachStep) {
            if (detachStep.parentField !== attachStep.parentField ||
                detachStep.parentIndex !== attachStep.parentIndex) {
                break;
            }
        }
        sharedDepth += 1;
    }
    return sharedDepth;
}
//# sourceMappingURL=defaultChangeFamily.js.map