/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { assert, fail } from "@fluidframework/core-utils/internal";
import { UsageError } from "@fluidframework/telemetry-utils/internal";
import { BTree } from "@tylerbu/sorted-btree-es6";
import { lt } from "semver-ts";
import { FluidClientVersion, } from "../../codec/index.js";
import { EditBuilder, makeDetachedNodeId, revisionMetadataSourceFromInfo, areEqualChangeAtomIds, areEqualChangeAtomIdOpts, tagChange, makeAnonChange, newChangeAtomIdRangeMap, mapTaggedChange, comparePartialRevisions, comparePartialChangesetLocalIds, } from "../../core/index.js";
import { brand, idAllocatorFromMaxId, idAllocatorFromState, getOrCreate, mergeTupleBTrees, RangeMap, balancedReduce, newTupleBTree, compareStrings, createTupleComparator, } from "../../util/index.js";
import { getFromChangeAtomIdMap, newChangeAtomIdBTree, setInChangeAtomIdMap, } from "../changeAtomIdBTree.js";
import { CrossFieldTarget, getFirstFromCrossFieldMap, setInCrossFieldMap, } from "./crossFieldQueries.js";
import { NodeAttachState, } from "./fieldChangeHandler.js";
import { convertGenericChange, genericFieldKind } from "./genericFieldKind.js";
import { newCrossFieldKeyTable, } from "./modularChangeTypes.js";
/**
 * Implementation of ChangeFamily which delegates work in a given field to the appropriate FieldKind
 * as determined by the schema.
 */
