/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { assert, oob } from "@fluidframework/core-utils/internal";
import { UsageError } from "@fluidframework/telemetry-utils/internal";
import { compareFieldUpPaths, getDetachedFieldContainingFieldPath, rootField, topDownPath, } from "../../core/index.js";
import { brand, RangeMap } from "../../util/index.js";
import { ModularChangeFamily, ModularEditBuilder, intoDelta as intoModularDelta, relevantRemovedRoots as relevantModularRemovedRoots, getBuildIds as getModularBuildIds, } from "../modular-schema/index.js";
import { fieldKinds, optional, sequence, required as valueFieldKind, } from "./defaultFieldKinds.js";
/**
 * Implementation of {@link ChangeFamily} based on the default set of supported field kinds.
 *
 * @sealed
 */
export class DefaultChangeFamily {
    constructor(codecs) {
        this.modularFamily = new ModularChangeFamily(fieldKinds, codecs);
    }
    get rebaser() {
        return this.modularFamily.rebaser;
    }
    get codecs() {
        return this.modularFamily.codecs;
    }
    buildEditor(mintRevisionTag, changeReceiver, options) {
        return new DefaultIdBasedDataEditor(this, mintRevisionTag, changeReceiver, options);
    }
}
/**
 * @param change - The change to convert into a delta.
 */
export function intoDelta(taggedChange) {
    return intoModularDelta(taggedChange, fieldKinds);
}
/**
 * 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.
 */
export function relevantRemovedRoots(change) {
    return relevantModularRemovedRoots(change, fieldKinds);
}
export function getBuildsIds(change) {
    return getModularBuildIds(change);
}
export function offsetChangesetLocalId(id, offset) {
    return brand(id + offset);
}
export function subtractChangesetLocalId(a, b) {
    return a - b;
}
/**
 * Implementation of {@link IdBasedChangeFamilyDataEditor} based on the default set of supported field kinds.
 * @sealed
 */
