"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.optionalChangeHandler = exports.optionalFieldIntoDelta = exports.optionalFieldEditor = exports.taggedRegister = exports.optionalChangeRebaser = exports.RegisterMap = void 0;
const internal_1 = require("@fluidframework/core-utils/internal");
const index_js_1 = require("../../core/index.js");
const index_js_2 = require("../../util/index.js");
const deltaUtils_js_1 = require("../deltaUtils.js");
const index_js_3 = require("../modular-schema/index.js");
const optionalFieldCodecs_js_1 = require("./optionalFieldCodecs.js");
class RegisterMap {
    constructor() {
        this.nestedMapData = new index_js_2.SizedNestedMap();
    }
    clone() {
        const clone = new RegisterMap();
        for (const [id, t] of this.entries()) {
            clone.set(id, t);
        }
        return clone;
    }
    set(id, childChange) {
        if (id === "self") {
            this.nestedMapData.set("self", undefined, childChange);
        }
        else {
            this.nestedMapData.set(id.localId, id.revision, childChange);
        }
    }
    get(id) {
        return id === "self"
            ? this.nestedMapData.tryGet(id, undefined)
            : this.nestedMapData.tryGet(id.localId, id.revision);
    }
    has(id) {
        return this.get(id) !== undefined;
    }
    delete(id) {
        return id === "self"
            ? this.nestedMapData.delete("self", undefined)
            : this.nestedMapData.delete(id.localId, id.revision);
    }
    keys() {
        const changeIds = [];
        for (const [localId, nestedMap] of this.nestedMapData) {
            if (localId === "self") {
                changeIds.push("self");
            }
            else {
                for (const [revisionTag, _] of nestedMap) {
                    changeIds.push(revisionTag === undefined
                        ? { localId }
                        : { localId, revision: revisionTag });
                }
            }
        }
        return changeIds;
    }
    values() {
        return this.nestedMapData.values();
    }
    entries() {
        const entries = [];
        for (const changeId of this.keys()) {
            if (changeId === "self") {
                const entry = this.nestedMapData.tryGet("self", undefined);
                (0, internal_1.assert)(entry !== undefined, 0x770 /* Entry should not be undefined when iterating keys. */);
                entries.push(["self", entry]);
            }
            else {
                const entry = this.nestedMapData.tryGet(changeId.localId, changeId.revision);
                (0, internal_1.assert)(entry !== undefined, 0x771 /* Entry should not be undefined when iterating keys. */);
                entries.push([changeId, entry]);
            }
        }
        return entries;
    }
    get size() {
        return this.nestedMapData.size;
    }
}
exports.RegisterMap = RegisterMap;
exports.optionalChangeRebaser = {
    compose: (change1, change2, composeChild) => {
        const { srcToDst, dstToSrc } = getBidirectionalMaps(change1.moves);
        const change1FieldSrc = change1.valueReplace?.src;
        const change1FieldDst = getEffectfulDst(change1.valueReplace);
        const change2FieldSrc = change2.valueReplace?.src;
        let composedFieldSrc;
        if (change2FieldSrc !== undefined) {
            if (change2FieldSrc === "self") {
                composedFieldSrc = change1FieldSrc ?? change2FieldSrc;
            }
            else if (change1FieldDst !== undefined &&
                areEqualRegisterIds(change1FieldDst, change2FieldSrc)) {
                composedFieldSrc = "self";
            }
            else {
                composedFieldSrc =
                    (0, index_js_2.tryGetFromNestedMap)(dstToSrc, change2FieldSrc.revision, change2FieldSrc.localId) ?? change2FieldSrc;
            }
        }
        else if (change1FieldSrc !== undefined && change2.valueReplace === undefined) {
            composedFieldSrc = change1FieldSrc;
        }
        const childChanges2ByOriginalId = new RegisterMap();
        for (const [id, change] of change2.childChanges) {
            if (id === "self") {
                if (change1FieldSrc !== undefined) {
                    childChanges2ByOriginalId.set(change1FieldSrc, change);
                }
                else {
                    childChanges2ByOriginalId.set("self", change);
                }
            }
            else {
                if (change1FieldDst !== undefined && (0, index_js_1.areEqualChangeAtomIds)(change1FieldDst, id)) {
                    childChanges2ByOriginalId.set("self", change);
                }
                else {
                    const originalId = (0, index_js_2.tryGetFromNestedMap)(dstToSrc, id.revision, id.localId);
                    childChanges2ByOriginalId.set(originalId ?? id, change);
                }
            }
        }
        const composedMoves = [];
        const composedChildChanges = [];
        const composed = {
            moves: composedMoves,
            childChanges: composedChildChanges,
        };
        for (const [id, childChange1] of change1.childChanges) {
            const childChange2 = childChanges2ByOriginalId.get(id);
            composedChildChanges.push([id, composeChild(childChange1, childChange2)]);
            childChanges2ByOriginalId.delete(id);
        }
        for (const [id, childChange2] of childChanges2ByOriginalId.entries()) {
            composedChildChanges.push([id, composeChild(undefined, childChange2)]);
        }
        for (const [leg2Src, leg2Dst] of change2.moves) {
            const leg1Src = (0, index_js_2.tryGetFromNestedMap)(dstToSrc, leg2Src.revision, leg2Src.localId);
            if (leg1Src !== undefined) {
                composedMoves.push([leg1Src, leg2Dst]);
                (0, index_js_2.deleteFromNestedMap)(srcToDst, leg1Src.revision, leg1Src.localId);
                (0, index_js_2.deleteFromNestedMap)(dstToSrc, leg2Src.revision, leg2Src.localId);
            }
            else if (change1FieldDst === undefined ||
                !(0, index_js_1.areEqualChangeAtomIds)(change1FieldDst, leg2Src)) {
                composedMoves.push([leg2Src, leg2Dst]);
            }
        }
        for (const [revision, innerMap] of srcToDst.entries()) {
            for (const [localId, dst] of innerMap.entries()) {
                const src = (0, index_js_1.makeChangeAtomId)(localId, revision);
                if (composedFieldSrc === undefined || !areEqualRegisterIds(src, composedFieldSrc)) {
                    composedMoves.push([src, dst]);
                }
            }
        }
        if (change1FieldSrc !== undefined &&
            change1FieldSrc !== "self" &&
            change2.valueReplace !== undefined) {
            const change2FieldDst = change2.valueReplace.dst;
            if (isReplaceEffectful(change2.valueReplace) &&
                !(0, index_js_1.areEqualChangeAtomIds)(change1FieldSrc, change2FieldDst)) {
                composedMoves.push([change1FieldSrc, change2FieldDst]);
            }
        }
        const firstChange = change1.valueReplace ?? change2.valueReplace;
        if (firstChange === undefined) {
            return composed;
        }
        const replace = {
            isEmpty: firstChange.isEmpty,
            dst: getComposedReplaceDst(change1.valueReplace, change2),
        };
        if (composedFieldSrc !== undefined) {
            replace.src = composedFieldSrc;
        }
        composed.valueReplace = replace;
        return composed;
    },
    invert: (change, isRollback, genId) => {
        const { moves, childChanges } = change;
        const invertIdMap = new RegisterMap();
        const invertedMoves = [];
        for (const [src, dst] of moves) {
            invertIdMap.set(src, dst);
            invertedMoves.push([dst, src]);
        }
        if (change.valueReplace !== undefined) {
            const effectfulDst = getEffectfulDst(change.valueReplace);
            if (effectfulDst !== undefined) {
                invertIdMap.set("self", change.valueReplace.dst);
            }
            if (change.valueReplace.src !== undefined) {
                invertIdMap.set(change.valueReplace.src, "self");
            }
        }
        const inverted = {
            moves: invertedMoves,
            childChanges: childChanges.map(([id, childChange]) => {
                return [invertIdMap.get(id) ?? id, childChange];
            }),
        };
        if (change.valueReplace !== undefined) {
            if (isReplaceEffectful(change.valueReplace)) {
                const replace = change.valueReplace.src === undefined
                    ? {
                        isEmpty: true,
                        dst: (0, index_js_1.makeChangeAtomId)(genId.allocate()),
                    }
                    : {
                        isEmpty: false,
                        dst: isRollback
                            ? change.valueReplace.src
                            : (0, index_js_1.makeChangeAtomId)(genId.allocate()),
                    };
                if (change.valueReplace.isEmpty === false) {
                    replace.src = change.valueReplace.dst;
                }
                inverted.valueReplace = replace;
            }
            else if (!isRollback && change.valueReplace.src === "self") {
                inverted.valueReplace = {
                    isEmpty: false,
                    src: "self",
                    dst: (0, index_js_1.makeChangeAtomId)(genId.allocate()),
                };
            }
        }
        return inverted;
    },
    rebase: (change, overChange, rebaseChild) => {
        const { moves, childChanges, valueReplace: field } = change;
        // TODO: avoid computing the dstToSrc map if it's not needed.
        // TODO: de-dupe overSrcToDst and forwardMap
        const { srcToDst: overSrcToDst } = getBidirectionalMaps(overChange.moves);
        const forwardMap = new RegisterMap();
        for (const [src, dst] of overChange.moves) {
            forwardMap.set(src, dst);
        }
        if (overChange.valueReplace !== undefined) {
            const effectfulDst = getEffectfulDst(overChange.valueReplace);
            if (effectfulDst !== undefined) {
                forwardMap.set("self", overChange.valueReplace.dst);
            }
            if (overChange.valueReplace.src !== undefined) {
                forwardMap.set(overChange.valueReplace.src, "self");
            }
        }
        const rebasedMoves = [];
        for (const [src, dst] of moves) {
            const newDst = (0, index_js_2.tryGetFromNestedMap)(overSrcToDst, src.revision, src.localId);
            rebasedMoves.push([src, newDst ?? dst]);
        }
        const overChildChangesBySrc = new RegisterMap();
        for (const [id, childChange] of overChange.childChanges) {
            overChildChangesBySrc.set(id, childChange);
        }
        const rebasedChildChanges = [];
        for (const [id, childChange] of childChanges) {
            const overChildChange = overChildChangesBySrc.get(id);
            const rebasedId = forwardMap.get(id) ?? id;
            const rebasedChildChange = rebaseChild(childChange, overChildChange, rebasedId === "self" ? index_js_3.NodeExistenceState.Alive : index_js_3.NodeExistenceState.Dead);
            if (rebasedChildChange !== undefined) {
                rebasedChildChanges.push([rebasedId, rebasedChildChange]);
            }
        }
        const rebased = {
            moves: rebasedMoves,
            childChanges: rebasedChildChanges,
        };
        if (field !== undefined) {
            const replace = {
                isEmpty: overChange.valueReplace === undefined
                    ? field.isEmpty
                    : overChange.valueReplace.src === undefined,
                dst: field.dst,
            };
            if (field.src !== undefined) {
                replace.src = forwardMap.get(field.src) ?? field.src;
            }
            rebased.valueReplace = replace;
        }
        return rebased;
    },
    prune: (change, pruneChild) => {
        const childChanges = [];
        const prunedChange = {
            moves: change.moves,
            childChanges,
        };
        if (change.valueReplace !== undefined) {
            prunedChange.valueReplace = change.valueReplace;
        }
        for (const [id, childChange] of change.childChanges) {
            const prunedChildChange = pruneChild(childChange);
            if (prunedChildChange !== undefined) {
                childChanges.push([id, prunedChildChange]);
            }
        }
        return prunedChange;
    },
    replaceRevisions: (change, oldRevisions, newRevision) => {
        const valueReplace = replaceReplaceRevisions(change.valueReplace, oldRevisions, newRevision);
        const childChanges = [];
        for (const [id, childChange] of change.childChanges) {
            childChanges.push([
                replaceRegisterRevisions(id, oldRevisions, newRevision),
                (0, index_js_1.replaceAtomRevisions)(childChange, oldRevisions, newRevision),
            ]);
        }
        const moves = [];
        for (const [src, dst] of change.moves) {
            moves.push([
                (0, index_js_1.replaceAtomRevisions)(src, oldRevisions, newRevision),
                (0, index_js_1.replaceAtomRevisions)(dst, oldRevisions, newRevision),
            ]);
        }
        const updated = { childChanges, moves };
        if (valueReplace !== undefined) {
            updated.valueReplace = valueReplace;
        }
        return updated;
    },
};
function replaceReplaceRevisions(replace, oldRevisions, newRevision) {
    if (replace === undefined) {
        return undefined;
    }
    const updated = {
        ...replace,
        dst: (0, index_js_1.replaceAtomRevisions)(replace.dst, oldRevisions, newRevision),
    };
    if (replace.src !== undefined) {
        updated.src = replaceRegisterRevisions(replace.src, oldRevisions, newRevision);
    }
    return updated;
}
function replaceRegisterRevisions(register, oldRevisions, newRevision) {
    return register === "self"
        ? register
        : (0, index_js_1.replaceAtomRevisions)(register, oldRevisions, newRevision);
}
function getComposedReplaceDst(change1, change2) {
    const dst1 = change1?.dst;
    if (change2.valueReplace === undefined) {
        (0, internal_1.assert)(dst1 !== undefined, 0x8ce /* Both replace replaces should not be undefined */);
        return getIdAfterMoves(dst1, change2.moves);
    }
    if (dst1 === undefined ||
        change1?.src === "self" ||
        (change2.valueReplace.src !== undefined &&
            areEqualRegisterIds(change2.valueReplace.src, dst1))) {
        (0, internal_1.assert)(change2.valueReplace !== undefined, 0x8cf /* Both replace replaces should not be undefined */);
        return change2.valueReplace.dst;
    }
    else {
        return getIdAfterMoves(dst1, change2.moves);
    }
}
function getIdAfterMoves(id, moves) {
    for (const [src, dst] of moves) {
        if ((0, index_js_1.areEqualChangeAtomIds)(id, src)) {
            return dst;
        }
    }
    return id;
}
function areEqualRegisterIds(id1, id2) {
    return id1 === "self" || id2 === "self" ? id1 === id2 : (0, index_js_1.areEqualChangeAtomIds)(id1, id2);
}
function getBidirectionalMaps(moves) {
    const srcToDst = new Map();
    const dstToSrc = new Map();
    for (const [src, dst] of moves) {
        (0, index_js_2.setInNestedMap)(srcToDst, src.revision, src.localId, dst);
        (0, index_js_2.setInNestedMap)(dstToSrc, dst.revision, dst.localId, src);
    }
    return { srcToDst, dstToSrc };
}
function isReplaceEffectful(replace) {
    if (replace.src === "self") {
        return false;
    }
    return !replace.isEmpty || replace.src !== undefined;
}
function getEffectfulDst(replace) {
    return replace === undefined || replace.isEmpty || replace.src === "self"
        ? undefined
        : replace.dst;
}
function taggedRegister(id, revision) {
    if (id === "self") {
        return id;
    }
    return (0, index_js_1.taggedAtomId)(id, revision);
}
exports.taggedRegister = taggedRegister;
exports.optionalFieldEditor = {
    set: (wasEmpty, ids) => ({
        moves: [],
        childChanges: [],
        valueReplace: {
            isEmpty: wasEmpty,
            src: { localId: ids.fill },
            dst: { localId: ids.detach },
        },
    }),
    clear: (wasEmpty, detachId) => ({
        moves: [],
        childChanges: [],
        valueReplace: {
            isEmpty: wasEmpty,
            dst: { localId: detachId },
        },
    }),
    buildChildChange: (index, childChange) => {
        (0, internal_1.assert)(index === 0, 0x404 /* Optional fields only support a single child node */);
        return {
            moves: [],
            childChanges: [["self", childChange]],
        };
    },
};
function optionalFieldIntoDelta(change, deltaFromChild) {
    const delta = {};
    let markIsANoop = true;
    const mark = { count: 1 };
    if (change.valueReplace !== undefined && isReplaceEffectful(change.valueReplace)) {
        if (!change.valueReplace.isEmpty) {
            mark.detach = (0, deltaUtils_js_1.nodeIdFromChangeAtom)(change.valueReplace.dst);
        }
        if (change.valueReplace.src !== undefined) {
            mark.attach = (0, deltaUtils_js_1.nodeIdFromChangeAtom)(change.valueReplace.src);
        }
        markIsANoop = false;
    }
    if (change.moves.length > 0) {
        delta.rename = change.moves.map(([src, dst]) => ({
            count: 1,
            oldId: (0, deltaUtils_js_1.nodeIdFromChangeAtom)(src),
            newId: (0, deltaUtils_js_1.nodeIdFromChangeAtom)(dst),
        }));
    }
    if (change.childChanges.length > 0) {
        const globals = [];
        for (const [id, childChange] of change.childChanges) {
            const childDelta = deltaFromChild(childChange);
            if (id !== "self") {
                const fields = childDelta;
                globals.push({
                    id: { major: id.revision, minor: id.localId },
                    fields,
                });
            }
            else {
                mark.fields = childDelta;
                markIsANoop = false;
            }
        }
        if (globals.length > 0) {
            delta.global = globals;
        }
    }
    if (!markIsANoop) {
        delta.local = [mark];
    }
    return delta;
}
exports.optionalFieldIntoDelta = optionalFieldIntoDelta;
exports.optionalChangeHandler = {
    rebaser: exports.optionalChangeRebaser,
    codecsFactory: optionalFieldCodecs_js_1.makeOptionalFieldCodecFamily,
    editor: exports.optionalFieldEditor,
    intoDelta: optionalFieldIntoDelta,
    relevantRemovedRoots,
    isEmpty: (change) => change.childChanges.length === 0 &&
        change.moves.length === 0 &&
        change.valueReplace === undefined,
    createEmpty: () => ({ moves: [], childChanges: [] }),
};
function* relevantRemovedRoots(change, relevantRemovedRootsFromChild) {
    const alreadyYielded = new RegisterMap();
    for (const [src] of change.moves) {
        if (!alreadyYielded.has(src)) {
            alreadyYielded.set(src, true);
            yield (0, deltaUtils_js_1.nodeIdFromChangeAtom)(src);
        }
    }
    for (const [id, childChange] of change.childChanges) {
        // Child changes make the tree they apply to relevant unless that tree existed in the starting context of
        // of this change.
        if (id !== "self" && !alreadyYielded.has(id)) {
            alreadyYielded.set(id, true);
            yield (0, deltaUtils_js_1.nodeIdFromChangeAtom)(id);
        }
        yield* relevantRemovedRootsFromChild(childChange);
    }
    const selfSrc = change.valueReplace?.src;
    if (selfSrc !== undefined && selfSrc !== "self" && !alreadyYielded.has(selfSrc)) {
        yield (0, deltaUtils_js_1.nodeIdFromChangeAtom)(selfSrc);
    }
}
//# sourceMappingURL=optionalField.js.map