export class ModularChangeFamily {
    constructor(fieldKinds, codecs, codecOptions) {
        this.codecs = codecs;
        this.codecOptions = codecOptions;
        this.fieldKinds = fieldKinds;
    }
    get rebaser() {
        return this;
    }
    /**
     * Produces an equivalent list of `FieldChangeset`s that all target the same {@link FlexFieldKind}.
     * @param changes - The list of `FieldChange`s whose `FieldChangeset`s needs to be normalized.
     * @returns An object that contains both the equivalent list of `FieldChangeset`s that all
     * target the same {@link FlexFieldKind}, and the `FieldKind` that they target.
     * The returned `FieldChangeset`s may be a shallow copy of the input `FieldChange`s.
     */
    normalizeFieldChanges(change1, change2) {
        // TODO: Handle the case where changes have conflicting field kinds
        const kind = change1.fieldKind === genericFieldKind.identifier
            ? change2.fieldKind
            : change1.fieldKind;
        if (kind === genericFieldKind.identifier) {
            // Both changes are generic
            return {
                fieldKind: genericFieldKind.identifier,
                changeHandler: genericFieldKind.changeHandler,
                change1: change1.change,
                change2: change2.change,
            };
        }
        const fieldKind = getFieldKind(this.fieldKinds, kind);
        const changeHandler = fieldKind.changeHandler;
        const normalizedChange1 = this.normalizeFieldChange(change1, changeHandler);
        const normalizedChange2 = this.normalizeFieldChange(change2, changeHandler);
        return {
            fieldKind: kind,
            changeHandler,
            change1: normalizedChange1,
            change2: normalizedChange2,
        };
    }
    normalizeFieldChange(fieldChange, handler) {
        if (fieldChange.fieldKind !== genericFieldKind.identifier) {
            return fieldChange.change;
        }
        // The cast is based on the `fieldKind` check above
        const genericChange = fieldChange.change;
        const convertedChange = convertGenericChange(genericChange, handler);
        return convertedChange;
    }
    compose(changes) {
        const { revInfos, maxId } = getRevInfoFromTaggedChanges(changes);
        const idState = { maxId };
        const pairwiseDelegate = (left, right) => {
            return this.composePair(left, right, revInfos, idState);
        };
        const innerChanges = changes.map((change) => change.change);
        return balancedReduce(innerChanges, pairwiseDelegate, makeModularChangeset);
    }
    composePair(change1, change2, revInfos, idState) {
        const { fieldChanges, nodeChanges, nodeToParent, nodeAliases, crossFieldKeys } = this.composeAllFields(change1, change2, revInfos, idState);
        const { allBuilds, allDestroys, allRefreshers } = composeBuildsDestroysAndRefreshers(change1, change2);
        // The composed changeset has a "no change" constraint if either change has one
        const noChangeConstraint = change1.noChangeConstraint ?? change2.noChangeConstraint;
        const noChangeConstraintOnRevert = change1.noChangeConstraintOnRevert ?? change2.noChangeConstraintOnRevert;
        return makeModularChangeset({
            fieldChanges,
            nodeChanges,
            nodeToParent,
            nodeAliases,
            crossFieldKeys,
            maxId: idState.maxId,
            revisions: revInfos,
            noChangeConstraint,
            noChangeConstraintOnRevert,
            builds: allBuilds,
            destroys: allDestroys,
            refreshers: allRefreshers,
        });
    }
    composeAllFields(potentiallyConflictedChange1, potentiallyConflictedChange2, revInfos, idState) {
        // Our current cell ordering scheme in sequences depends on being able to rebase over a change with conflicts.
        // This means that compose must preserve declarations (e.g., new cells) made by conflicted changes (so that we can rebase over the composition).
        // TODO: remove once AB#46104 is completed
        const change1 = this.getEffectiveChange(potentiallyConflictedChange1);
        const change2 = this.getEffectiveChange(potentiallyConflictedChange2);
        const genId = idAllocatorFromState(idState);
        const revisionMetadata = revisionMetadataSourceFromInfo(revInfos);
        // We merge nodeChanges, nodeToParent, and nodeAliases from the two changesets.
        // The merged tables will have correct entries for all nodes which are only referenced in one of the input changesets.
        // During composeFieldMaps and composeInvalidatedElements we will find all nodes referenced in both input changesets
        // and adjust these tables as necessary.
        // Note that when merging these tables we may encounter key collisions and will arbitrarily drop values in that case.
        // A collision for a node ID means that that node is referenced in both changesets
        // (since we assume that if two changesets use the same node ID they are referring to the same node),
        // therefore all collisions will be addressed when processing the intersection of the changesets.
        const composedNodeChanges = brand(mergeTupleBTrees(change1.nodeChanges, change2.nodeChanges));
        const composedNodeToParent = brand(mergeTupleBTrees(change1.nodeToParent, change2.nodeToParent));
        const composedNodeAliases = brand(mergeTupleBTrees(change1.nodeAliases, change2.nodeAliases));
        const crossFieldTable = newComposeTable(change1, change2, composedNodeToParent);
        const composedFields = this.composeFieldMaps(change1.fieldChanges, change2.fieldChanges, undefined, genId, crossFieldTable, revisionMetadata);
        this.composeInvalidatedElements(crossFieldTable, composedFields, composedNodeChanges, composedNodeToParent, composedNodeAliases, genId, revisionMetadata);
        // Currently no field kinds require making changes to cross-field keys during composition, so we can just merge the two tables.
        const composedCrossFieldKeys = RangeMap.union(change1.crossFieldKeys, change2.crossFieldKeys);
        return {
            fieldChanges: composedFields,
            nodeChanges: composedNodeChanges,
            nodeToParent: composedNodeToParent,
            nodeAliases: composedNodeAliases,
            crossFieldKeys: composedCrossFieldKeys,
        };
    }
    composeInvalidatedField(fieldChange, crossFieldTable, genId, revisionMetadata) {
        const context = crossFieldTable.fieldToContext.get(fieldChange);
        assert(context !== undefined, 0x8cc /* Should have context for every invalidated field */);
        const { fieldId, change1: fieldChange1, change2: fieldChange2, composedChange } = context;
        const rebaser = getChangeHandler(this.fieldKinds, composedChange.fieldKind).rebaser;
        const composeNodes = (child1, child2) => {
            if (child1 !== undefined &&
                child2 !== undefined &&
                getFromChangeAtomIdMap(crossFieldTable.newToBaseNodeId, child2) === undefined) {
                setInChangeAtomIdMap(crossFieldTable.newToBaseNodeId, child2, child1);
                crossFieldTable.pendingCompositions.nodeIdsToCompose.push([child1, child2]);
            }
            return child1 ?? child2 ?? fail(0xb22 /* Should not compose two undefined nodes */);
        };
        const amendedChange = rebaser.compose(fieldChange1, fieldChange2, composeNodes, genId, new ComposeManager(crossFieldTable, fieldChange, fieldId, false), revisionMetadata);
        composedChange.change = brand(amendedChange);
    }
    /**
     * Updates everything in the composed output which may no longer be valid.
     * This could be due to
     * - discovering that two node changesets refer to the same node (`nodeIdsToCompose`)
     * - a previously composed field being invalidated by a cross field effect (`invalidatedFields`)
     * - a field which was copied directly from an input changeset being invalidated by a cross field effect
     * (`affectedBaseFields` and `affectedNewFields`)
     *
     * Updating an element may invalidate further elements. This function runs until there is no more invalidation.
     */
    composeInvalidatedElements(table, composedFields, composedNodes, composedNodeToParent, nodeAliases, genId, metadata) {
        const pending = table.pendingCompositions;
        while (table.invalidatedFields.size > 0 ||
            pending.nodeIdsToCompose.length > 0 ||
            pending.affectedBaseFields.length > 0 ||
            pending.affectedNewFields.length > 0) {
            // Note that the call to `composeNodesById` can add entries to `crossFieldTable.nodeIdPairs`.
            for (const [id1, id2] of pending.nodeIdsToCompose) {
                this.composeNodesById(table.baseChange.nodeChanges, table.newChange.nodeChanges, composedNodes, composedNodeToParent, nodeAliases, id1, id2, genId, table, metadata);
            }
            pending.nodeIdsToCompose.length = 0;
            this.composeAffectedFields(table, table.baseChange, true, pending.affectedBaseFields, composedFields, composedNodes, genId, metadata);
            this.composeAffectedFields(table, table.newChange, false, pending.affectedNewFields, composedFields, composedNodes, genId, metadata);
            this.processInvalidatedCompositions(table, genId, metadata);
        }
    }
    processInvalidatedCompositions(table, genId, metadata) {
        const fieldsToUpdate = table.invalidatedFields;
        table.invalidatedFields = new Set();
        for (const fieldChange of fieldsToUpdate) {
            this.composeInvalidatedField(fieldChange, table, genId, metadata);
        }
    }
    /**
     * Ensures that each field in `affectedFields` has been updated in the composition output.
     * Any field which has already been composed is ignored.
     * All other fields are optimistically assumed to not have any changes in the other input changeset.
     *
     * @param change - The changeset which contains the affected fields.
     * This should be one of the two changesets being composed.
     * @param areBaseFields - Whether the affected fields are part of the base changeset.
     * If not, they are assumed to be part of the new changeset.
     * @param affectedFields - The set of fields to process.
     */
    composeAffectedFields(table, change, areBaseFields, affectedFields, composedFields, composedNodes, genId, metadata) {
        for (const fieldIdKey of affectedFields.keys()) {
            const fieldId = normalizeFieldId(fieldIdFromFieldIdKey(fieldIdKey), change.nodeAliases);
            const fieldChange = fieldChangeFromId(change.fieldChanges, change.nodeChanges, fieldId);
            if (table.fieldToContext.has(fieldChange) ||
                table.newFieldToBaseField.has(fieldChange)) {
                // This function handles fields which were not part of the intersection of the two changesets but which need to be updated anyway.
                // If we've already processed this field then either it is up to date
                // or there is pending inval which will be handled in processInvalidatedCompositions.
                continue;
            }
            const emptyChange = this.createEmptyFieldChange(fieldChange.fieldKind);
            const [change1, change2] = areBaseFields
                ? [fieldChange, emptyChange]
                : [emptyChange, fieldChange];
            const composedField = this.composeFieldChanges(fieldId, change1, change2, genId, table, metadata);
            if (fieldId.nodeId === undefined) {
                composedFields.set(fieldId.field, composedField);
                continue;
            }
            const nodeId = getFromChangeAtomIdMap(table.newToBaseNodeId, fieldId.nodeId) ?? fieldId.nodeId;
            let nodeChangeset = nodeChangeFromId(composedNodes, nodeId);
            if (!table.composedNodes.has(nodeChangeset)) {
                nodeChangeset = cloneNodeChangeset(nodeChangeset);
                setInChangeAtomIdMap(composedNodes, nodeId, nodeChangeset);
            }
            // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- using ??= could change behavior if value is falsy
            if (nodeChangeset.fieldChanges === undefined) {
                nodeChangeset.fieldChanges = new Map();
            }
            nodeChangeset.fieldChanges.set(fieldId.field, composedField);
        }
        affectedFields.clear();
    }
    composeFieldMaps(change1, change2, parentId, genId, crossFieldTable, revisionMetadata) {
        const composedFields = new Map();
        if (change1 === undefined || change2 === undefined) {
            return change1 ?? change2 ?? composedFields;
        }
        for (const [field, fieldChange1] of change1) {
            const fieldId = { nodeId: parentId, field };
            const fieldChange2 = change2.get(field);
            const composedField = fieldChange2 === undefined
                ? fieldChange1
                : this.composeFieldChanges(fieldId, fieldChange1, fieldChange2, genId, crossFieldTable, revisionMetadata);
            composedFields.set(field, composedField);
        }
        for (const [field, fieldChange2] of change2) {
            if (!change1?.has(field)) {
                composedFields.set(field, fieldChange2);
            }
        }
        return composedFields;
    }
    /**
     * Returns the composition of the two input fields.
     *
     * Any nodes in this field which were modified by both changesets
     * will be added to `crossFieldTable.pendingCompositions.nodeIdsToCompose`.
     *
     * Any fields which had cross-field information sent to them as part of this field composition
     * will be added to either `affectedBaseFields` or `affectedNewFields` in `crossFieldTable.pendingCompositions`.
     *
     * Any composed `FieldChange` which is invalidated by new cross-field information will be added to `crossFieldTable.invalidatedFields`.
     */
    composeFieldChanges(fieldId, change1, change2, idAllocator, crossFieldTable, revisionMetadata) {
        const { fieldKind, changeHandler, change1: change1Normalized, change2: change2Normalized, } = this.normalizeFieldChanges(change1, change2);
        const manager = new ComposeManager(crossFieldTable, change1, fieldId);
        const composedChange = changeHandler.rebaser.compose(change1Normalized, change2Normalized, (child1, child2) => {
            if (child1 !== undefined && child2 !== undefined) {
                setInChangeAtomIdMap(crossFieldTable.newToBaseNodeId, child2, child1);
                crossFieldTable.pendingCompositions.nodeIdsToCompose.push([child1, child2]);
            }
            return child1 ?? child2 ?? fail(0xb23 /* Should not compose two undefined nodes */);
        }, idAllocator, manager, revisionMetadata);
        const composedField = {
            fieldKind,
            change: brand(composedChange),
        };
        crossFieldTable.fieldToContext.set(change1, {
            fieldId,
            change1: change1Normalized,
            change2: change2Normalized,
            composedChange: composedField,
        });
        crossFieldTable.newFieldToBaseField.set(change2, change1);
        return composedField;
    }
    composeNodesById(nodeChanges1, nodeChanges2, composedNodes, composedNodeToParent, nodeAliases, id1, id2, idAllocator, crossFieldTable, revisionMetadata) {
        const nodeChangeset1 = nodeChangeFromId(nodeChanges1, id1);
        const nodeChangeset2 = nodeChangeFromId(nodeChanges2, id2);
        const composedNodeChangeset = this.composeNodeChanges(id1, nodeChangeset1, nodeChangeset2, idAllocator, crossFieldTable, revisionMetadata);
        setInChangeAtomIdMap(composedNodes, id1, composedNodeChangeset);
        if (!areEqualChangeAtomIds(id1, id2)) {
            composedNodes.delete([id2.revision, id2.localId]);
            composedNodeToParent.delete([id2.revision, id2.localId]);
            setInChangeAtomIdMap(nodeAliases, id2, id1);
            // We need to delete id1 to avoid forming a cycle in case id1 already had an alias.
            nodeAliases.delete([id1.revision, id1.localId]);
        }
        crossFieldTable.composedNodes.add(composedNodeChangeset);
    }
    composeNodeChanges(nodeId, change1, change2, genId, crossFieldTable, revisionMetadata) {
        // WARNING: this composition logic assumes that we never make compositions of the following form:
        // change1: a changeset that impact the existence of a node
        // change2: a node-exists constraint on that node.
        // This is currently enforced by the fact that constraints which apply to the input context are included first in the composition.
        // If that weren't the case, we would need to rebase the status of the constraint backward over the changes from change1.
        const nodeExistsConstraint = change1.nodeExistsConstraint ?? change2.nodeExistsConstraint;
        // WARNING: this composition logic assumes that we never make compositions of the following form:
        // change1: a node-exists-on-revert constraint on a node
        // change2: a changeset that impacts the existence of that node
        // This is currently enforced by the fact that constraints which apply to the revert are included last in the composition.
        // If that weren't the case, we would need to rebase the status of the constraint forward over the changes from change2.
        const nodeExistsConstraintOnRevert = change1.nodeExistsConstraintOnRevert ?? change2.nodeExistsConstraintOnRevert;
        const composedFieldChanges = this.composeFieldMaps(change1.fieldChanges, change2.fieldChanges, nodeId, genId, crossFieldTable, revisionMetadata);
        const composedNodeChange = {};
        if (composedFieldChanges.size > 0) {
            composedNodeChange.fieldChanges = composedFieldChanges;
        }
        if (nodeExistsConstraint !== undefined) {
            composedNodeChange.nodeExistsConstraint = nodeExistsConstraint;
        }
        if (nodeExistsConstraintOnRevert !== undefined) {
            composedNodeChange.nodeExistsConstraintOnRevert = nodeExistsConstraintOnRevert;
        }
        return composedNodeChange;
    }
    /**
     * @param change - The change to invert.
     * @param isRollback - Whether the inverted change is meant to rollback a change on a branch as is the case when
     * performing a sandwich rebase.
     * @param revisionForInvert - The revision for the invert changeset.
     */
    invert(change, isRollback, revisionForInvert) {
        // Rollback changesets destroy the nodes created by the change being rolled back.
        const destroys = isRollback ? invertBuilds(change.change.builds) : undefined;
        // Destroys only occur in rollback changesets, which are never inverted.
        assert(change.change.destroys === undefined, 0x89a /* Unexpected destroys in change to invert */);
        const revInfos = isRollback
            ? [{ revision: revisionForInvert, rollbackOf: change.revision }]
            : [{ revision: revisionForInvert }];
        const noChangeConstraint = change.change.noChangeConstraintOnRevert;
        const noChangeConstraintOnRevert = change.change.noChangeConstraint;
        if (hasConflicts(change.change)) {
            return makeModularChangeset({
                maxId: change.change.maxId,
                revisions: revInfos,
                destroys,
            });
        }
        const genId = idAllocatorFromMaxId(change.change.maxId ?? -1);
        const crossFieldTable = {
            ...newCrossFieldTable(),
            originalFieldToContext: new Map(),
            invertedNodeToParent: brand(change.change.nodeToParent.clone()),
        };
        const { revInfos: oldRevInfos } = getRevInfoFromTaggedChanges([change]);
        const revisionMetadata = revisionMetadataSourceFromInfo(oldRevInfos);
        const invertedFields = this.invertFieldMap(change.change.fieldChanges, undefined, isRollback, genId, crossFieldTable, revisionMetadata, revisionForInvert);
        const invertedNodes = newChangeAtomIdBTree();
        change.change.nodeChanges.forEachPair(([revision, localId], nodeChangeset) => {
            invertedNodes.set([revision, localId], this.invertNodeChange(nodeChangeset, { revision, localId }, isRollback, genId, crossFieldTable, revisionMetadata, revisionForInvert));
        });
        if (crossFieldTable.invalidatedFields.size > 0) {
            const fieldsToUpdate = crossFieldTable.invalidatedFields;
            crossFieldTable.invalidatedFields = new Set();
            for (const fieldChange of fieldsToUpdate) {
                const originalFieldChange = fieldChange.change;
                const context = crossFieldTable.originalFieldToContext.get(fieldChange);
                assert(context !== undefined, 0x851 /* Should have context for every invalidated field */);
                const { invertedField, fieldId } = context;
                const amendedChange = getChangeHandler(this.fieldKinds, fieldChange.fieldKind).rebaser.invert(originalFieldChange, isRollback, genId, revisionForInvert, new InvertManager(crossFieldTable, fieldChange, fieldId), revisionMetadata);
                invertedField.change = brand(amendedChange);
            }
        }
        const crossFieldKeys = this.makeCrossFieldKeyTable(invertedFields, invertedNodes);
        return makeModularChangeset({
            fieldChanges: invertedFields,
            nodeChanges: invertedNodes,
            nodeToParent: crossFieldTable.invertedNodeToParent,
            nodeAliases: change.change.nodeAliases,
            crossFieldKeys,
            maxId: genId.getMaxId(),
            revisions: revInfos,
            constraintViolationCount: change.change.constraintViolationCountOnRevert,
            constraintViolationCountOnRevert: change.change.constraintViolationCount,
            noChangeConstraint,
            noChangeConstraintOnRevert,
            destroys,
        });
    }
    invertFieldMap(changes, parentId, isRollback, genId, crossFieldTable, revisionMetadata, revisionForInvert) {
        const invertedFields = new Map();
        for (const [field, fieldChange] of changes) {
            const fieldId = { nodeId: parentId, field };
            const manager = new InvertManager(crossFieldTable, fieldChange, fieldId);
            const invertedChange = getChangeHandler(this.fieldKinds, fieldChange.fieldKind).rebaser.invert(fieldChange.change, isRollback, genId, revisionForInvert, manager, revisionMetadata);
            const invertedFieldChange = {
                ...fieldChange,
                change: brand(invertedChange),
            };
            invertedFields.set(field, invertedFieldChange);
            crossFieldTable.originalFieldToContext.set(fieldChange, {
                fieldId,
                invertedField: invertedFieldChange,
            });
        }
        return invertedFields;
    }
    invertNodeChange(change, id, isRollback, genId, crossFieldTable, revisionMetadata, revisionForInvert) {
        const inverse = {};
        // If the node has a constraint, it should be inverted to a node-exist-on-revert constraint. This ensure that if
        // the inverse is inverted again, the original input constraint will be restored.
        if (change.nodeExistsConstraint !== undefined) {
            inverse.nodeExistsConstraintOnRevert = change.nodeExistsConstraint;
        }
        // The node-exist-on-revert constraint of a node is the constraint that should apply when the a change is reverted.
        // So, it should become the constraint in the inverse. If this constraint is violated when applying the inverse,
        // it will be discarded.
        if (change.nodeExistsConstraintOnRevert !== undefined) {
            inverse.nodeExistsConstraint = change.nodeExistsConstraintOnRevert;
        }
        if (change.fieldChanges !== undefined) {
            inverse.fieldChanges = this.invertFieldMap(change.fieldChanges, id, isRollback, genId, crossFieldTable, revisionMetadata, revisionForInvert);
        }
        return inverse;
    }
    rebase(taggedChange, potentiallyConflictedOver, revisionMetadata, ignoreNoChangeViolation = false) {
        // Our current cell ordering scheme in sequences depends on being able to rebase over a change with conflicts.
        // This means that we must rebase over a muted version of the conflicted changeset.
        // That is, a version that includes its declarations (e.g., new cells) but not its changes.
        // TODO: remove once AB#46104 is completed
        const over = mapTaggedChange(potentiallyConflictedOver, this.getEffectiveChange(potentiallyConflictedOver.change));
        const change = taggedChange.change;
        const maxId = Math.max(change.maxId ?? -1, over.change.maxId ?? -1);
        const idState = { maxId };
        const genId = idAllocatorFromState(idState);
        const crossFieldTable = {
            ...newCrossFieldTable(),
            newChange: change,
            baseChange: over.change,
            baseFieldToContext: new Map(),
            baseToRebasedNodeId: newChangeAtomIdBTree(),
            rebasedFields: new Set(),
            rebasedNodeToParent: brand(change.nodeToParent.clone()),
            rebasedCrossFieldKeys: change.crossFieldKeys.clone(),
            nodeIdPairs: [],
            affectedBaseFields: newFieldIdKeyBTree(),
            fieldsWithUnattachedChild: new Set(),
        };
        const getBaseRevisions = () => revisionInfoFromTaggedChange(over).map((info) => info.revision);
        const rebaseMetadata = {
            ...revisionMetadata,
            getRevisionToRebase: () => taggedChange.revision,
            getBaseRevisions,
        };
        const rebasedNodes = brand(change.nodeChanges.clone());
        const rebasedFields = this.rebaseIntersectingFields(crossFieldTable, rebasedNodes, genId, rebaseMetadata);
        this.rebaseInvalidatedElements(rebasedFields, rebasedNodes, crossFieldTable, rebaseMetadata, genId);
        const constraintState = newConstraintState(change.constraintViolationCount ?? 0);
        const revertConstraintState = newConstraintState(change.constraintViolationCountOnRevert ?? 0);
        let noChangeConstraint = change.noChangeConstraint;
        if (noChangeConstraint !== undefined &&
            !noChangeConstraint.violated &&
            !ignoreNoChangeViolation) {
            noChangeConstraint = { violated: true };
            constraintState.violationCount += 1;
        }
        this.updateConstraintsForFields(rebasedFields, NodeAttachState.Attached, NodeAttachState.Attached, constraintState, revertConstraintState, rebasedNodes);
        const rebased = makeModularChangeset({
            fieldChanges: this.pruneFieldMap(rebasedFields, rebasedNodes),
            nodeChanges: rebasedNodes,
            nodeToParent: crossFieldTable.rebasedNodeToParent,
            nodeAliases: change.nodeAliases,
            crossFieldKeys: crossFieldTable.rebasedCrossFieldKeys,
            maxId: idState.maxId,
            revisions: change.revisions,
            constraintViolationCount: constraintState.violationCount,
            constraintViolationCountOnRevert: revertConstraintState.violationCount,
            noChangeConstraint,
            noChangeConstraintOnRevert: change.noChangeConstraintOnRevert,
            builds: change.builds,
            destroys: change.destroys,
            refreshers: change.refreshers,
        });
        return rebased;
    }
    // This performs a first pass on all fields which have both new and base changes.
    // TODO: Can we also handle additional passes in this method?
    rebaseIntersectingFields(crossFieldTable, rebasedNodes, genId, metadata) {
        const change = crossFieldTable.newChange;
        const baseChange = crossFieldTable.baseChange;
        const rebasedFields = this.rebaseFieldMap(change.fieldChanges, baseChange.fieldChanges, undefined, genId, crossFieldTable, metadata);
        // This loop processes all fields which have both base and new changes.
        // Note that the call to `rebaseNodeChange` can add entries to `crossFieldTable.nodeIdPairs`.
        for (const [newId, baseId, _attachState] of crossFieldTable.nodeIdPairs) {
            const rebasedNode = this.rebaseNodeChange(newId, baseId, genId, crossFieldTable, metadata);
            setInChangeAtomIdMap(rebasedNodes, newId, rebasedNode);
        }
        return rebasedFields;
    }
    // This processes fields which have no new changes but have been invalidated by another field.
    rebaseFieldsWithoutNewChanges(rebasedFields, rebasedNodes, crossFieldTable, genId, metadata) {
        const baseChange = crossFieldTable.baseChange;
        for (const [revision, localId, fieldKey] of crossFieldTable.affectedBaseFields.keys()) {
            const baseNodeId = localId === undefined
                ? undefined
                : normalizeNodeId({ revision, localId }, baseChange.nodeAliases);
            const baseFieldChange = fieldMapFromNodeId(baseChange.fieldChanges, baseChange.nodeChanges, baseNodeId).get(fieldKey);
            assert(baseFieldChange !== undefined, 0x9c2 /* Cross field key registered for empty field */);
            if (crossFieldTable.baseFieldToContext.has(baseFieldChange)) {
                // This field has already been processed because there were changes to rebase.
                continue;
            }
            // This field has no changes in the new changeset, otherwise it would have been added to
            // `crossFieldTable.baseFieldToContext` when processing fields with both base and new changes.
            const handler = getChangeHandler(this.fieldKinds, baseFieldChange.fieldKind);
            const fieldChange = {
                ...baseFieldChange,
                change: brand(handler.createEmpty()),
            };
            const rebasedNodeId = baseNodeId === undefined
                ? undefined
                : rebasedNodeIdFromBaseNodeId(crossFieldTable, baseNodeId);
            const fieldId = { nodeId: rebasedNodeId, field: fieldKey };
            const rebasedField = handler.rebaser.rebase(fieldChange.change, baseFieldChange.change, noNewChangesRebaseChild, genId, new RebaseManager(crossFieldTable, baseFieldChange, fieldId), metadata);
            const rebasedFieldChange = {
                ...baseFieldChange,
                change: brand(rebasedField),
            };
            // TODO: Deduplicate
            crossFieldTable.baseFieldToContext.set(baseFieldChange, {
                newChange: fieldChange,
                baseChange: baseFieldChange,
                rebasedChange: rebasedFieldChange,
                fieldId,
                baseNodeIds: [],
            });
            crossFieldTable.rebasedFields.add(rebasedFieldChange);
            this.attachRebasedField(rebasedFields, rebasedNodes, crossFieldTable, rebasedFieldChange, fieldId, genId, metadata);
        }
    }
    rebaseInvalidatedElements(rebasedFields, rebasedNodes, table, metadata, idAllocator) {
        this.rebaseFieldsWithoutNewChanges(rebasedFields, rebasedNodes, table, idAllocator, metadata);
        this.rebaseFieldsWithUnattachedChild(table, metadata, idAllocator);
        this.rebaseInvalidatedFields(table, metadata, idAllocator);
    }
    rebaseInvalidatedFields(crossFieldTable, rebaseMetadata, genId) {
        const fieldsToUpdate = crossFieldTable.invalidatedFields;
        crossFieldTable.invalidatedFields = new Set();
        for (const field of fieldsToUpdate) {
            this.rebaseInvalidatedField(field, crossFieldTable, rebaseMetadata, genId);
        }
    }
    rebaseFieldsWithUnattachedChild(table, metadata, idAllocator) {
        for (const field of table.fieldsWithUnattachedChild) {
            table.invalidatedFields.delete(field);
            this.rebaseInvalidatedField(field, table, metadata, idAllocator, true);
        }
    }
    rebaseInvalidatedField(baseField, crossFieldTable, rebaseMetadata, genId, allowInval = false) {
        const context = crossFieldTable.baseFieldToContext.get(baseField);
        assert(context !== undefined, 0x852 /* Every field should have a context */);
        const { changeHandler, change1: fieldChangeset, change2: baseChangeset, } = this.normalizeFieldChanges(context.newChange, context.baseChange);
        const rebaseChild = (curr, base) => {
            if (curr !== undefined) {
                return curr;
            }
            if (base !== undefined) {
                for (const id of context.baseNodeIds) {
                    if (areEqualChangeAtomIds(base, id)) {
                        return base;
                    }
                }
            }
            return undefined;
        };
        context.rebasedChange.change = brand(changeHandler.rebaser.rebase(fieldChangeset, baseChangeset, rebaseChild, genId, new RebaseManager(crossFieldTable, baseField, context.fieldId, allowInval), rebaseMetadata));
    }
    attachRebasedField(rebasedFields, rebasedNodes, table, rebasedField, { nodeId, field: fieldKey }, idAllocator, metadata) {
        if (nodeId === undefined) {
            rebasedFields.set(fieldKey, rebasedField);
            return;
        }
        const rebasedNode = getFromChangeAtomIdMap(rebasedNodes, nodeId);
        if (rebasedNode !== undefined) {
            if (rebasedNode.fieldChanges === undefined) {
                rebasedNode.fieldChanges = new Map([[fieldKey, rebasedField]]);
                return;
            }
            assert(!rebasedNode.fieldChanges.has(fieldKey), 0x9c4 /* Expected an empty field */);
            rebasedNode.fieldChanges.set(fieldKey, rebasedField);
            return;
        }
        const newNode = {
            fieldChanges: new Map([[fieldKey, rebasedField]]),
        };
        setInChangeAtomIdMap(rebasedNodes, nodeId, newNode);
        setInChangeAtomIdMap(table.baseToRebasedNodeId, nodeId, nodeId);
        const parentFieldId = getParentFieldId(table.baseChange, nodeId);
        this.attachRebasedNode(rebasedFields, rebasedNodes, table, nodeId, parentFieldId, idAllocator, metadata);
    }
    attachRebasedNode(rebasedFields, rebasedNodes, table, baseNodeId, parentFieldIdBase, idAllocator, metadata) {
        const baseFieldChange = fieldChangeFromId(table.baseChange.fieldChanges, table.baseChange.nodeChanges, parentFieldIdBase);
        const rebasedFieldId = rebasedFieldIdFromBaseId(table, parentFieldIdBase);
        setInChangeAtomIdMap(table.rebasedNodeToParent, baseNodeId, rebasedFieldId);
        const context = table.baseFieldToContext.get(baseFieldChange);
        if (context !== undefined) {
            // We've already processed this field.
            // The new child node will be attached in rebaseFieldsWithUnattachedChild.
            context.baseNodeIds.push(baseNodeId);
            table.fieldsWithUnattachedChild.add(baseFieldChange);
            return;
        }
        const handler = getChangeHandler(this.fieldKinds, baseFieldChange.fieldKind);
        const fieldChange = {
            ...baseFieldChange,
            change: brand(handler.createEmpty()),
        };
        const rebasedChangeset = handler.rebaser.rebase(handler.createEmpty(), baseFieldChange.change, (_idNew, idBase) => idBase !== undefined && areEqualChangeAtomIds(idBase, baseNodeId)
            ? baseNodeId
            : undefined, idAllocator, new RebaseManager(table, baseFieldChange, rebasedFieldId), metadata);
        const rebasedField = { ...baseFieldChange, change: brand(rebasedChangeset) };
        table.rebasedFields.add(rebasedField);
        table.baseFieldToContext.set(baseFieldChange, {
            newChange: fieldChange,
            baseChange: baseFieldChange,
            rebasedChange: rebasedField,
            fieldId: rebasedFieldId,
            baseNodeIds: [],
        });
        this.attachRebasedField(rebasedFields, rebasedNodes, table, rebasedField, rebasedFieldId, idAllocator, metadata);
    }
    rebaseFieldMap(change, over, parentId, genId, crossFieldTable, revisionMetadata) {
        const rebasedFields = new Map();
        const rebaseChild = (child, baseChild, stateChange) => {
            if (child !== undefined && baseChild !== undefined) {
                crossFieldTable.nodeIdPairs.push([child, baseChild, stateChange]);
            }
            return child;
        };
        for (const [field, fieldChange] of change) {
            const fieldId = { nodeId: parentId, field };
            const baseChange = over.get(field);
            if (baseChange === undefined) {
                rebasedFields.set(field, fieldChange);
                continue;
            }
            const { fieldKind, changeHandler, change1: fieldChangeset, change2: baseChangeset, } = this.normalizeFieldChanges(fieldChange, baseChange);
            const manager = new RebaseManager(crossFieldTable, baseChange, fieldId);
            const rebasedField = changeHandler.rebaser.rebase(fieldChangeset, baseChangeset, rebaseChild, genId, manager, revisionMetadata);
            const rebasedFieldChange = {
                fieldKind,
                change: brand(rebasedField),
            };
            rebasedFields.set(field, rebasedFieldChange);
            crossFieldTable.baseFieldToContext.set(baseChange, {
                baseChange,
                newChange: fieldChange,
                rebasedChange: rebasedFieldChange,
                fieldId,
                baseNodeIds: [],
            });
            crossFieldTable.rebasedFields.add(rebasedFieldChange);
        }
        return rebasedFields;
    }
    rebaseNodeChange(newId, baseId, genId, crossFieldTable, revisionMetadata) {
        const change = nodeChangeFromId(crossFieldTable.newChange.nodeChanges, newId);
        const over = nodeChangeFromId(crossFieldTable.baseChange.nodeChanges, baseId);
        const baseMap = over?.fieldChanges ?? new Map();
        const fieldChanges = change.fieldChanges !== undefined && over.fieldChanges !== undefined
            ? this.rebaseFieldMap(change?.fieldChanges ?? new Map(), baseMap, newId, genId, crossFieldTable, revisionMetadata)
            : change.fieldChanges;
        const rebasedChange = {};
        if (fieldChanges !== undefined && fieldChanges.size > 0) {
            rebasedChange.fieldChanges = fieldChanges;
        }
        if (change?.nodeExistsConstraint !== undefined) {
            rebasedChange.nodeExistsConstraint = change.nodeExistsConstraint;
        }
        if (change?.nodeExistsConstraintOnRevert !== undefined) {
            rebasedChange.nodeExistsConstraintOnRevert = change.nodeExistsConstraintOnRevert;
        }
        setInChangeAtomIdMap(crossFieldTable.baseToRebasedNodeId, baseId, newId);
        return rebasedChange;
    }
    updateConstraintsForFields(fields, parentInputAttachState, parentOutputAttachState, constraintState, revertConstraintState, nodes) {
        for (const field of fields.values()) {
            const handler = getChangeHandler(this.fieldKinds, field.fieldKind);
            for (const [nodeId, inputIndex, outputIndex] of handler.getNestedChanges(field.change)) {
                const isInputDetached = inputIndex === undefined;
                const inputAttachState = parentInputAttachState === NodeAttachState.Detached || isInputDetached
                    ? NodeAttachState.Detached
                    : NodeAttachState.Attached;
                const isOutputDetached = outputIndex === undefined;
                const outputAttachState = parentOutputAttachState === NodeAttachState.Detached || isOutputDetached
                    ? NodeAttachState.Detached
                    : NodeAttachState.Attached;
                this.updateConstraintsForNode(nodeId, inputAttachState, outputAttachState, nodes, constraintState, revertConstraintState);
            }
        }
    }
    updateConstraintsForNode(nodeId, inputAttachState, outputAttachState, nodes, constraintState, revertConstraintState) {
        const node = nodes.get([nodeId.revision, nodeId.localId]) ?? fail(0xb24 /* Unknown node ID */);
        if (node.nodeExistsConstraint !== undefined) {
            const isNowViolated = inputAttachState === NodeAttachState.Detached;
            if (node.nodeExistsConstraint.violated !== isNowViolated) {
                node.nodeExistsConstraint = {
                    ...node.nodeExistsConstraint,
                    violated: isNowViolated,
                };
                constraintState.violationCount += isNowViolated ? 1 : -1;
            }
        }
        if (node.nodeExistsConstraintOnRevert !== undefined) {
            const isNowViolated = outputAttachState === NodeAttachState.Detached;
            if (node.nodeExistsConstraintOnRevert.violated !== isNowViolated) {
                node.nodeExistsConstraintOnRevert = {
                    ...node.nodeExistsConstraintOnRevert,
                    violated: isNowViolated,
                };
                revertConstraintState.violationCount += isNowViolated ? 1 : -1;
            }
        }
        if (node.fieldChanges !== undefined) {
            this.updateConstraintsForFields(node.fieldChanges, inputAttachState, outputAttachState, constraintState, revertConstraintState, nodes);
        }
    }
    pruneFieldMap(changeset, nodeMap) {
        if (changeset === undefined) {
            return undefined;
        }
        const prunedChangeset = new Map();
        for (const [field, fieldChange] of changeset) {
            const handler = getChangeHandler(this.fieldKinds, fieldChange.fieldKind);
            const prunedFieldChangeset = handler.rebaser.prune(fieldChange.change, (nodeId) => this.pruneNodeChange(nodeId, nodeMap));
            if (!handler.isEmpty(prunedFieldChangeset)) {
                prunedChangeset.set(field, { ...fieldChange, change: brand(prunedFieldChangeset) });
            }
        }
        return prunedChangeset.size > 0 ? prunedChangeset : undefined;
    }
    pruneNodeChange(nodeId, nodeMap) {
        const changeset = nodeChangeFromId(nodeMap, nodeId);
        const prunedFields = changeset.fieldChanges === undefined
            ? undefined
            : this.pruneFieldMap(changeset.fieldChanges, nodeMap);
        const prunedChange = { ...changeset, fieldChanges: prunedFields };
        if (prunedChange.fieldChanges === undefined) {
            delete prunedChange.fieldChanges;
        }
        if (isEmptyNodeChangeset(prunedChange)) {
            nodeMap.delete([nodeId.revision, nodeId.localId]);
            return undefined;
        }
        else {
            setInChangeAtomIdMap(nodeMap, nodeId, prunedChange);
            return nodeId;
        }
    }
    getRevisions(change) {
        if (change.revisions === undefined || change.revisions.length === 0) {
            return new Set([undefined]);
        }
        const aggregated = new Set();
        for (const revInfo of change.revisions) {
            aggregated.add(revInfo.revision);
        }
        return aggregated;
    }
    changeRevision(change, replacer) {
        const updatedFields = this.replaceFieldMapRevisions(change.fieldChanges, replacer);
        const updatedNodes = replaceIdMapRevisions(change.nodeChanges, replacer, (nodeChangeset) => this.replaceNodeChangesetRevisions(nodeChangeset, replacer));
        const updatedNodeToParent = replaceIdMapRevisions(change.nodeToParent, replacer, (fieldId) => replaceFieldIdRevision(normalizeFieldId(fieldId, change.nodeAliases), replacer));
        const updated = {
            ...change,
            fieldChanges: updatedFields,
            nodeChanges: updatedNodes,
            nodeToParent: updatedNodeToParent,
            // We've updated all references to old node IDs, so we no longer need an alias table.
            nodeAliases: newChangeAtomIdBTree(),
            crossFieldKeys: replaceCrossFieldKeyTableRevisions(change.crossFieldKeys, replacer, change.nodeAliases),
        };
        if (change.builds !== undefined) {
            updated.builds = replaceIdMapRevisions(change.builds, replacer);
        }
        if (change.destroys !== undefined) {
            updated.destroys = replaceIdMapRevisions(change.destroys, replacer);
        }
        if (change.refreshers !== undefined) {
            updated.refreshers = replaceIdMapRevisions(change.refreshers, replacer);
        }
        updated.revisions = [{ revision: replacer.updatedRevision }];
        return updated;
    }
    replaceNodeChangesetRevisions(nodeChangeset, replacer) {
        const updated = { ...nodeChangeset };
        if (nodeChangeset.fieldChanges !== undefined) {
            updated.fieldChanges = this.replaceFieldMapRevisions(nodeChangeset.fieldChanges, replacer);
        }
        return updated;
    }
    replaceFieldMapRevisions(fields, replacer) {
        const updatedFields = new Map();
        for (const [field, fieldChange] of fields) {
            const updatedFieldChange = getChangeHandler(this.fieldKinds, fieldChange.fieldKind).rebaser.replaceRevisions(fieldChange.change, replacer);
            updatedFields.set(field, { ...fieldChange, change: brand(updatedFieldChange) });
        }
        return updatedFields;
    }
    makeCrossFieldKeyTable(fields, nodes) {
        const keys = newCrossFieldKeyTable();
        this.populateCrossFieldKeyTableForFieldMap(keys, fields, undefined);
        nodes.forEachPair(([revision, localId], node) => {
            if (node.fieldChanges !== undefined) {
                this.populateCrossFieldKeyTableForFieldMap(keys, node.fieldChanges, {
                    revision,
                    localId,
                });
            }
        });
        return keys;
    }
    populateCrossFieldKeyTableForFieldMap(table, fields, parent) {
        for (const [fieldKey, fieldChange] of fields) {
            const keys = getChangeHandler(this.fieldKinds, fieldChange.fieldKind).getCrossFieldKeys(fieldChange.change);
            for (const { key, count } of keys) {
                table.set(key, count, { nodeId: parent, field: fieldKey });
            }
        }
    }
    buildEditor(mintRevisionTag, changeReceiver) {
        return new ModularEditBuilder(this, this.fieldKinds, changeReceiver, this.codecOptions);
    }
    createEmptyFieldChange(fieldKind) {
        const emptyChange = getChangeHandler(this.fieldKinds, fieldKind).createEmpty();
        return { fieldKind, change: brand(emptyChange) };
    }
    validateChangeset(change) {
        let numNodes = this.validateFieldChanges(change, change.fieldChanges, undefined);
        for (const [[revision, localId], node] of change.nodeChanges.entries()) {
            if (node.fieldChanges === undefined) {
                continue;
            }
            const nodeId = { revision, localId };
            const numChildren = this.validateFieldChanges(change, node.fieldChanges, nodeId);
            numNodes += numChildren;
        }
        assert(numNodes === change.nodeChanges.size, 0xa4d /* Node table contains unparented nodes */);
    }
    /**
     * Asserts that each child and cross field key in each field has a correct entry in
     * `nodeToParent` or `crossFieldKeyTable`.
     * @returns the number of children found.
     */
    validateFieldChanges(change, fieldChanges, nodeParent) {
        let numChildren = 0;
        for (const [field, fieldChange] of fieldChanges.entries()) {
            const fieldId = { nodeId: nodeParent, field };
            const handler = getChangeHandler(this.fieldKinds, fieldChange.fieldKind);
            for (const [child, _index] of handler.getNestedChanges(fieldChange.change)) {
                const parentFieldId = getParentFieldId(change, child);
                assert(areEqualFieldIds(parentFieldId, fieldId), 0xa4e /* Inconsistent node parentage */);
                numChildren += 1;
            }
            for (const keyRange of handler.getCrossFieldKeys(fieldChange.change)) {
                const fields = getFieldsForCrossFieldKey(change, keyRange.key, keyRange.count);
                assert(fields.length === 1 &&
                    fields[0] !== undefined &&
                    areEqualFieldIds(fields[0], fieldId), 0xa4f /* Inconsistent cross field keys */);
            }
        }
        return numChildren;
    }
    getEffectiveChange(change) {
        if (hasConflicts(change)) {
            return this.muteChange(change);
        }
        return change;
    }
    /**
     * Returns a copy of the given changeset with the same declarations (e.g., new cells) but no actual changes.
     */
    muteChange(change) {
        const muted = {
            ...change,
            crossFieldKeys: newCrossFieldKeyTable(),
            fieldChanges: this.muteFieldChanges(change.fieldChanges),
            nodeChanges: brand(change.nodeChanges.mapValues((v) => this.muteNodeChange(v))),
        };
        return muted;
    }
    muteNodeChange(change) {
        if (change.fieldChanges === undefined) {
            return change;
        }
        return {
            ...change,
            fieldChanges: this.muteFieldChanges(change.fieldChanges),
        };
    }
    muteFieldChanges(change) {
        return new Map(Array.from(change.entries(), ([key, value]) => [key, this.muteFieldChange(value)]));
    }
    muteFieldChange(change) {
        const handler = getChangeHandler(this.fieldKinds, change.fieldKind);
        return {
            fieldKind: change.fieldKind,
            change: brand(handler.rebaser.mute(change.change)),
        };
    }
}
ModularChangeFamily.emptyChange = makeModularChangeset();
function replaceCrossFieldKeyTableRevisions(table, replacer, nodeAliases) {
    const updated = newCrossFieldKeyTable();
    for (const entry of table.entries()) {
        const key = entry.start;
        const updatedKey = replacer.getUpdatedAtomId(key, entry.length);
        const field = entry.value;
        const normalizedFieldId = normalizeFieldId(field, nodeAliases);
        const updatedNodeId = normalizedFieldId.nodeId === undefined
            ? undefined
            : replacer.getUpdatedAtomId(normalizedFieldId.nodeId);
        const updatedValue = {
            ...normalizedFieldId,
            nodeId: updatedNodeId,
        };
        updated.set(updatedKey, entry.length, updatedValue);
    }
    return updated;
}
function replaceIdMapRevisions(map, replacer, valueMapper = (value) => value) {
    const updated = newChangeAtomIdBTree();
    for (const [[revision, localId], value] of map.entries()) {
        const newAtom = replacer.getUpdatedAtomId({ revision, localId });
        updated.set([newAtom.revision, newAtom.localId], valueMapper(value));
    }
    return updated;
}
function composeBuildsDestroysAndRefreshers(change1, change2) {
    // Duplicate builds can happen in compositions of commits that needed to include detached tree refreshers (e.g., undos):
    // In that case, it's possible for the refreshers to contain different trees because the latter
    // refresher may already reflect the changes made by the commit that includes the earlier
    // refresher. This composition includes the changes made by the commit that includes the
    // earlier refresher, so we need to include the build for the earlier refresher, otherwise
    // the produced changeset will build a tree one which those changes have already been applied
    // and also try to apply the changes again, effectively applying them twice.
    // Note that it would in principle be possible to adopt the later build and exclude from the
    // composition all the changes already reflected on the tree, but that is not something we
    // care to support at this time.
    const allBuilds = brand(mergeTupleBTrees(change1.builds ?? newChangeAtomIdBTree(), change2.builds ?? newChangeAtomIdBTree(), true));
    const allDestroys = brand(mergeTupleBTrees(change1.destroys ?? newChangeAtomIdBTree(), change2.destroys ?? newChangeAtomIdBTree()));
    const allRefreshers = brand(mergeTupleBTrees(change1.refreshers ?? newChangeAtomIdBTree(), change2.refreshers ?? newChangeAtomIdBTree(), true));
    if (change1.destroys !== undefined && change2.builds !== undefined) {
        for (const [key, chunk] of change2.builds.entries()) {
            const destroyCount = change1.destroys.get(key);
            if (destroyCount !== undefined) {
                assert(destroyCount === chunk.topLevelLength, 0x89b /* Expected build and destroy to have the same length */);
                allBuilds.delete(key);
                allDestroys.delete(key);
            }
        }
    }
    if (change1.builds !== undefined && change2.destroys !== undefined) {
        for (const [key, chunk] of change1.builds.entries()) {
            const destroyCount = change2.destroys.get(key);
            if (destroyCount !== undefined) {
                assert(destroyCount === chunk.topLevelLength, 0x9f0 /* Expected build and destroy to have the same length */);
                allBuilds.delete(key);
                allDestroys.delete(key);
            }
        }
    }
    return { allBuilds, allDestroys, allRefreshers };
}
function invertBuilds(builds) {
    if (builds !== undefined) {
        return brand(builds.mapValues((chunk) => chunk.topLevelLength));
    }
    return undefined;
}
/**
 * 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.
 * @param fieldKinds - The field kinds to delegate to.
 */