export class DefaultIdBasedDataEditor {
    constructor(family, mintRevisionTag, changeReceiver, options = { canMakeDetachedRootEdits: false }) {
        this.mintRevisionTag = mintRevisionTag;
        this.options = options;
        this.nodesWithoutCells = new RangeMap(offsetChangesetLocalId, subtractChangesetLocalId);
        this.modularBuilder = new ModularEditBuilder(family, fieldKinds, changeReceiver);
    }
    enterTransaction() {
        if (this.modularBuilder.isInTransaction() === false) {
            this.nodesWithoutCells.clear();
        }
        this.modularBuilder.enterTransaction();
    }
    exitTransaction() {
        this.modularBuilder.exitTransaction();
        if (this.modularBuilder.isInTransaction() === false) {
            this.nodesWithoutCells.clear();
        }
    }
    addNodeExistsConstraint(path) {
        enforceEditsToDetachedTreesOptions({ parent: path.parent, field: path.parentField }, this.options);
        this.modularBuilder.addNodeExistsConstraint(path, this.mintRevisionTag());
    }
    addNodeExistsConstraintOnRevert(path) {
        enforceEditsToDetachedTreesOptions({ parent: path.parent, field: path.parentField }, this.options);
        this.modularBuilder.addNodeExistsConstraintOnRevert(path, this.mintRevisionTag());
    }
    buildRoots(content) {
        const detachedRoots = [];
        const count = content.topLevelLength;
        if (count > 0) {
            const buildRevision = this.mintRevisionTag();
            const buildId = {
                localId: this.modularBuilder.generateId(count),
                revision: buildRevision,
            };
            const build = this.modularBuilder.buildTrees(buildId.localId, content, buildRevision);
            this.modularBuilder.submitChanges([build], buildRevision);
            detachedRoots.push({ first: buildId, count });
            this.nodesWithoutCells.set(buildId.localId, count, true);
        }
        return detachedRoots;
    }
    valueField(field) {
        enforceEditsToDetachedTreesOptions(field, this.options);
        const makeAttachEditDescription = (fill, revision) => {
            const detachLocalId = this.modularBuilder.generateId();
            const detach = { localId: detachLocalId, revision };
            const change = valueFieldKind.changeHandler.editor.set({ fill, detach });
            return {
                type: "field",
                field,
                fieldKind: valueFieldKind.identifier,
                change: brand(change),
                revision,
            };
        };
        return {
            set: (newContent) => {
                assert(newContent.topLevelLength === 1, "Expected exactly one node");
                const revision = this.mintRevisionTag();
                const buildLocalId = this.modularBuilder.generateId();
                const buildId = { localId: buildLocalId, revision };
                const build = this.modularBuilder.buildTrees(buildLocalId, newContent, revision);
                const attach = makeAttachEditDescription(buildId, revision);
                this.modularBuilder.submitChanges([build, attach], revision);
            },
            attach: (newContent) => {
                const isWithoutCell = this.nodesWithoutCells.delete(newContent.localId, 1) === 1;
                if (!isWithoutCell) {
                    throw new UsageError("A hydrated node that has been attached before cannot be attached into an object's required field. Assign new content to the field instead.");
                }
                const revision = this.mintRevisionTag();
                const attach = makeAttachEditDescription(newContent, revision);
                this.modularBuilder.submitChanges([attach], revision);
            },
        };
    }
    optionalField(field) {
        enforceEditsToDetachedTreesOptions(field, this.options);
        const makeAttachEditDescription = (fill, revision, wasEmpty) => {
            const detachLocalId = this.modularBuilder.generateId();
            const detach = { localId: detachLocalId, revision };
            const change = optional.changeHandler.editor.set(wasEmpty, { fill, detach });
            return {
                type: "field",
                field,
                fieldKind: optional.identifier,
                change: brand(change),
                revision,
            };
        };
        const editBuilder = {
            set: (newContent, wasEmpty) => {
                if (newContent === undefined) {
                    editBuilder.clear(wasEmpty);
                    return;
                }
                assert(newContent.topLevelLength === 1, "Expected exactly one node");
                const revision = this.mintRevisionTag();
                const buildLocalId = this.modularBuilder.generateId();
                const buildId = { localId: buildLocalId, revision };
                const build = this.modularBuilder.buildTrees(buildLocalId, newContent, revision);
                const attach = makeAttachEditDescription(buildId, revision, wasEmpty);
                this.modularBuilder.submitChanges([build, attach], revision);
            },
            attach: (content, wasEmpty) => {
                if (content === undefined) {
                    editBuilder.clear(wasEmpty);
                    return;
                }
                const isWithoutCell = this.nodesWithoutCells.delete(content.localId, 1) === 1;
                if (!isWithoutCell && this.options.canMakeDetachedRootEdits !== true) {
                    throw new UsageError(`Attach edits require a minimum version for collaboration >= TBD.`);
                }
                const revision = this.mintRevisionTag();
                const edits = [];
                let stableSrcId;
                if (isWithoutCell) {
                    stableSrcId = content;
                }
                else {
                    // If the node has an associated cell, then it may be concurrently moved to another location.
                    // We use a rename to ensure that the node will be moved to a specific grave before the attach.
                    stableSrcId = { localId: this.modularBuilder.generateId(), revision };
                    const rename = {
                        type: "global",
                        revision,
                        renames: [
                            { count: 1, oldId: content, newId: stableSrcId, detachLocation: undefined },
                        ],
                    };
                    edits.push(rename);
                }
                const attach = makeAttachEditDescription(stableSrcId, revision, wasEmpty);
                edits.push(attach);
                this.modularBuilder.submitChanges(edits, revision);
            },
            clear: (wasEmpty) => {
                const revision = this.mintRevisionTag();
                const detach = { localId: this.modularBuilder.generateId(), revision };
                const optionalChange = optional.changeHandler.editor.clear(wasEmpty, detach);
                const change = brand(optionalChange);
                const edit = {
                    type: "field",
                    field,
                    fieldKind: optional.identifier,
                    change,
                    revision,
                };
                this.modularBuilder.submitChanges([edit], revision);
            },
        };
        return editBuilder;
    }
    move(sourceField, sourceIndex, count, destinationField, destIndex) {
        if (count === 0) {
            return;
        }
        else if (count < 0 || !Number.isSafeInteger(count)) {
            throw new UsageError(`Expected non-negative integer count, got ${count}.`);
        }
        enforceEditsToDetachedTreesOptions(sourceField, this.options);
        enforceEditsToDetachedTreesOptions(destinationField, this.options);
        const revision = this.mintRevisionTag();
        const detachCellId = this.modularBuilder.generateId(count);
        const attachCellId = { localId: this.modularBuilder.generateId(count), revision };
        if (compareFieldUpPaths(sourceField, destinationField)) {
            const change = sequence.changeHandler.editor.move(sourceIndex, count, destIndex, detachCellId, attachCellId, revision);
            this.modularBuilder.submitChange(sourceField, sequence.identifier, brand(change), revision);
        }
        else {
            const detachPath = topDownPath(sourceField.parent);
            const attachPath = topDownPath(destinationField.parent);
            const sharedDepth = getSharedPrefixLength(detachPath, attachPath);
            let adjustedAttachField = destinationField;
            // `sharedDepth` is the number of elements, starting from the root, that both paths have in common.
            if (sharedDepth === detachPath.length) {
                const lowestCommonAncestor = attachPath[sharedDepth];
                const attachField = lowestCommonAncestor?.parentField ?? destinationField.field;
                if (attachField === sourceField.field) {
                    // The detach occurs in an ancestor field of the field where the attach occurs.
                    const attachAncestorIndex = lowestCommonAncestor?.parentIndex ?? destIndex;
                    if (attachAncestorIndex < sourceIndex) {
                        // The attach path runs through a node located before the detached nodes.
                        // No need to adjust the attach path.
                    }
                    else if (sourceIndex + count <= attachAncestorIndex) {
                        // 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`.
                        const adjustedAttachAncestorIndex = attachAncestorIndex - count;
                        let parent = lowestCommonAncestor === undefined
                            ? {
                                parent: undefined,
                                detachedNodeId: undefined,
                                parentIndex: adjustedAttachAncestorIndex,
                                parentField: destinationField.field,
                            }
                            : {
                                ...lowestCommonAncestor,
                                parentIndex: adjustedAttachAncestorIndex,
                            };
                        for (let i = sharedDepth + 1; i < attachPath.length; i += 1) {
                            parent = {
                                ...(attachPath[i] ?? oob()),
                                parent,
                            };
                        }
                        adjustedAttachField = { parent, field: destinationField.field };
                    }
                    else {
                        throw new 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 = sequence.changeHandler.editor.remove(sourceIndex, count, detachCellId, revision);
            const moveIn = sequence.changeHandler.editor.insert(destIndex, count, attachCellId, revision, detachCellId);
            this.modularBuilder.submitChanges([
                {
                    type: "field",
                    field: sourceField,
                    fieldKind: sequence.identifier,
                    change: brand(moveOut),
                    revision,
                },
                {
                    type: "field",
                    field: adjustedAttachField,
                    fieldKind: sequence.identifier,
                    change: brand(moveIn),
                    revision,
                },
            ], revision);
        }
    }
    sequenceField(field) {
        enforceEditsToDetachedTreesOptions(field, this.options);
        const makeAttachEditDescription = (index, { first, count }, revision, areWithoutCells) => {
            const edits = [];
            // If the nodes have never been attached in cell, then we must use a cell ID that matches the build ID.
            // This ensures back-compatibility with the v1 ModularChangeFamily model which requires that every node be associated with cell
            // by generating an insert whose destination cell is the cell associated with the build ID.
            let stableSrcId;
            if (areWithoutCells) {
                stableSrcId = first;
            }
            else {
                // If the node has an associated cell, then it may be concurrently moved to another location.
                // We use a rename to ensure that the node will be moved to a specific grave before the attach.
                stableSrcId = { localId: this.modularBuilder.generateId(count), revision };
                const rename = {
                    type: "global",
                    revision,
                    renames: [
                        {
                            count,
                            oldId: first,
                            newId: stableSrcId,
                            detachLocation: undefined,
                        },
                    ],
                };
                edits.push(rename);
            }
            assert(stableSrcId.revision !== undefined, "Detached nodes ID must have a revision");
            const change = sequence.changeHandler.editor.insert(index, count, stableSrcId, stableSrcId.revision, stableSrcId.localId);
            const attach = {
                type: "field",
                field,
                fieldKind: sequence.identifier,
                change: brand(change),
                revision,
            };
            edits.push(attach);
            return edits;
        };
        const editBuilder = {
            insert: (index, content) => {
                const count = content.topLevelLength;
                if (count === 0) {
                    return;
                }
                const revision = this.mintRevisionTag();
                const buildLocalId = this.modularBuilder.generateId();
                const build = this.modularBuilder.buildTrees(buildLocalId, content, revision);
                const roots = {
                    first: { localId: buildLocalId, revision },
                    count,
                };
                const attach = makeAttachEditDescription(index, roots, revision, true);
                this.modularBuilder.submitChanges([build, ...attach], revision);
            },
            attach: (index, newContent) => {
                const attachRevision = this.mintRevisionTag();
                let areAllWithoutCells = true;
                let insertOffset = 0;
                const edits = [];
                for (const range of newContent) {
                    if (range.count === 0) {
                        continue;
                    }
                    const countWithoutCells = this.nodesWithoutCells.delete(range.first.localId, range.count);
                    assert(countWithoutCells === 0 || countWithoutCells === range.count, "All detached roots within the same ID range must either all have or all not have a cell");
                    const areWithoutCells = countWithoutCells === range.count;
                    if (!areWithoutCells) {
                        areAllWithoutCells = false;
                    }
                    const renameAndAttach = makeAttachEditDescription(index + insertOffset, range, attachRevision, areWithoutCells);
                    edits.push(...renameAndAttach);
                    insertOffset += range.count;
                }
                if (!areAllWithoutCells && this.options.canMakeDetachedRootEdits !== true) {
                    throw new UsageError(`Attach edits require a minimum version for collaboration >= TBD.`);
                }
                if (edits.length > 0) {
                    this.modularBuilder.submitChanges(edits, attachRevision);
                }
            },
            remove: (index, count) => {
                if (count === 0) {
                    return;
                }
                const revision = this.mintRevisionTag();
                const id = this.modularBuilder.generateId(count);
                const change = brand(sequence.changeHandler.editor.remove(index, count, id, revision));
                this.modularBuilder.submitChange(field, sequence.identifier, change, revision);
            },
        };
        return editBuilder;
    }
}
function enforceEditsToDetachedTreesOptions(field, options) {
    if (options.canMakeDetachedRootEdits !== true) {
        const topField = getDetachedFieldContainingFieldPath(field);
        if (topField !== rootField) {
            throw new UsageError(`Edits and constraints on detached trees require a minimum version for collaboration >= TBD.`);
        }
    }
}
/**
 * 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] ?? oob();
        const attachStep = pathB[sharedDepth] ?? oob();
        if (detachStep !== attachStep) {
            if (detachStep.parentField !== attachStep.parentField ||
                detachStep.parentIndex !== attachStep.parentIndex) {
                break;
            }
        }
        sharedDepth += 1;
    }
    return sharedDepth;
}
//# sourceMappingURL=defaultEditBuilder.js.map