/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { assert } from "@fluidframework/common-utils";
import { ProgressiveEditBuilderBase, Delta, tagChange, makeAnonChange, } from "../../core";
import { addToNestedSet, brand, getOrAddEmptyToMap, getOrAddInNestedMap, nestedSetContains, setInNestedMap, tryGetFromNestedMap, } from "../../util";
import { dummyRepairDataStore } from "../fakeRepairDataStore";
import { CrossFieldTarget, idAllocatorFromMaxId, idAllocatorFromState, } from "./crossFieldQueries";
import { convertGenericChange, genericFieldKind, newGenericChangeset, } from "./genericFieldKind";
import { makeModularChangeCodecFamily } from "./modularChangeEncoding";
/**
 * Implementation of ChangeFamily which delegates work in a given field to the appropriate FieldKind
 * as determined by the schema.
 *
 * @sealed
 * @alpha
 */
export class ModularChangeFamily {
    constructor(fieldKinds) {
        this.fieldKinds = fieldKinds;
        this.codecs = makeModularChangeCodecFamily(this.fieldKinds);
    }
    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.identifier);
        if (nonGenericChange === undefined) {
            // All the changes are generic
            return { fieldKind: 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.identifier) {
                // The cast is based on the `fieldKind` check above
                const genericChange = change.change;
                return 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 = idAllocatorFromState(idState);
        const crossFieldTable = newCrossFieldTable();
        const changesWithoutConstraintViolations = changes.filter((change) => { var _a; return ((_a = change.change.constraintViolationCount) !== null && _a !== void 0 ? _a : 0) === 0; });
        const composedFields = this.composeFieldMaps(changesWithoutConstraintViolations.map((change) => 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 = brand(amendedChange);
            }
        }
        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
                    : Object.assign(Object.assign({}, fieldChange), { revision: change.revision });
                getOrAddEmptyToMap(fieldChanges, key).push(fieldChangeToCompose);
            }
        }
        const composedFields = new Map();
        for (const [field, changesForField] of fieldChanges) {
            let composedField;
            if (changesForField.length === 1) {
                // BUG: This field might be affected by cross-field effects, so we must recurse into it.
                composedField = changesForField[0];
            }
            else {
                const { fieldKind, changesets } = this.normalizeFieldChanges(changesForField, genId, revisionMetadata);
                assert(changesets.length === changesForField.length, 0x4a8 /* Number of changes should be constant when normalizing */);
                const manager = newCrossFieldManager(crossFieldTable);
                const taggedChangesets = changesets.map((change, i) => tagChange(change, changesForField[i].revision));
                const composedChange = fieldKind.changeHandler.rebaser.compose(taggedChangesets, (children) => this.composeNodeChanges(children, genId, crossFieldTable, revisionMetadata), genId, manager, revisionMetadata);
                composedField = {
                    fieldKind: fieldKind.identifier,
                    change: 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) {
        var _a;
        const fieldChanges = [];
        let valueChange;
        let valueConstraint;
        for (const change of changes) {
            // Use the first defined value constraint before any value changes.
            // Any value constraints defined after a value change can never be violated so they are ignored in the composition.
            if (change.change.valueConstraint !== undefined &&
                valueConstraint === undefined &&
                valueChange === undefined) {
                valueConstraint = Object.assign({}, change.change.valueConstraint);
            }
            if (change.change.valueChange !== undefined) {
                valueChange = Object.assign({}, change.change.valueChange);
                (_a = valueChange.revision) !== null && _a !== void 0 ? _a : (valueChange.revision = change.revision);
            }
            if (change.change.fieldChanges !== undefined) {
                fieldChanges.push(tagChange(change.change.fieldChanges, change.revision));
            }
        }
        const composedFieldChanges = this.composeFieldMaps(fieldChanges, genId, crossFieldTable, revisionMetadata);
        const composedNodeChange = {};
        if (valueChange !== undefined) {
            composedNodeChange.valueChange = valueChange;
        }
        if (composedFieldChanges.size > 0) {
            composedNodeChange.fieldChanges = composedFieldChanges;
        }
        if (valueConstraint !== undefined) {
            composedNodeChange.valueConstraint = valueConstraint;
        }
        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, repairStore) {
        var _a;
        const idState = { maxId: brand((_a = change.change.maxId) !== null && _a !== void 0 ? _a : -1) };
        const genId = idAllocatorFromState(idState);
        const crossFieldTable = newCrossFieldTable();
        const resolvedRepairStore = repairStore !== null && repairStore !== void 0 ? repairStore : dummyRepairDataStore;
        const invertedFields = this.invertFieldMap(tagChange(change.change.fieldChanges, change.revision), genId, resolvedRepairStore, undefined, crossFieldTable);
        if (crossFieldTable.invalidatedFields.size > 0) {
            const fieldsToUpdate = crossFieldTable.invalidatedFields;
            crossFieldTable.invalidatedFields = new Set();
            for (const { fieldKey, fieldChange, path, originalRevision } of fieldsToUpdate) {
                const amendedChange = getChangeHandler(this.fieldKinds, fieldChange.fieldKind).rebaser.amendInvert(fieldChange.change, originalRevision, (revision, index, count) => resolvedRepairStore.getNodes(revision, path, fieldKey, index, count), genId, newCrossFieldManager(crossFieldTable));
                fieldChange.change = brand(amendedChange);
            }
        }
        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, repairStore, path, crossFieldTable) {
        const invertedFields = new Map();
        for (const [field, fieldChange] of changes.change) {
            const { revision } = fieldChange.revision !== undefined ? fieldChange : changes;
            const reviver = (revisionTag, index, count) => repairStore.getNodes(revisionTag, path, field, index, count);
            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, repairStore, index === undefined
                ? undefined
                : {
                    parent: path,
                    parentField: field,
                    parentIndex: index,
                }), reviver, genId, manager);
            const invertedFieldChange = Object.assign(Object.assign({}, fieldChange), { change: 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, repairStore, path) {
        var _a;
        const inverse = {};
        if (change.change.valueChange !== undefined) {
            assert(!("revert" in change.change.valueChange), 0x4a9 /* Inverting inverse changes is currently not supported */);
            assert(path !== undefined, 0x59d /* Only existing nodes can have their value restored */);
            const revision = (_a = change.change.valueChange.revision) !== null && _a !== void 0 ? _a : change.revision;
            assert(revision !== undefined, 0x59e /* Unable to revert to undefined revision */);
            inverse.valueChange = { value: repairStore.getValue(revision, path) };
        }
        if (change.change.fieldChanges !== undefined) {
            inverse.fieldChanges = this.invertFieldMap(Object.assign(Object.assign({}, change), { change: change.change.fieldChanges }), genId, repairStore, path, crossFieldTable);
        }
        return inverse;
    }
    rebase(change, over) {
        var _a, _b, _c;
        const maxId = Math.max((_a = change.maxId) !== null && _a !== void 0 ? _a : -1, (_b = over.change.maxId) !== null && _b !== void 0 ? _b : -1);
        const idState = { maxId: brand(maxId) };
        const genId = idAllocatorFromState(idState);
        const crossFieldTable = Object.assign(Object.assign({}, newCrossFieldTable()), { baseMapToRebased: new Map(), baseChangeToContext: new Map(), baseMapToParentField: new Map() });
        const constraintState = newConstraintState((_c = change.constraintViolationCount) !== null && _c !== void 0 ? _c : 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, 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 [fieldsToAmend, invalidatedEmptyFields] = getFieldsToAmend(crossFieldTable.invalidatedFields, crossFieldTable.baseMapToRebased, crossFieldTable.baseChangeToContext, crossFieldTable.baseMapToParentField);
        const constraintViolations = constraintState.violationCount;
        this.amendRebase(crossFieldTable, fieldsToAmend, invalidatedEmptyFields, genId, revisionMetadata, constraintState);
        assert(crossFieldTable.invalidatedFields.size === 0, 0x59f /* Should not need more than one amend pass. */);
        assert(constraintState.violationCount === constraintViolations, 0x5b4 /* Should not change constraint violation count during amend pass */);
        if (idState.maxId >= 0) {
            rebasedChangeset.maxId = brand(idState.maxId);
        }
        return rebasedChangeset;
    }
    amendRebase(crossFieldTable, fieldsToAmend, invalidatedEmptyFields, genId, revisionMetadata, constraintState) {
        var _a, _b, _c, _d;
        crossFieldTable.invalidatedFields = new Set();
        for (const fieldToAmend of fieldsToAmend) {
            const fieldContext = crossFieldTable.baseChangeToContext.get(fieldToAmend);
            let rebasedChange;
            let baseChanges;
            let baseRevision;
            let newNode;
            let field;
            if (fieldContext !== undefined) {
                // fieldToAmend is part of the base changeset.
                newNode = crossFieldTable.baseMapToRebased.get(fieldContext.map);
                assert(newNode !== undefined, 0x5b5 /* Should be a new HasFieldChanges associated with this base change */);
                baseChanges = fieldToAmend;
                field = fieldContext.field;
                rebasedChange = (_b = (_a = newNode.fieldChanges) === null || _a === void 0 ? void 0 : _a.get(field)) !== null && _b !== void 0 ? _b : {
                    fieldKind: genericFieldKind.identifier,
                    change: brand(newGenericChangeset()),
                };
                baseRevision = (_c = baseChanges.revision) !== null && _c !== void 0 ? _c : fieldContext.revision;
            }
            else {
                // fieldToAmend is part of the rebased changeset.
                rebasedChange = fieldToAmend;
                baseChanges = {
                    fieldKind: genericFieldKind.identifier,
                    change: brand(newGenericChangeset()),
                };
            }
            const { fieldKind, changesets: [fieldChangeset, baseChangeset], } = this.normalizeFieldChanges([rebasedChange, baseChanges], genId, revisionMetadata);
            const amendedChange = fieldKind.changeHandler.rebaser.amendRebase(fieldChangeset, tagChange(baseChangeset, baseRevision), (child, baseChild) => this.rebaseNodeChange(child, baseChild !== undefined ? tagChange(baseChild, baseRevision) : undefined, genId, crossFieldTable, undefined, (base, newNodeChange) => newNodeChange === undefined && invalidatedEmptyFields.has(base), revisionMetadata, constraintState), genId, newCrossFieldManager(crossFieldTable), revisionMetadata);
            if (newNode !== undefined && field !== undefined) {
                if (fieldKind.changeHandler.isEmpty(amendedChange)) {
                    (_d = newNode.fieldChanges) === null || _d === void 0 ? void 0 : _d.delete(field);
                }
                else {
                    if (newNode.fieldChanges === undefined) {
                        newNode.fieldChanges = new Map();
                    }
                    newNode.fieldChanges.set(field, {
                        fieldKind: fieldKind.identifier,
                        change: brand(amendedChange),
                    });
                }
            }
            else {
                rebasedChange.change = brand(amendedChange);
            }
        }
    }
    rebaseFieldMap(change, over, genId, crossFieldTable, fieldFilter, revisionMetadata, constraintState) {
        var _a, _b;
        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 = (_a = change.get(field)) !== null && _a !== void 0 ? _a : {
                fieldKind: genericFieldKind.identifier,
                change: brand(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 rebasedField = fieldKind.changeHandler.rebaser.rebase(fieldChangeset, taggedBaseChange, (child, baseChild) => this.rebaseNodeChange(child, baseChild !== undefined ? { revision, change: baseChild } : undefined, genId, crossFieldTable, baseChanges, fieldFilter, revisionMetadata, constraintState), genId, manager, revisionMetadata);
            if (!fieldKind.changeHandler.isEmpty(rebasedField)) {
                const rebasedFieldChange = {
                    fieldKind: fieldKind.identifier,
                    change: 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 (!((_b = over.change) === null || _b === void 0 ? void 0 : _b.has(field))) {
                const baseChanges = {
                    fieldKind: genericFieldKind.identifier,
                    change: brand(newGenericChangeset()),
                };
                const { fieldKind, changesets: [fieldChangeset, baseChangeset], } = this.normalizeFieldChanges([fieldChange, baseChanges], genId, revisionMetadata);
                const manager = newCrossFieldManager(crossFieldTable);
                const rebasedChangeset = fieldKind.changeHandler.rebaser.rebase(fieldChangeset, makeAnonChange(baseChangeset), (child, baseChild) => {
                    assert(baseChild === undefined, 0x5b6 /* This field should not have any base changes */);
                    return this.rebaseNodeChange(child, undefined, genId, crossFieldTable, baseChanges, fieldFilter, revisionMetadata, constraintState);
                }, genId, manager, revisionMetadata);
                const rebasedFieldChange = {
                    fieldKind: fieldKind.identifier,
                    change: brand(rebasedChangeset),
                };
                rebasedFields.set(field, rebasedFieldChange);
                addFieldData(manager, rebasedChangeset);
            }
        }
        return rebasedFields;
    }
    rebaseNodeChange(change, over, genId, crossFieldTable, parentField, fieldFilter, revisionMetadata, constraintState) {
        var _a, _b, _c, _d, _e;
        if (change === undefined && ((_a = over === null || over === void 0 ? void 0 : over.change) === null || _a === void 0 ? void 0 : _a.fieldChanges) === undefined) {
            return undefined;
        }
        if (((_b = over === null || over === void 0 ? void 0 : over.change) === null || _b === void 0 ? void 0 : _b.fieldChanges) !== undefined && parentField !== undefined) {
            crossFieldTable.baseMapToParentField.set(over.change.fieldChanges, parentField);
        }
        const baseMap = ((_c = over === null || over === void 0 ? void 0 : over.change) === null || _c === void 0 ? void 0 : _c.fieldChanges) !== undefined
            ? Object.assign(Object.assign({}, over), { change: over.change.fieldChanges }) : makeAnonChange(new Map());
        const fieldChanges = this.rebaseFieldMap((_d = change === null || change === void 0 ? void 0 : change.fieldChanges) !== null && _d !== void 0 ? _d : new Map(), baseMap, genId, crossFieldTable, fieldFilter, revisionMetadata, constraintState);
        const rebasedChange = {};
        if ((change === null || change === void 0 ? void 0 : change.valueChange) !== undefined) {
            rebasedChange.valueChange = change.valueChange;
        }
        if (fieldChanges.size > 0) {
            rebasedChange.fieldChanges = fieldChanges;
        }
        if ((change === null || change === void 0 ? void 0 : change.valueConstraint) !== undefined) {
            rebasedChange.valueConstraint = change.valueConstraint;
        }
        // We only care if a violated constraint is fixed or if a non-violated
        // constraint becomes violated
        if (rebasedChange.valueConstraint !== undefined && (over === null || over === void 0 ? void 0 : over.change.valueChange) !== undefined) {
            const violatedByOver = over.change.valueChange.value !== rebasedChange.valueConstraint.value;
            if (rebasedChange.valueConstraint.violated !== violatedByOver) {
                rebasedChange.valueConstraint = Object.assign(Object.assign({}, rebasedChange.valueConstraint), { violated: violatedByOver });
                constraintState.violationCount += violatedByOver ? 1 : -1;
            }
        }
        if (isEmptyNodeChangeset(rebasedChange)) {
            return undefined;
        }
        if (((_e = over === null || over === void 0 ? void 0 : over.change) === null || _e === void 0 ? void 0 : _e.fieldChanges) !== undefined) {
            crossFieldTable.baseMapToRebased.set(over.change.fieldChanges, rebasedChange);
        }
        return rebasedChange;
    }
    rebaseAnchors(anchors, over) {
        anchors.applyDelta(this.intoDelta(over));
    }
    intoDelta(change) {
        return this.intoDeltaImpl(change.fieldChanges);
    }
    /**
     * @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) {
        const delta = new Map();
        for (const [field, fieldChange] of change) {
            const deltaField = getChangeHandler(this.fieldKinds, fieldChange.fieldKind).intoDelta(fieldChange.change, (childChange) => this.deltaFromNodeChange(childChange));
            delta.set(field, deltaField);
        }
        return delta;
    }
    deltaFromNodeChange(change) {
        const modify = {
            type: Delta.MarkType.Modify,
        };
        const valueChange = change.valueChange;
        if (valueChange !== undefined) {
            modify.setValue = valueChange.value;
        }
        if (change.fieldChanges !== undefined) {
            modify.fields = this.intoDeltaImpl(change.fieldChanges);
        }
        return modify;
    }
    buildEditor(changeReceiver, anchors) {
        return new ModularEditBuilder(this, changeReceiver, anchors);
    }
}
function getFieldsToAmend(invalidatedFields, baseMapToRebased, baseChangeToContext, baseMapToParentField) {
    const fieldsToAmend = new Set();
    const invalidatedEmptyFields = new Set();
    for (let fieldChange of invalidatedFields) {
        // fieldChange may be either part of the base changeset or the rebased changeset.
        // If `baseChangeToContext` has an entry for it, it is part of the base changeset.
        // Otherwise it is part of the rebased changeset, and there is no corresponding base FieldChange.
        // If part of the rebased changeset we can add it to `fieldsToAmend`.
        // If part of the base changeset we may have to walk up to find an ancestor with a corresponding HasFieldChanges
        // and add that ancestor to `fieldsToAmend`.
        let baseFieldContext = baseChangeToContext.get(fieldChange);
        while (baseFieldContext !== undefined && !baseMapToRebased.has(baseFieldContext.map)) {
            invalidatedEmptyFields.add(fieldChange);
            const baseFieldTemp = baseMapToParentField.get(baseFieldContext.map);
            assert(baseFieldTemp !== undefined, 0x5b7 /* Should have parent for field */);
            fieldChange = baseFieldTemp;
            const contextTemp = baseChangeToContext.get(fieldChange);
            assert(contextTemp !== undefined, 0x5b8 /* Should have context for field */);
            baseFieldContext = contextTemp;
        }
        fieldsToAmend.add(fieldChange);
    }
    return [fieldsToAmend, invalidatedEmptyFields];
}
/**
 * @alpha
 */
export function revisionMetadataSourceFromInfo(revInfos) {
    const getIndex = (revision) => {
        const index = revInfos.findIndex((revInfo) => revInfo.revision === revision);
        assert(index !== -1, 0x5a0 /* Unable to index unknown revision */);
        return index;
    };
    const getInfo = (revision) => {
        return revInfos[getIndex(revision)];
    };
    return { getIndex, getInfo };
}
function isEmptyNodeChangeset(change) {
    return (change.fieldChanges === undefined &&
        change.valueChange === undefined &&
        change.valueConstraint === 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 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 === CrossFieldTarget.Source ? crossFieldTable.srcTable : crossFieldTable.dstTable;
    const getQueries = (target) => target === CrossFieldTarget.Source ? srcQueries : dstQueries;
    const manager = {
        table: crossFieldTable,
        srcQueries,
        dstQueries,
        fieldInvalidated: false,
        getOrCreate: (target, revision, id, newValue, invalidateDependents) => {
            if (invalidateDependents) {
                const dependents = target === CrossFieldTarget.Source
                    ? crossFieldTable.srcDependents
                    : crossFieldTable.dstDependents;
                const dependent = tryGetFromNestedMap(dependents, revision, id);
                if (dependent !== undefined) {
                    crossFieldTable.invalidatedFields.add(dependent);
                }
                if (nestedSetContains(getQueries(target), revision, id)) {
                    manager.fieldInvalidated = true;
                }
            }
            return getOrAddInNestedMap(getMap(target), revision, id, newValue);
        },
        get: (target, revision, id, addDependency) => {
            if (addDependency) {
                addToNestedSet(getQueries(target), revision, id);
            }
            return tryGetFromNestedMap(getMap(target), revision, id);
        },
    };
    return manager;
}
function addFieldData(manager, fieldData) {
    for (const [revision, ids] of manager.srcQueries) {
        for (const id of ids.keys()) {
            assert(tryGetFromNestedMap(manager.table.srcDependents, revision, id) === undefined, 0x564 /* TODO: Support multiple dependents per key */);
            setInNestedMap(manager.table.srcDependents, revision, id, fieldData);
        }
    }
    for (const [revision, ids] of manager.dstQueries) {
        for (const id of ids.keys()) {
            assert(tryGetFromNestedMap(manager.table.dstDependents, revision, id) === undefined, 0x565 /* TODO: Support multiple dependents per key */);
            setInNestedMap(manager.table.dstDependents, revision, id, 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 = brand(maxId);
    }
    if (constraintViolationCount !== undefined && constraintViolationCount > 0) {
        changeset.constraintViolationCount = constraintViolationCount;
    }
    return changeset;
}
/**
 * @sealed
 * @alpha
 */
export class ModularEditBuilder extends ProgressiveEditBuilderBase {
    constructor(family, changeReceiver, anchors) {
        super(family, changeReceiver, anchors);
        this.transactionDepth = 0;
        this.idAllocator = idAllocatorFromMaxId();
    }
    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();
        }
    }
    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 = brand(-1)) {
        const changeMap = this.buildChangeMap(field, fieldKind, change);
        this.applyChange(makeModularChangeset(changeMap, maxId));
    }
    submitChanges(changes, maxId = brand(-1)) {
        const changeMaps = changes.map((change) => 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 this.idAllocator(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.changeHandler.editor.buildChildChange(remainingPath.parentIndex, nodeChange);
            fieldChangeMap = new Map([
                [
                    remainingPath.parentField,
                    { fieldKind: genericFieldKind.identifier, change: brand(fieldChange) },
                ],
            ]);
            remainingPath = remainingPath.parent;
        }
        return fieldChangeMap;
    }
    setValue(path, value) {
        const valueChange = value === undefined ? {} : { value };
        const nodeChange = { valueChange };
        const fieldChange = genericFieldKind.changeHandler.editor.buildChildChange(path.parentIndex, nodeChange);
        this.submitChange({ parent: path.parent, field: path.parentField }, genericFieldKind.identifier, brand(fieldChange));
    }
    addValueConstraint(path, currentValue) {
        const nodeChange = {
            valueConstraint: { value: currentValue, violated: false },
        };
        const fieldChange = genericFieldKind.changeHandler.editor.buildChildChange(path.parentIndex, nodeChange);
        this.submitChange({ parent: path.parent, field: path.parentField }, genericFieldKind.identifier, brand(fieldChange));
    }
}
function getRevInfoFromTaggedChanges(changes) {
    var _a;
    let maxId = -1;
    const revInfos = [];
    for (const taggedChange of changes) {
        const change = taggedChange.change;
        maxId = Math.max((_a = change.maxId) !== null && _a !== void 0 ? _a : -1, maxId);
        revInfos.push(...revisionInfoFromTaggedChange(taggedChange));
    }
    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;
}
//# sourceMappingURL=modularChangeFamily.js.map