export function* relevantRemovedRoots(change, fieldKinds) {
    yield* relevantRemovedRootsFromFields(change.fieldChanges, change.nodeChanges, fieldKinds);
}
function* relevantRemovedRootsFromNode(node, nodeChanges, fieldKinds) {
    const nodeChangeset = nodeChangeFromId(nodeChanges, node);
    if (nodeChangeset.fieldChanges !== undefined) {
        yield* relevantRemovedRootsFromFields(nodeChangeset.fieldChanges, nodeChanges, fieldKinds);
    }
}
function* relevantRemovedRootsFromFields(change, nodeChanges, fieldKinds) {
    const delegate = (node) => relevantRemovedRootsFromNode(node, nodeChanges, fieldKinds);
    for (const [_, fieldChange] of change) {
        const handler = getChangeHandler(fieldKinds, fieldChange.fieldKind);
        yield* handler.relevantRemovedRoots(fieldChange.change, delegate);
    }
}
/**
 * Adds any refreshers missing from the provided change that are relevant to the change and
 * removes any refreshers from the provided change that are not relevant to the change.
 *
 * @param change - The change that possibly has missing or superfluous refreshers. Not mutated by this function.
 * @param getDetachedNode - The function to retrieve a tree chunk from the corresponding detached node id.
 * @param removedRoots - The set of removed roots that should be in memory for the given change to be applied.
 * Can be retrieved by calling {@link relevantRemovedRoots}.
 * @param requireRefreshers - when true, this function enforces that all relevant removed roots have a
 * corresponding build or refresher.
 */
