"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.ModularEditBuilder = exports.getChangeHandler = exports.getFieldKind = exports.revisionMetadataSourceFromInfo = exports.ModularChangeFamily = void 0;
const core_utils_1 = require("@fluidframework/core-utils");
const core_1 = require("../../core");
const util_1 = require("../../util");
const memoizedIdRangeAllocator_1 = require("../memoizedIdRangeAllocator");
const crossFieldQueries_1 = require("./crossFieldQueries");
const fieldChangeHandler_1 = require("./fieldChangeHandler");
const fieldKind_1 = require("./fieldKind");
const genericFieldKind_1 = require("./genericFieldKind");
const modularChangeCodecs_1 = require("./modularChangeCodecs");
/**
 * Implementation of ChangeFamily which delegates work in a given field to the appropriate FieldKind
 * as determined by the schema.
 */
class ModularChangeFamily {
    constructor(fieldKinds, codecOptions) {
        this.fieldKinds = fieldKinds;
        this.codecs = (0, modularChangeCodecs_1.makeModularChangeCodecFamily)(this.fieldKinds, codecOptions);
    }
    get rebaser() {
        return this;
    }
    /**
     * Produces an equivalent list of `FieldChangeset`s that all target the same {@link FieldKind}.
     * @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 FieldKind}, and the `FieldKind` that they target.
     * The returned `FieldChangeset`s may be a shallow copy of the input `FieldChange`s.
     */
    normalizeFieldChanges(changes, genId, revisionMetadata) {
        // TODO: Handle the case where changes have conflicting field kinds
        const nonGenericChange = changes.find((change) => change.fieldKind !== genericFieldKind_1.genericFieldKind.identifier);
        if (nonGenericChange === undefined) {
            // All the changes are generic
            return { fieldKind: genericFieldKind_1.genericFieldKind, changesets: changes.map((c) => c.change) };
        }
        const kind = nonGenericChange.fieldKind;
        const fieldKind = getFieldKind(this.fieldKinds, kind);
        const handler = fieldKind.changeHandler;
        const normalizedChanges = changes.map((change) => {
            if (change.fieldKind === genericFieldKind_1.genericFieldKind.identifier) {
                // The cast is based on the `fieldKind` check above
                const genericChange = change.change;
                return (0, genericFieldKind_1.convertGenericChange)(genericChange, handler, (children) => this.composeNodeChanges(children, genId, newCrossFieldTable(), revisionMetadata), genId, revisionMetadata);
            }
            return change.change;
        });
        return { fieldKind, changesets: normalizedChanges };
    }
    compose(changes) {
        const { revInfos, maxId } = getRevInfoFromTaggedChanges(changes);
        const revisionMetadata = revisionMetadataSourceFromInfo(revInfos);
        const idState = { maxId };
        const genId = (0, util_1.idAllocatorFromState)(idState);
        const crossFieldTable = newCrossFieldTable();
        const changesWithoutConstraintViolations = changes.filter((change) => (change.change.constraintViolationCount ?? 0) === 0);
        const composedFields = this.composeFieldMaps(changesWithoutConstraintViolations.map((change) => (0, core_1.tagChange)(change.change.fieldChanges, change.revision)), genId, crossFieldTable, revisionMetadata);
        if (crossFieldTable.invalidatedFields.size > 0) {
            const fieldsToUpdate = crossFieldTable.invalidatedFields;
            crossFieldTable.invalidatedFields = new Set();
            for (const field of fieldsToUpdate) {
                const amendedChange = getChangeHandler(this.fieldKinds, field.fieldKind).rebaser.amendCompose(field.change, (children) => this.composeNodeChanges(children, genId, crossFieldTable, revisionMetadata), genId, newCrossFieldManager(crossFieldTable), revisionMetadata);
                field.change = (0, util_1.brand)(amendedChange);
            }
        }
        (0, core_utils_1.assert)(crossFieldTable.invalidatedFields.size === 0, 0x59b /* Should not need more than one amend pass. */);
        return makeModularChangeset(composedFields, idState.maxId, revInfos);
    }
    composeFieldMaps(changes, genId, crossFieldTable, revisionMetadata) {
        const fieldChanges = new Map();
        for (const change of changes) {
            for (const [key, fieldChange] of change.change) {
                const fieldChangeToCompose = fieldChange.revision !== undefined || change.revision === undefined
                    ? fieldChange
                    : {
                        ...fieldChange,
                        revision: change.revision,
                    };
                (0, util_1.getOrAddEmptyToMap)(fieldChanges, key).push(fieldChangeToCompose);
            }
        }
        const composedFields = new Map();
        for (const [field, changesForField] of fieldChanges) {
            const { fieldKind, changesets } = this.normalizeFieldChanges(changesForField, genId, revisionMetadata);
            (0, core_utils_1.assert)(changesets.length === changesForField.length, 0x4a8 /* Number of changes should be constant when normalizing */);
            const manager = newCrossFieldManager(crossFieldTable);
            const taggedChangesets = changesets.map((change, i) => (0, core_1.tagChange)(change, changesForField[i].revision));
            const composedChange = fieldKind.changeHandler.rebaser.compose(taggedChangesets, (children) => this.composeNodeChanges(children, genId, crossFieldTable, revisionMetadata), genId, manager, revisionMetadata);
            const composedField = {
                fieldKind: fieldKind.identifier,
                change: (0, util_1.brand)(composedChange),
            };
            addFieldData(manager, composedField);
            // TODO: Could optimize by checking that composedField is non-empty
            composedFields.set(field, composedField);
        }
        return composedFields;
    }
    composeNodeChanges(changes, genId, crossFieldTable, revisionMetadata) {
        const fieldChanges = [];
        let nodeExistsConstraint;
        for (const change of changes) {
            // Composition is part of two codepaths:
            //   1. Combining multiple changesets into a transaction
            //   2. Generating a state update after rebasing a branch that has
            //        more than one changeset
            // In the first codepath, none of the constraints will be violated and
            // we need the constraint to be stored on the given node in the transaction.
            // In the second path, the constraint may have been violated, but the state is tracked
            // as part of `constraintViolationCount` and the change won't be rebased further.
            if (change.change.nodeExistsConstraint !== undefined) {
                nodeExistsConstraint = { ...change.change.nodeExistsConstraint };
            }
            if (change.change.fieldChanges !== undefined) {
                fieldChanges.push((0, core_1.tagChange)(change.change.fieldChanges, change.revision));
            }
        }
        const composedFieldChanges = this.composeFieldMaps(fieldChanges, genId, crossFieldTable, revisionMetadata);
        const composedNodeChange = {};
        if (composedFieldChanges.size > 0) {
            composedNodeChange.fieldChanges = composedFieldChanges;
        }
        if (nodeExistsConstraint !== undefined) {
            composedNodeChange.nodeExistsConstraint = nodeExistsConstraint;
        }
        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 repairStore - The store to query for repair data.
     */
    invert(change, isRollback) {
        // Return an empty inverse for changes with constraint violations
        if ((change.change.constraintViolationCount ?? 0) > 0) {
            return makeModularChangeset(new Map());
        }
        const idState = { maxId: change.change.maxId ?? -1 };
        // This idState is used for the whole of the IdAllocator's lifetime, which allows
        // this function to read the updated idState.maxId after more IDs are allocated.
        // TODO: add a getMax function to IdAllocator to make for a clearer contract.
        const genId = (0, util_1.idAllocatorFromState)(idState);
        const crossFieldTable = newCrossFieldTable();
        const invertedFields = this.invertFieldMap((0, core_1.tagChange)(change.change.fieldChanges, change.revision), genId, undefined, crossFieldTable);
        if (crossFieldTable.invalidatedFields.size > 0) {
            const fieldsToUpdate = crossFieldTable.invalidatedFields;
            crossFieldTable.invalidatedFields = new Set();
            for (const { fieldChange, originalRevision } of fieldsToUpdate) {
                const amendedChange = getChangeHandler(this.fieldKinds, fieldChange.fieldKind).rebaser.amendInvert(fieldChange.change, originalRevision, genId, newCrossFieldManager(crossFieldTable));
                fieldChange.change = (0, util_1.brand)(amendedChange);
            }
        }
        (0, core_utils_1.assert)(crossFieldTable.invalidatedFields.size === 0, 0x59c /* Should not need more than one amend pass. */);
        const revInfo = change.change.revisions;
        return makeModularChangeset(invertedFields, idState.maxId, revInfo === undefined
            ? undefined
            : (isRollback
                ? revInfo.map(({ revision }) => ({ revision, rollbackOf: revision }))
                : Array.from(revInfo)).reverse(), change.change.constraintViolationCount);
    }
    invertFieldMap(changes, genId, path, crossFieldTable) {
        const invertedFields = new Map();
        for (const [field, fieldChange] of changes.change) {
            const { revision } = fieldChange.revision !== undefined ? fieldChange : changes;
            const manager = newCrossFieldManager(crossFieldTable);
            const invertedChange = getChangeHandler(this.fieldKinds, fieldChange.fieldKind).rebaser.invert({ revision, change: fieldChange.change }, (childChanges, index) => this.invertNodeChange({ revision, change: childChanges }, genId, crossFieldTable, index === undefined
                ? undefined
                : {
                    parent: path,
                    parentField: field,
                    parentIndex: index,
                }), genId, manager);
            const invertedFieldChange = {
                ...fieldChange,
                change: (0, util_1.brand)(invertedChange),
            };
            invertedFields.set(field, invertedFieldChange);
            const invertData = {
                fieldKey: field,
                fieldChange: invertedFieldChange,
                path,
                originalRevision: changes.revision,
            };
            addFieldData(manager, invertData);
        }
        return invertedFields;
    }
    invertNodeChange(change, genId, crossFieldTable, path) {
        const inverse = {};
        if (change.change.fieldChanges !== undefined) {
            inverse.fieldChanges = this.invertFieldMap({ ...change, change: change.change.fieldChanges }, genId, path, crossFieldTable);
        }
        return inverse;
    }
    rebase(change, over) {
        const maxId = Math.max(change.maxId ?? -1, over.change.maxId ?? -1);
        const idState = { maxId };
        const genId = (0, util_1.idAllocatorFromState)(idState);
        const crossFieldTable = {
            ...newCrossFieldTable(),
            baseMapToRebased: new Map(),
            baseChangeToContext: new Map(),
            baseMapToParentField: new Map(),
        };
        const constraintState = newConstraintState(change.constraintViolationCount ?? 0);
        const revInfos = [];
        revInfos.push(...revisionInfoFromTaggedChange(over));
        if (change.revisions !== undefined) {
            revInfos.push(...change.revisions);
        }
        const revisionMetadata = revisionMetadataSourceFromInfo(revInfos);
        const rebasedFields = this.rebaseFieldMap(change.fieldChanges, (0, core_1.tagChange)(over.change.fieldChanges, over.revision), genId, crossFieldTable, () => true, revisionMetadata, constraintState);
        const rebasedChangeset = makeModularChangeset(rebasedFields, idState.maxId, change.revisions, constraintState.violationCount);
        crossFieldTable.baseMapToRebased.set(over.change.fieldChanges, rebasedChangeset);
        const constraintViolations = constraintState.violationCount;
        crossFieldTable.invalidatedFields.clear();
        const amendedFields = this.rebaseFieldMap(rebasedChangeset.fieldChanges, (0, core_1.tagChange)(over.change.fieldChanges, over.revision), genId, crossFieldTable, () => true, revisionMetadata, constraintState, true);
        // assert(
        // 	crossFieldTable.invalidatedFields.size === 0,
        // 	0x59f /* Should not need more than one amend pass. */,
        // );
        (0, core_utils_1.assert)(constraintState.violationCount === constraintViolations, 0x5b4 /* Should not change constraint violation count during amend pass */);
        const amendedChangeset = makeModularChangeset(amendedFields, idState.maxId, change.revisions, constraintState.violationCount);
        return amendedChangeset;
    }
    rebaseFieldMap(change, over, genId, crossFieldTable, fieldFilter, revisionMetadata, constraintState, amend = false, existenceState = fieldChangeHandler_1.NodeExistenceState.Alive) {
        const rebasedFields = new Map();
        // Rebase fields contained in the base changeset
        for (const [field, baseChanges] of over.change) {
            if (!fieldFilter(baseChanges, change.get(field))) {
                continue;
            }
            const fieldChange = change.get(field) ?? {
                fieldKind: genericFieldKind_1.genericFieldKind.identifier,
                change: (0, util_1.brand)((0, genericFieldKind_1.newGenericChangeset)()),
            };
            const { fieldKind, changesets: [fieldChangeset, baseChangeset], } = this.normalizeFieldChanges([fieldChange, baseChanges], genId, revisionMetadata);
            const { revision } = over;
            const taggedBaseChange = { revision, change: baseChangeset };
            const manager = newCrossFieldManager(crossFieldTable);
            const rebaseChild = (child, baseChild, stateChange) => this.rebaseNodeChange(child, { revision, change: baseChild }, genId, crossFieldTable, baseChanges, fieldFilter, revisionMetadata, constraintState, stateChange, amend);
            const rebasedField = !amend
                ? fieldKind.changeHandler.rebaser.rebase(fieldChangeset, taggedBaseChange, rebaseChild, genId, manager, revisionMetadata)
                : fieldKind.changeHandler.rebaser.amendRebase(fieldChangeset, taggedBaseChange, rebaseChild, genId, manager, revisionMetadata);
            if (!fieldKind.changeHandler.isEmpty(rebasedField)) {
                const rebasedFieldChange = {
                    fieldKind: fieldKind.identifier,
                    change: (0, util_1.brand)(rebasedField),
                };
                rebasedFields.set(field, rebasedFieldChange);
            }
            addFieldData(manager, baseChanges);
            crossFieldTable.baseChangeToContext.set(baseChanges, {
                map: over.change,
                field,
                revision,
            });
        }
        // Rebase the fields of the new changeset which don't have a corresponding base field.
        for (const [field, fieldChange] of change) {
            if (!over.change?.has(field)) {
                const baseChanges = {
                    fieldKind: genericFieldKind_1.genericFieldKind.identifier,
                    change: (0, util_1.brand)((0, genericFieldKind_1.newGenericChangeset)()),
                };
                const { fieldKind, changesets: [fieldChangeset, baseChangeset], } = this.normalizeFieldChanges([fieldChange, baseChanges], genId, revisionMetadata);
                const manager = newCrossFieldManager(crossFieldTable);
                const rebasedChangeset = fieldKind.changeHandler.rebaser.rebase(fieldChangeset, (0, core_1.tagChange)(baseChangeset, over.revision), (child, baseChild) => {
                    (0, core_utils_1.assert)(baseChild === undefined, 0x5b6 /* This field should not have any base changes */);
                    return this.rebaseNodeChange(child, (0, core_1.tagChange)(undefined, over.revision), genId, crossFieldTable, baseChanges, fieldFilter, revisionMetadata, constraintState, existenceState);
                }, genId, manager, revisionMetadata, existenceState);
                const rebasedFieldChange = {
                    fieldKind: fieldKind.identifier,
                    change: (0, util_1.brand)(rebasedChangeset),
                };
                rebasedFields.set(field, rebasedFieldChange);
                addFieldData(manager, rebasedFieldChange);
            }
        }
        return rebasedFields;
    }
    rebaseNodeChange(change, over, genId, crossFieldTable, parentField, fieldFilter, revisionMetadata, constraintState, existenceState = fieldChangeHandler_1.NodeExistenceState.Alive, amend = false) {
        if (change === undefined && over.change?.fieldChanges === undefined) {
            return undefined;
        }
        if (over.change?.fieldChanges !== undefined && parentField !== undefined) {
            crossFieldTable.baseMapToParentField.set(over.change.fieldChanges, parentField);
        }
        const baseMap = over.change?.fieldChanges !== undefined
            ? {
                ...over,
                change: over.change.fieldChanges,
            }
            : (0, core_1.tagChange)(new Map(), over.revision);
        const fieldChanges = this.rebaseFieldMap(change?.fieldChanges ?? new Map(), baseMap, genId, crossFieldTable, fieldFilter, revisionMetadata, constraintState, amend, existenceState);
        const rebasedChange = {};
        if (fieldChanges.size > 0) {
            rebasedChange.fieldChanges = fieldChanges;
        }
        if (change?.nodeExistsConstraint !== undefined) {
            rebasedChange.nodeExistsConstraint = change.nodeExistsConstraint;
        }
        // If there's a node exists constraint and we deleted or revived the node, update constraint state
        if (!amend && rebasedChange.nodeExistsConstraint !== undefined) {
            const violatedAfter = existenceState === fieldChangeHandler_1.NodeExistenceState.Dead;
            if (rebasedChange.nodeExistsConstraint.violated !== violatedAfter) {
                rebasedChange.nodeExistsConstraint = {
                    ...rebasedChange.nodeExistsConstraint,
                    violated: violatedAfter,
                };
                constraintState.violationCount += violatedAfter ? 1 : -1;
            }
        }
        if (isEmptyNodeChangeset(rebasedChange)) {
            return undefined;
        }
        if (over?.change?.fieldChanges !== undefined) {
            crossFieldTable.baseMapToRebased.set(over.change.fieldChanges, rebasedChange);
        }
        return rebasedChange;
    }
    intoDelta({ change, revision }) {
        // Return an empty delta for changes with constraint violations
        if ((change.constraintViolationCount ?? 0) > 0) {
            return new Map();
        }
        const idAllocator = memoizedIdRangeAllocator_1.MemoizedIdRangeAllocator.fromNextId();
        return this.intoDeltaImpl(change.fieldChanges, revision, idAllocator);
    }
    /**
     * @param change - The change to convert into a delta.
     * @param repairStore - The store to query for repair data.
     * @param path - The path of the node being altered by the change as defined by the input context.
     * Undefined for the root and for nodes that do not exist in the input context.
     */
    intoDeltaImpl(change, revision, idAllocator) {
        const delta = new Map();
        for (const [field, fieldChange] of change) {
            const fieldRevision = fieldChange.revision ?? revision;
            const deltaField = getChangeHandler(this.fieldKinds, fieldChange.fieldKind).intoDelta((0, core_1.tagChange)(fieldChange.change, fieldRevision), (childChange) => this.deltaFromNodeChange((0, core_1.tagChange)(childChange, fieldRevision), idAllocator), idAllocator);
            if (!(0, core_1.isEmptyFieldChanges)(deltaField)) {
                delta.set(field, deltaField);
            }
        }
        return delta;
    }
    deltaFromNodeChange({ change, revision }, idAllocator) {
        if (change.fieldChanges !== undefined) {
            return this.intoDeltaImpl(change.fieldChanges, revision, idAllocator);
        }
        // TODO: update the API to allow undefined to be returned here
        return new Map();
    }
    buildEditor(changeReceiver) {
        return new ModularEditBuilder(this, changeReceiver);
    }
}
exports.ModularChangeFamily = ModularChangeFamily;
/**
 * @alpha
 */
function revisionMetadataSourceFromInfo(revInfos) {
    const getIndex = (revision) => {
        const index = revInfos.findIndex((revInfo) => revInfo.revision === revision);
        (0, core_utils_1.assert)(index !== -1, 0x5a0 /* Unable to index unknown revision */);
        return index;
    };
    const tryGetInfo = (revision) => {
        return revision === undefined ? undefined : revInfos[getIndex(revision)];
    };
    return { getIndex, tryGetInfo };
}
exports.revisionMetadataSourceFromInfo = revisionMetadataSourceFromInfo;
function isEmptyNodeChangeset(change) {
    return change.fieldChanges === undefined && change.nodeExistsConstraint === undefined;
}
function getFieldKind(fieldKinds, kind) {
    if (kind === genericFieldKind_1.genericFieldKind.identifier) {
        return genericFieldKind_1.genericFieldKind;
    }
    const fieldKind = fieldKinds.get(kind);
    (0, core_utils_1.assert)(fieldKind !== undefined, 0x3ad /* Unknown field kind */);
    return (0, fieldKind_1.withEditor)(fieldKind);
}
exports.getFieldKind = getFieldKind;
function getChangeHandler(fieldKinds, kind) {
    return getFieldKind(fieldKinds, kind).changeHandler;
}
exports.getChangeHandler = getChangeHandler;
function newCrossFieldTable() {
    return {
        srcTable: new Map(),
        dstTable: new Map(),
        srcDependents: new Map(),
        dstDependents: new Map(),
        invalidatedFields: new Set(),
    };
}
function newConstraintState(violationCount) {
    return {
        violationCount,
    };
}
function newCrossFieldManager(crossFieldTable) {
    const srcQueries = new Map();
    const dstQueries = new Map();
    const getMap = (target) => target === crossFieldQueries_1.CrossFieldTarget.Source ? crossFieldTable.srcTable : crossFieldTable.dstTable;
    const getQueries = (target) => target === crossFieldQueries_1.CrossFieldTarget.Source ? srcQueries : dstQueries;
    const manager = {
        table: crossFieldTable,
        srcQueries,
        dstQueries,
        fieldInvalidated: false,
        set: (target, revision, id, count, newValue, invalidateDependents) => {
            if (invalidateDependents) {
                const dependentsMap = target === crossFieldQueries_1.CrossFieldTarget.Source
                    ? crossFieldTable.srcDependents
                    : crossFieldTable.dstDependents;
                let dependentEntry = (0, crossFieldQueries_1.getFirstFromCrossFieldMap)(dependentsMap, revision, id, count);
                const lastChangedId = id + count - 1;
                while (dependentEntry !== undefined) {
                    crossFieldTable.invalidatedFields.add(dependentEntry.value);
                    const lastEntryId = dependentEntry.start + dependentEntry.length - 1;
                    const numChangedIdsAfterEntry = lastChangedId - lastEntryId;
                    if (numChangedIdsAfterEntry > 0) {
                        dependentEntry = (0, crossFieldQueries_1.getFirstFromCrossFieldMap)(dependentsMap, revision, (0, util_1.brand)(lastEntryId + 1), numChangedIdsAfterEntry);
                    }
                    else {
                        // We have found the last entry for the changed ID range.
                        break;
                    }
                }
                if ((0, crossFieldQueries_1.getFirstFromCrossFieldMap)(getQueries(target), revision, id, count) !== undefined) {
                    manager.fieldInvalidated = true;
                }
            }
            (0, crossFieldQueries_1.setInCrossFieldMap)(getMap(target), revision, id, count, newValue);
        },
        get: (target, revision, id, count, addDependency) => {
            if (addDependency) {
                (0, crossFieldQueries_1.addCrossFieldQuery)(getQueries(target), revision, id, count);
            }
            return (0, crossFieldQueries_1.getFirstFromCrossFieldMap)(getMap(target), revision, id, count);
        },
    };
    return manager;
}
function addFieldData(manager, fieldData) {
    for (const [revision, rangeMap] of manager.srcQueries) {
        for (const range of rangeMap) {
            // 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 amend the latest version, so we overwrite the dependency.
            (0, crossFieldQueries_1.setInCrossFieldMap)(manager.table.srcDependents, revision, (0, util_1.brand)(range.start), range.length, fieldData);
        }
    }
    for (const [revision, rangeMap] of manager.dstQueries) {
        for (const range of rangeMap) {
            // See above comment
            (0, crossFieldQueries_1.setInCrossFieldMap)(manager.table.dstDependents, revision, (0, util_1.brand)(range.start), range.length, fieldData);
        }
    }
    if (manager.fieldInvalidated) {
        manager.table.invalidatedFields.add(fieldData);
    }
}
function makeModularChangeset(changes, maxId = -1, revisions = undefined, constraintViolationCount = undefined) {
    const changeset = { fieldChanges: changes };
    if (revisions !== undefined && revisions.length > 0) {
        changeset.revisions = revisions;
    }
    if (maxId >= 0) {
        changeset.maxId = (0, util_1.brand)(maxId);
    }
    if (constraintViolationCount !== undefined && constraintViolationCount > 0) {
        changeset.constraintViolationCount = constraintViolationCount;
    }
    return changeset;
}
class ModularEditBuilder extends core_1.EditBuilder {
    constructor(family, changeReceiver) {
        super(family, changeReceiver);
        this.transactionDepth = 0;
        this.idAllocator = (0, util_1.idAllocatorFromMaxId)();
    }
    enterTransaction() {
        this.transactionDepth += 1;
        if (this.transactionDepth === 1) {
            this.idAllocator = (0, util_1.idAllocatorFromMaxId)();
        }
    }
    exitTransaction() {
        (0, core_utils_1.assert)(this.transactionDepth > 0, 0x5b9 /* Cannot exit inexistent transaction */);
        this.transactionDepth -= 1;
        if (this.transactionDepth === 0) {
            this.idAllocator = (0, util_1.idAllocatorFromMaxId)();
        }
    }
    apply(change) {
        this.applyChange(change);
    }
    /**
     * 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 maxId - the highest `ChangesetLocalId` used in this change
     */
    submitChange(field, fieldKind, change, maxId = (0, util_1.brand)(-1)) {
        const changeMap = this.buildChangeMap(field, fieldKind, change);
        this.applyChange(makeModularChangeset(changeMap, maxId));
    }
    submitChanges(changes, maxId = (0, util_1.brand)(-1)) {
        const changeMaps = changes.map((change) => (0, core_1.makeAnonChange)(makeModularChangeset(this.buildChangeMap(change.field, change.fieldKind, change.change))));
        const composedChange = this.changeFamily.rebaser.compose(changeMaps);
        if (maxId >= 0) {
            composedChange.maxId = maxId;
        }
        this.applyChange(composedChange);
    }
    generateId(count) {
        return (0, util_1.brand)(this.idAllocator.allocate(count));
    }
    buildChangeMap(field, fieldKind, change) {
        let fieldChangeMap = new Map([[field.field, { fieldKind, change }]]);
        let remainingPath = field.parent;
        while (remainingPath !== undefined) {
            const nodeChange = { fieldChanges: fieldChangeMap };
            const fieldChange = genericFieldKind_1.genericFieldKind.changeHandler.editor.buildChildChange(remainingPath.parentIndex, nodeChange);
            fieldChangeMap = new Map([
                [
                    remainingPath.parentField,
                    { fieldKind: genericFieldKind_1.genericFieldKind.identifier, change: (0, util_1.brand)(fieldChange) },
                ],
            ]);
            remainingPath = remainingPath.parent;
        }
        return fieldChangeMap;
    }
    addNodeExistsConstraint(path) {
        const nodeChange = {
            nodeExistsConstraint: { violated: false },
        };
        const fieldChange = genericFieldKind_1.genericFieldKind.changeHandler.editor.buildChildChange(path.parentIndex, nodeChange);
        this.submitChange({ parent: path.parent, field: path.parentField }, genericFieldKind_1.genericFieldKind.identifier, (0, util_1.brand)(fieldChange));
    }
}
exports.ModularEditBuilder = ModularEditBuilder;
function getRevInfoFromTaggedChanges(changes) {
    let maxId = -1;
    const revInfos = [];
    for (const taggedChange of changes) {
        const change = taggedChange.change;
        maxId = Math.max(change.maxId ?? -1, maxId);
        revInfos.push(...revisionInfoFromTaggedChange(taggedChange));
    }
    return { maxId: (0, util_1.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;
}
//# sourceMappingURL=modularChangeFamily.js.map