export function updateRefreshers(change, getDetachedNode, removedRoots, requireRefreshers = true) {
    const refreshers = newChangeAtomIdBTree();
    const chunkLengths = new Map();
    if (change.builds !== undefined) {
        for (const [[revision, id], chunk] of change.builds.entries()) {
            const lengthTree = getOrCreate(chunkLengths, revision, () => new BTree());
            lengthTree.set(id, chunk.topLevelLength);
        }
    }
    for (const root of removedRoots) {
        if (change.builds !== undefined) {
            const lengthTree = chunkLengths.get(root.major);
            if (lengthTree !== undefined) {
                const lengthPair = lengthTree.getPairOrNextLower(root.minor);
                if (lengthPair !== undefined) {
                    const [firstMinor, length] = lengthPair;
                    // if the root minor is within the length of the minor of the retrieved pair
                    // then there's no need to check for the detached node
                    if (root.minor < firstMinor + length) {
                        continue;
                    }
                }
            }
        }
        const node = getDetachedNode(root);
        if (node === undefined) {
            assert(!requireRefreshers, 0x8cd /* detached node should exist */);
        }
        else {
            refreshers.set([root.major, brand(root.minor)], node);
        }
    }
    const { fieldChanges, nodeChanges, nodeToParent, nodeAliases, crossFieldKeys, maxId, revisions, constraintViolationCount, constraintViolationCountOnRevert, builds, destroys, } = change;
    return makeModularChangeset({
        fieldChanges,
        nodeChanges,
        nodeToParent,
        nodeAliases,
        crossFieldKeys,
        maxId: maxId,
        revisions,
        constraintViolationCount,
        constraintViolationCountOnRevert,
        builds,
        destroys,
        refreshers,
    });
}
/**
 * Converts a change into the delta format.
 *
 * @param change - The change to convert into a delta.
 * @param fieldKinds - The field kinds to delegate to.
 */
export function intoDelta(taggedChange, fieldKinds) {
    const change = taggedChange.change;
    const rootDelta = {};
    const global = [];
    const rename = [];
    if (!hasConflicts(change)) {
        // If there are no constraint violations, then tree changes apply.
        const fieldDeltas = intoDeltaImpl(change.fieldChanges, change.nodeChanges, fieldKinds, global, rename);
        if (fieldDeltas.size > 0) {
            rootDelta.fields = fieldDeltas;
        }
        if (global.length > 0) {
            rootDelta.global = global;
        }
        if (rename.length > 0) {
            rootDelta.rename = rename;
        }
    }
    // Constraint violations should not prevent nodes from being built
    if (change.builds && change.builds.size > 0) {
        rootDelta.build = copyDetachedNodes(change.builds);
    }
    if (change.destroys !== undefined && change.destroys.size > 0) {
        const destroys = [];
        for (const [[major, minor], count] of change.destroys.entries()) {
            destroys.push({
                id: makeDetachedNodeId(major, minor),
                count,
            });
        }
        rootDelta.destroy = destroys;
    }
    if (change.refreshers && change.refreshers.size > 0) {
        rootDelta.refreshers = copyDetachedNodes(change.refreshers);
    }
    return rootDelta;
}
function copyDetachedNodes(detachedNodes) {
    const copiedDetachedNodes = [];
    for (const [[major, minor], chunk] of detachedNodes.entries()) {
        if (chunk.topLevelLength > 0) {
            chunk.referenceAdded();
            copiedDetachedNodes.push({
                id: makeDetachedNodeId(major, minor),
                trees: chunk,
            });
        }
    }
    return copiedDetachedNodes.length > 0 ? copiedDetachedNodes : undefined;
}
/**
 * @param change - The change to convert into a delta.
 */
function intoDeltaImpl(change, nodeChanges, fieldKinds, global, rename) {
    const delta = new Map();
    for (const [field, fieldChange] of change) {
        const { local: fieldChanges, global: fieldGlobal, rename: fieldRename, } = getChangeHandler(fieldKinds, fieldChange.fieldKind).intoDelta(fieldChange.change, (childChange) => {
            const nodeChange = nodeChangeFromId(nodeChanges, childChange);
            return deltaFromNodeChange(nodeChange, nodeChanges, fieldKinds, global, rename);
        });
        if (fieldChanges !== undefined && fieldChanges.marks.length > 0) {
            delta.set(field, fieldChanges);
        }
        for (const c of fieldGlobal ?? []) {
            global.push(c);
        }
        for (const r of fieldRename ?? []) {
            rename.push(r);
        }
    }
    return delta;
}
function deltaFromNodeChange(change, nodeChanges, fieldKinds, global, rename) {
    if (change.fieldChanges !== undefined) {
        return intoDeltaImpl(change.fieldChanges, nodeChanges, fieldKinds, global, rename);
    }
    // TODO: update the API to allow undefined to be returned here
    return new Map();
}
/**
 * @param revInfos - This should describe the revision being rebased and all revisions in the rebase path,
 * even if not part of the current base changeset.
 * For example, when rebasing change B from a local branch [A, B, C] over a branch [X, Y], the `revInfos` must include
 * the changes [A⁻¹ X, Y, A, B] for each rebase step of B.
 * @param revisionToRebase - The revision of the changeset which is being rebased.
 * @param baseRevisions - The set of revisions in the changeset being rebased over.
 * For example, when rebasing change B from a local branch [A, B, C] over a branch [X, Y], the `baseRevisions` must include
 * revisions [A⁻¹ X, Y, A] if rebasing over the composition of all those changes, or
 * revision [A⁻¹] for the first rebase, then [X], etc. if rebasing over edits individually.
 * @returns RebaseRevisionMetadata to be passed to `FieldChangeRebaser.rebase`*
 */
export function rebaseRevisionMetadataFromInfo(revInfos, revisionToRebase, baseRevisions) {
    const filteredRevisions = [];
    for (const revision of baseRevisions) {
        if (revision !== undefined) {
            filteredRevisions.push(revision);
        }
    }
    const getBaseRevisions = () => filteredRevisions;
    return {
        ...revisionMetadataSourceFromInfo(revInfos),
        getRevisionToRebase: () => revisionToRebase,
        getBaseRevisions,
    };
}
function isEmptyNodeChangeset(change) {
    return (change.fieldChanges === undefined &&
        change.nodeExistsConstraint === undefined &&
        change.nodeExistsConstraintOnRevert === undefined);
}
export function getFieldKind(fieldKinds, kind) {
    if (kind === genericFieldKind.identifier) {
        return genericFieldKind;
    }
    const fieldKind = fieldKinds.get(kind);
    assert(fieldKind !== undefined, 0x3ad /* Unknown field kind */);
    return fieldKind;
}
export function getChangeHandler(fieldKinds, kind) {
    return getFieldKind(fieldKinds, kind).changeHandler;
}
function newComposeTable(baseChange, newChange, composedNodeToParent) {
    return {
        ...newCrossFieldTable(),
        baseChange,
        newChange,
        fieldToContext: new Map(),
        newFieldToBaseField: new Map(),
        newToBaseNodeId: newChangeAtomIdBTree(),
        composedNodes: new Set(),
        composedNodeToParent,
        pendingCompositions: {
            nodeIdsToCompose: [],
            affectedBaseFields: newFieldIdKeyBTree(),
            affectedNewFields: newFieldIdKeyBTree(),
        },
    };
}
function newCrossFieldTable() {
    return {
        srcTable: newChangeAtomIdRangeMap(),
        dstTable: newChangeAtomIdRangeMap(),
        srcDependents: newChangeAtomIdRangeMap(),
        dstDependents: newChangeAtomIdRangeMap(),
        invalidatedFields: new Set(),
    };
}
function newConstraintState(violationCount) {
    return {
        violationCount,
    };
}
class CrossFieldManagerI {
    constructor(crossFieldTable, currentFieldKey, allowInval = true) {
        this.crossFieldTable = crossFieldTable;
        this.currentFieldKey = currentFieldKey;
        this.allowInval = allowInval;
    }
    set(target, revision, id, count, newValue, invalidateDependents) {
        if (invalidateDependents && this.allowInval) {
            const lastChangedId = id + count - 1;
            let firstId = id;
            while (firstId <= lastChangedId) {
                const dependentEntry = getFirstFromCrossFieldMap(this.getDependents(target), revision, firstId, lastChangedId - firstId + 1);
                if (dependentEntry.value !== undefined) {
                    this.crossFieldTable.invalidatedFields.add(dependentEntry.value);
                }
                firstId = brand(firstId + dependentEntry.length);
            }
        }
        setInCrossFieldMap(this.getMap(target), revision, id, count, newValue);
    }
    get(target, revision, id, count, addDependency) {
        if (addDependency) {
            // We assume that if there is already an entry for this ID it is because
            // a field handler has called compose on the same node multiple times.
            // In this case we only want to update the latest version, so we overwrite the dependency.
            setInCrossFieldMap(this.getDependents(target), revision, id, count, this.currentFieldKey);
        }
        return getFirstFromCrossFieldMap(this.getMap(target), revision, id, count);
    }
    getMap(target) {
        return target === CrossFieldTarget.Source
            ? this.crossFieldTable.srcTable
            : this.crossFieldTable.dstTable;
    }
    getDependents(target) {
        return target === CrossFieldTarget.Source
            ? this.crossFieldTable.srcDependents
            : this.crossFieldTable.dstDependents;
    }
}
class InvertManager extends CrossFieldManagerI {
    constructor(table, field, fieldId, allowInval = true) {
        super(table, field, allowInval);
        this.fieldId = fieldId;
    }
    onMoveIn(id) {
        setInChangeAtomIdMap(this.table.invertedNodeToParent, id, this.fieldId);
    }
    moveKey(target, revision, id, count) {
        assert(false, 0x9c5 /* Keys should not be moved manually during invert */);
    }
    get table() {
        return this.crossFieldTable;
    }
}
class RebaseManager extends CrossFieldManagerI {
    constructor(table, currentField, fieldId, allowInval = true) {
        super(table, currentField, allowInval);
        this.fieldId = fieldId;
    }
    set(target, revision, id, count, newValue, invalidateDependents) {
        if (invalidateDependents && this.allowInval) {
            const newFieldIds = getFieldsForCrossFieldKey(this.table.newChange, {
                target,
                revision,
                localId: id,
            }, count);
            assert(newFieldIds.length === 0, 0x9c6 /* TODO: Modifying a cross-field key from the new changeset is currently unsupported */);
            const baseFieldIds = getFieldsForCrossFieldKey(this.table.baseChange, {
                target,
                revision,
                localId: id,
            }, count);
            assert(baseFieldIds.length > 0, 0x9c7 /* Cross field key not registered in base or new change */);
            for (const baseFieldId of baseFieldIds) {
                this.table.affectedBaseFields.set([baseFieldId.nodeId?.revision, baseFieldId.nodeId?.localId, baseFieldId.field], true);
            }
        }
        super.set(target, revision, id, count, newValue, invalidateDependents);
    }
    onMoveIn(id) {
        setInChangeAtomIdMap(this.table.rebasedNodeToParent, id, this.fieldId);
    }
    moveKey(target, revision, id, count) {
        this.table.rebasedCrossFieldKeys.set({ target, revision, localId: id }, count, this.fieldId);
    }
    get table() {
        return this.crossFieldTable;
    }
}
// TODO: Deduplicate this with RebaseTable
class ComposeManager extends CrossFieldManagerI {
    constructor(table, currentField, fieldId, allowInval = true) {
        super(table, currentField, allowInval);
        this.fieldId = fieldId;
    }
    set(target, revision, id, count, newValue, invalidateDependents) {
        if (invalidateDependents && this.allowInval) {
            const newFieldIds = getFieldsForCrossFieldKey(this.table.newChange, {
                target,
                revision,
                localId: id,
            }, count);
            if (newFieldIds.length > 0) {
                for (const newFieldId of newFieldIds) {
                    this.table.pendingCompositions.affectedNewFields.set([newFieldId.nodeId?.revision, newFieldId.nodeId?.localId, newFieldId.field], true);
                }
            }
            else {
                const baseFieldIds = getFieldsForCrossFieldKey(this.table.baseChange, {
                    target,
                    revision,
                    localId: id,
                }, count);
                assert(baseFieldIds.length > 0, 0x9c8 /* Cross field key not registered in base or new change */);
                for (const baseFieldId of baseFieldIds) {
                    this.table.pendingCompositions.affectedBaseFields.set([baseFieldId.nodeId?.revision, baseFieldId.nodeId?.localId, baseFieldId.field], true);
                }
            }
        }
        super.set(target, revision, id, count, newValue, invalidateDependents);
    }
    onMoveIn(id) {
        setInChangeAtomIdMap(this.table.composedNodeToParent, id, this.fieldId);
    }
    moveKey(target, revision, id, count) {
        throw new Error("Moving cross-field keys during compose is currently unsupported");
    }
    get table() {
        return this.crossFieldTable;
    }
}
function makeModularChangeset(props) {
    const p = props ?? { maxId: -1 };
    const changeset = {
        fieldChanges: p.fieldChanges ?? new Map(),
        nodeChanges: p.nodeChanges ?? newChangeAtomIdBTree(),
        nodeToParent: p.nodeToParent ?? newChangeAtomIdBTree(),
        nodeAliases: p.nodeAliases ?? newChangeAtomIdBTree(),
        crossFieldKeys: p.crossFieldKeys ?? newCrossFieldKeyTable(),
    };
    if (p.revisions !== undefined && p.revisions.length > 0) {
        changeset.revisions = p.revisions;
    }
    if (p.maxId >= 0) {
        changeset.maxId = brand(p.maxId);
    }
    if (p.constraintViolationCount !== undefined && p.constraintViolationCount > 0) {
        changeset.constraintViolationCount = p.constraintViolationCount;
    }
    if (p.constraintViolationCountOnRevert !== undefined &&
        p.constraintViolationCountOnRevert > 0) {
        changeset.constraintViolationCountOnRevert = p.constraintViolationCountOnRevert;
    }
    if (p.noChangeConstraint !== undefined) {
        changeset.noChangeConstraint = p.noChangeConstraint;
    }
    if (p.noChangeConstraintOnRevert !== undefined) {
        changeset.noChangeConstraintOnRevert = p.noChangeConstraintOnRevert;
    }
    if (p.builds !== undefined && p.builds.size > 0) {
        changeset.builds = p.builds;
    }
    if (p.destroys !== undefined && p.destroys.size > 0) {
        changeset.destroys = p.destroys;
    }
    if (p.refreshers !== undefined && p.refreshers.size > 0) {
        changeset.refreshers = p.refreshers;
    }
    return changeset;
}
export class ModularEditBuilder extends EditBuilder {
    constructor(family, fieldKinds, changeReceiver, codecOptions) {
        super(family, changeReceiver);
        this.fieldKinds = fieldKinds;
        this.transactionDepth = 0;
        this.idAllocator = idAllocatorFromMaxId();
        this.codecOptions = codecOptions;
    }
    enterTransaction() {
        this.transactionDepth += 1;
        if (this.transactionDepth === 1) {
            this.idAllocator = idAllocatorFromMaxId();
        }
    }
    exitTransaction() {
        assert(this.transactionDepth > 0, 0x5b9 /* Cannot exit inexistent transaction */);
        this.transactionDepth -= 1;
        if (this.transactionDepth === 0) {
            this.idAllocator = idAllocatorFromMaxId();
        }
    }
    /**
     * Builds a new tree to use in an edit.
     *
     * @param firstId - The ID to associate with the first node
     * @param content - The node(s) to build.
     * @param revision - The revision to use for the build.
     * @returns A description of the edit that can be passed to `submitChanges`.
     * The returned object may contain an owning reference to the given TreeChunk.
     */
    buildTrees(firstId, content, revision) {
        if (content.topLevelLength === 0) {
            return { type: "global", revision };
        }
        // This content will be added to a GlobalEditDescription whose lifetime exceeds the scope of this function.
        content.referenceAdded();
        const builds = newChangeAtomIdBTree();
        builds.set([revision, firstId], content);
        return {
            type: "global",
            builds,
            revision,
        };
    }
    /**
     * Adds a change to the edit builder
     * @param field - the field which is being edited
     * @param fieldKind - the kind of the field
     * @param change - the change to the field
     * @param revision - the revision of the change
     */
    submitChange(field, fieldKind, change, revision) {
        const localCrossFieldKeys = getChangeHandler(this.fieldKinds, fieldKind).getCrossFieldKeys(change);
        const modularChange = buildModularChangesetFromField({
            path: field,
            fieldChange: { fieldKind, change },
            nodeChanges: newChangeAtomIdBTree(),
            nodeToParent: newChangeAtomIdBTree(),
            crossFieldKeys: newCrossFieldKeyTable(),
            idAllocator: this.idAllocator,
            localCrossFieldKeys,
            revision,
        });
        this.applyChange(tagChange(modularChange, revision));
    }
    submitChanges(changes, revision) {
        const modularChange = this.buildChanges(changes);
        this.applyChange(tagChange(modularChange, revision));
    }
    buildChanges(changes) {
        const revisions = new Set();
        const changeMaps = changes.map((change) => {
            revisions.add(change.revision);
            return makeAnonChange(change.type === "global"
                ? makeModularChangeset({
                    maxId: this.idAllocator.getMaxId(),
                    builds: change.builds,
                    revisions: [{ revision: change.revision }],
                })
                : buildModularChangesetFromField({
                    path: change.field,
                    fieldChange: {
                        fieldKind: change.fieldKind,
                        change: change.change,
                    },
                    nodeChanges: newChangeAtomIdBTree(),
                    nodeToParent: newChangeAtomIdBTree(),
                    crossFieldKeys: newCrossFieldKeyTable(),
                    idAllocator: this.idAllocator,
                    localCrossFieldKeys: getChangeHandler(this.fieldKinds, change.fieldKind).getCrossFieldKeys(change.change),
                    revision: change.revision,
                }));
        });
        const revInfo = [...revisions].map((revision) => ({ revision }));
        const composedChange = {
            ...this.changeFamily.rebaser.compose(changeMaps),
            revisions: revInfo,
        };
        const maxId = brand(this.idAllocator.getMaxId());
        if (maxId >= 0) {
            composedChange.maxId = maxId;
        }
        return composedChange;
    }
    generateId(count) {
        return brand(this.idAllocator.allocate(count));
    }
    addNodeExistsConstraint(path, revision) {
        const nodeChange = {
            nodeExistsConstraint: { violated: false },
        };
        this.applyChange(tagChange(buildModularChangesetFromNode({
            path,
            nodeChange,
            nodeChanges: newChangeAtomIdBTree(),
            nodeToParent: newChangeAtomIdBTree(),
            crossFieldKeys: newCrossFieldKeyTable(),
            idAllocator: this.idAllocator,
            revision,
        }), revision));
    }
    addNodeExistsConstraintOnRevert(path, revision) {
        const nodeChange = {
            nodeExistsConstraintOnRevert: { violated: false },
        };
        this.applyChange(tagChange(buildModularChangesetFromNode({
            path,
            nodeChange,
            nodeChanges: newChangeAtomIdBTree(),
            nodeToParent: newChangeAtomIdBTree(),
            crossFieldKeys: newCrossFieldKeyTable(),
            idAllocator: this.idAllocator,
            revision,
        }), revision));
    }
    addNoChangeConstraint(revision) {
        if (lt(this.codecOptions.minVersionForCollab, FluidClientVersion.v2_80)) {
            throw new UsageError(`No change constraints require min client version of at least ${FluidClientVersion.v2_80}`);
        }
        const changeset = makeModularChangeset({
            maxId: -1,
            noChangeConstraint: { violated: false },
        });
        this.applyChange(tagChange(changeset, revision));
    }
    addNoChangeConstraintOnRevert(revision) {
        if (lt(this.codecOptions.minVersionForCollab, FluidClientVersion.v2_80)) {
            throw new UsageError(`No change constraints require min client version of at least ${FluidClientVersion.v2_80}`);
        }
        const changeset = makeModularChangeset({
            maxId: -1,
            noChangeConstraintOnRevert: { violated: false },
        });
        this.applyChange(tagChange(changeset, revision));
    }
}
function buildModularChangesetFromField(props) {
    const { path, fieldChange, nodeChanges, nodeToParent, crossFieldKeys, idAllocator = idAllocatorFromMaxId(), localCrossFieldKeys = [], childId, revision, } = props;
    const fieldChanges = new Map([[path.field, fieldChange]]);
    if (path.parent === undefined) {
        for (const { key, count } of localCrossFieldKeys) {
            crossFieldKeys.set(key, count, { nodeId: undefined, field: path.field });
        }
        if (childId !== undefined) {
            setInChangeAtomIdMap(nodeToParent, childId, {
                nodeId: undefined,
                field: path.field,
            });
        }
        return makeModularChangeset({
            fieldChanges,
            nodeChanges,
            nodeToParent,
            crossFieldKeys,
            maxId: idAllocator.getMaxId(),
            revisions: [{ revision }],
        });
    }
    const nodeChangeset = {
        fieldChanges,
    };
    const parentId = { localId: brand(idAllocator.allocate()), revision };
    for (const { key, count } of localCrossFieldKeys) {
        crossFieldKeys.set(key, count, { nodeId: parentId, field: path.field });
    }
    if (childId !== undefined) {
        setInChangeAtomIdMap(nodeToParent, childId, {
            nodeId: parentId,
            field: path.field,
        });
    }
    return buildModularChangesetFromNode({
        path: path.parent,
        nodeChange: nodeChangeset,
        nodeChanges,
        nodeToParent,
        crossFieldKeys,
        idAllocator,
        revision,
        nodeId: parentId,
    });
}
function buildModularChangesetFromNode(props) {
    const { path, idAllocator, revision, nodeChanges, nodeChange, nodeId = { localId: brand(idAllocator.allocate()), revision }, } = props;
    setInChangeAtomIdMap(nodeChanges, nodeId, nodeChange);
    const fieldChangeset = genericFieldKind.changeHandler.editor.buildChildChanges([
        [path.parentIndex, nodeId],
    ]);
    const fieldChange = {
        fieldKind: genericFieldKind.identifier,
        change: fieldChangeset,
    };
    return buildModularChangesetFromField({
        ...props,
        path: { parent: path.parent, field: path.parentField },
        fieldChange,
        localCrossFieldKeys: [],
        childId: nodeId,
    });
}
function getRevInfoFromTaggedChanges(changes) {
    let maxId = -1;
    const revInfos = [];
    const revisions = new Set();
    for (const taggedChange of changes) {
        const change = taggedChange.change;
        maxId = Math.max(change.maxId ?? -1, maxId);
        const infosToAdd = revisionInfoFromTaggedChange(taggedChange);
        for (const info of infosToAdd) {
            if (!revisions.has(info.revision)) {
                revisions.add(info.revision);
                revInfos.push(info);
            }
        }
    }
    const rolledBackRevisions = [];
    for (const info of revInfos) {
        if (info.rollbackOf !== undefined) {
            rolledBackRevisions.push(info.rollbackOf);
        }
    }
    rolledBackRevisions.reverse();
    for (const revision of rolledBackRevisions) {
        if (!revisions.has(revision)) {
            revInfos.push({ revision });
        }
    }
    return { maxId: brand(maxId), revInfos };
}
function revisionInfoFromTaggedChange(taggedChange) {
    const revInfos = [];
    if (taggedChange.change.revisions !== undefined) {
        revInfos.push(...taggedChange.change.revisions);
    }
    else if (taggedChange.revision !== undefined) {
        const info = { revision: taggedChange.revision };
        if (taggedChange.rollbackOf !== undefined) {
            info.rollbackOf = taggedChange.rollbackOf;
        }
        revInfos.push(info);
    }
    return revInfos;
}
function fieldChangeFromId(fields, nodes, id) {
    const fieldMap = fieldMapFromNodeId(fields, nodes, id.nodeId);
    return fieldMap.get(id.field) ?? fail(0xb25 /* No field exists for the given ID */);
}
function fieldMapFromNodeId(rootFieldMap, nodes, nodeId) {
    if (nodeId === undefined) {
        return rootFieldMap;
    }
    const node = nodeChangeFromId(nodes, nodeId);
    assert(node.fieldChanges !== undefined, 0x9c9 /* Expected node to have field changes */);
    return node.fieldChanges;
}
function rebasedFieldIdFromBaseId(table, baseId) {
    if (baseId.nodeId === undefined) {
        return baseId;
    }
    return { ...baseId, nodeId: rebasedNodeIdFromBaseNodeId(table, baseId.nodeId) };
}
function rebasedNodeIdFromBaseNodeId(table, baseId) {
    return getFromChangeAtomIdMap(table.baseToRebasedNodeId, baseId) ?? baseId;
}
function nodeChangeFromId(nodes, id) {
    const node = getFromChangeAtomIdMap(nodes, id);
    assert(node !== undefined, 0x9ca /* Unknown node ID */);
    return node;
}
function fieldIdFromFieldIdKey([revision, localId, field]) {
    const nodeId = localId === undefined ? undefined : { revision, localId };
    return { nodeId, field };
}
function cloneNodeChangeset(nodeChangeset) {
    if (nodeChangeset.fieldChanges !== undefined) {
        return { ...nodeChangeset, fieldChanges: new Map(nodeChangeset.fieldChanges) };
    }
    return { ...nodeChangeset };
}
function replaceFieldIdRevision(fieldId, replacer) {
    if (fieldId.nodeId === undefined) {
        return fieldId;
    }
    return {
        ...fieldId,
        nodeId: replacer.getUpdatedAtomId(fieldId.nodeId),
    };
}
export function getParentFieldId(changeset, nodeId) {
    const parentId = getFromChangeAtomIdMap(changeset.nodeToParent, nodeId);
    assert(parentId !== undefined, 0x9cb /* Parent field should be defined */);
    return normalizeFieldId(parentId, changeset.nodeAliases);
}
function getFieldsForCrossFieldKey(changeset, key, count) {
    const fieldIds = [];
    for (const { value: fieldId } of changeset.crossFieldKeys.getAll(key, count)) {
        if (fieldId !== undefined) {
            fieldIds.push(normalizeFieldId(fieldId, changeset.nodeAliases));
        }
    }
    return fieldIds;
}
// This is only exported for use in test utilities.
export function normalizeFieldId(fieldId, nodeAliases) {
    return fieldId.nodeId === undefined
        ? fieldId
        : { ...fieldId, nodeId: normalizeNodeId(fieldId.nodeId, nodeAliases) };
}
/**
 * @returns The canonical form of nodeId, according to nodeAliases
 */
function normalizeNodeId(nodeId, nodeAliases) {
    let currentId = nodeId;
    while (true) {
        const dealiased = getFromChangeAtomIdMap(nodeAliases, currentId);
        if (dealiased === undefined) {
            return currentId;
        }
        currentId = dealiased;
    }
}
function hasConflicts(change) {
    return (change.constraintViolationCount ?? 0) > 0;
}
/**
 * A rebaseChild callback for fields with no new changes.
 * Asserts that there are no new changes and returns undefined.
 */
function noNewChangesRebaseChild(child, _baseChild, _stateChange) {
    assert(child === undefined, 0x9c3 /* There should be no new changes in this field */);
    return undefined;
}
function areEqualFieldIds(a, b) {
    return areEqualChangeAtomIdOpts(a.nodeId, b.nodeId) && a.field === b.field;
}
function newFieldIdKeyBTree() {
    return newTupleBTree(compareFieldIdKeys);
}
const compareFieldIdKeys = createTupleComparator([
    comparePartialRevisions,
    comparePartialChangesetLocalIds,
    (compareStrings),
]);
//# sourceMappingURL=modularChangeFamily.js.map