/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { assert } from "@fluidframework/core-utils/internal";
import { areEqualChangeAtomIds, makeChangeAtomId, taggedAtomId, taggedOptAtomId, } from "../../core/index.js";
import { SizedNestedMap, deleteFromNestedMap, setInNestedMap, tryGetFromNestedMap, } from "../../util/index.js";
import { nodeIdFromChangeAtom } from "../deltaUtils.js";
import { NodeExistenceState, } from "../modular-schema/index.js";
import { makeOptionalFieldCodecFamily } from "./optionalFieldCodecs.js";
export class RegisterMap {
    constructor() {
        this.nestedMapData = new 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);
                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);
                assert(entry !== undefined, 0x771 /* Entry should not be undefined when iterating keys. */);
                entries.push([changeId, entry]);
            }
        }
        return entries;
    }
    get size() {
        return this.nestedMapData.size;
    }
}
export const optionalChangeRebaser = {
    compose: ({ change: change1, revision: revision1 }, { change: change2, revision: revision2 }, composeChild) => {
        const { srcToDst, dstToSrc } = getBidirectionalMaps(change1.moves, revision1);
        const change1FieldSrc = taggedOptRegister(change1.valueReplace?.src, revision1);
        const change1FieldDst = taggedOptAtomId(getEffectfulDst(change1.valueReplace), revision1);
        const change2FieldSrc = taggedOptRegister(change2.valueReplace?.src, revision2);
        let composedFieldSrc;
        if (change2FieldSrc !== undefined) {
            if (change2FieldSrc === "self") {
                composedFieldSrc = change1FieldSrc ?? change2FieldSrc;
            }
            else if (change1FieldDst !== undefined &&
                areEqualRegisterIds(change1FieldDst, change2FieldSrc)) {
                composedFieldSrc = "self";
            }
            else {
                composedFieldSrc =
                    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 {
                const idWithRevision = taggedAtomId(id, revision2);
                if (change1FieldDst !== undefined &&
                    areEqualChangeAtomIds(change1FieldDst, idWithRevision)) {
                    childChanges2ByOriginalId.set("self", change);
                }
                else {
                    const originalId = tryGetFromNestedMap(dstToSrc, idWithRevision.revision, idWithRevision.localId);
                    childChanges2ByOriginalId.set(originalId ?? idWithRevision, change);
                }
            }
        }
        const composedMoves = [];
        const composedChildChanges = [];
        const composed = {
            moves: composedMoves,
            childChanges: composedChildChanges,
        };
        for (const [id, childChange1] of change1.childChanges) {
            const idWithRevision = taggedRegister(id, revision1);
            const childChange2 = childChanges2ByOriginalId.get(idWithRevision);
            composedChildChanges.push([idWithRevision, composeChild(childChange1, childChange2)]);
            childChanges2ByOriginalId.delete(idWithRevision);
        }
        for (const [id, childChange2] of childChanges2ByOriginalId.entries()) {
            composedChildChanges.push([id, composeChild(undefined, childChange2)]);
        }
        for (const [src, dst] of change2.moves) {
            const leg2Src = taggedAtomId(src, revision2);
            const leg2Dst = taggedAtomId(dst, revision2);
            const leg1Src = tryGetFromNestedMap(dstToSrc, leg2Src.revision, leg2Src.localId);
            if (leg1Src !== undefined) {
                composedMoves.push([leg1Src, leg2Dst]);
                deleteFromNestedMap(srcToDst, leg1Src.revision, leg1Src.localId);
                deleteFromNestedMap(dstToSrc, leg2Src.revision, leg2Src.localId);
            }
            else if (change1FieldDst === undefined ||
                !areEqualChangeAtomIds(change1FieldDst, leg2Src)) {
                composedMoves.push([leg2Src, leg2Dst]);
            }
        }
        for (const [revision, innerMap] of srcToDst.entries()) {
            for (const [localId, dst] of innerMap.entries()) {
                const src = makeChangeAtomId(localId, revision);
                if (composedFieldSrc === undefined || !areEqualRegisterIds(src, composedFieldSrc)) {
                    composedMoves.push([src, dst]);
                }
            }
        }
        if (change1FieldSrc !== undefined &&
            change1FieldSrc !== "self" &&
            change2.valueReplace !== undefined) {
            const change2FieldDst = taggedAtomId(change2.valueReplace.dst, revision2);
            if (isReplaceEffectful(change2.valueReplace) &&
                !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, revision1, change2, revision2),
        };
        if (composedFieldSrc !== undefined) {
            replace.src = composedFieldSrc;
        }
        composed.valueReplace = replace;
        return composed;
    },
    invert: ({ revision, change }, isRollback, genId) => {
        const { moves, childChanges } = change;
        const tagAtomId = (id) => taggedAtomId(id, revision);
        const tagRegister = (id) => (id === "self" ? id : tagAtomId(id));
        const invertIdMap = new RegisterMap();
        const invertedMoves = [];
        for (const [src, dst] of moves) {
            const taggedSrc = tagAtomId(src);
            const taggedDst = tagAtomId(dst);
            invertIdMap.set(taggedSrc, taggedDst);
            invertedMoves.push([taggedDst, taggedSrc]);
        }
        if (change.valueReplace !== undefined) {
            const effectfulDst = getEffectfulDst(change.valueReplace);
            if (effectfulDst !== undefined) {
                invertIdMap.set("self", tagAtomId(change.valueReplace.dst));
            }
            if (change.valueReplace.src !== undefined) {
                invertIdMap.set(tagRegister(change.valueReplace.src), "self");
            }
        }
        const inverted = {
            moves: invertedMoves,
            childChanges: childChanges.map(([id, childChange]) => {
                const taggedId = tagRegister(id);
                return [invertIdMap.get(taggedId) ?? taggedId, childChange];
            }),
        };
        if (change.valueReplace !== undefined) {
            if (isReplaceEffectful(change.valueReplace)) {
                const replace = change.valueReplace.src === undefined
                    ? {
                        isEmpty: true,
                        dst: makeChangeAtomId(genId.allocate()),
                    }
                    : {
                        isEmpty: false,
                        dst: isRollback
                            ? tagAtomId(change.valueReplace.src)
                            : makeChangeAtomId(genId.allocate()),
                    };
                if (change.valueReplace.isEmpty === false) {
                    replace.src = tagAtomId(change.valueReplace.dst);
                }
                inverted.valueReplace = replace;
            }
            else if (!isRollback && change.valueReplace.src === "self") {
                inverted.valueReplace = {
                    isEmpty: false,
                    src: "self",
                    dst: makeChangeAtomId(genId.allocate()),
                };
            }
        }
        return inverted;
    },
    rebase: (change, overTagged, rebaseChild) => {
        const tagAtomId = (id) => taggedAtomId(id, overTagged.revision);
        const tagRegister = (id) => (id === "self" ? id : tagAtomId(id));
        const { moves, childChanges, valueReplace: field } = change;
        const { change: overChange } = overTagged;
        // TODO: avoid computing the dstToSrc map if it's not needed.
        // TODO: de-dupe overSrcToDst and forwardMap
        const { srcToDst: overSrcToDst } = getBidirectionalMaps(overChange.moves, overTagged.revision);
        const forwardMap = new RegisterMap();
        for (const [src, dst] of overChange.moves) {
            forwardMap.set(tagAtomId(src), tagAtomId(dst));
        }
        if (overChange.valueReplace !== undefined) {
            const effectfulDst = getEffectfulDst(overChange.valueReplace);
            if (effectfulDst !== undefined) {
                forwardMap.set("self", tagAtomId(overChange.valueReplace.dst));
            }
            if (overChange.valueReplace.src !== undefined) {
                forwardMap.set(tagRegister(overChange.valueReplace.src), "self");
            }
        }
        const rebasedMoves = [];
        for (const [src, dst] of moves) {
            const newDst = tryGetFromNestedMap(overSrcToDst, src.revision, src.localId);
            rebasedMoves.push([src, newDst ?? dst]);
        }
        const overChildChangesBySrc = new RegisterMap();
        for (const [id, childChange] of overChange.childChanges) {
            overChildChangesBySrc.set(tagRegister(id) ?? 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" ? NodeExistenceState.Alive : 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;
    },
};
function getComposedReplaceDst(change1, revision1, change2, revision2) {
    const dst1 = taggedOptAtomId(change1?.dst, revision1);
    if (change2.valueReplace === undefined) {
        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(taggedRegister(change2.valueReplace.src, revision2), dst1))) {
        assert(change2.valueReplace !== undefined, 0x8cf /* Both replace replaces should not be undefined */);
        return taggedAtomId(change2.valueReplace.dst, revision2);
    }
    else {
        return getIdAfterMoves(dst1, change2.moves);
    }
}
function getIdAfterMoves(id, moves) {
    for (const [src, dst] of moves) {
        if (areEqualChangeAtomIds(id, src)) {
            return dst;
        }
    }
    return id;
}
function areEqualRegisterIds(id1, id2) {
    return id1 === "self" || id2 === "self" ? id1 === id2 : areEqualChangeAtomIds(id1, id2);
}
function getBidirectionalMaps(moves, revision) {
    const srcToDst = new Map();
    const dstToSrc = new Map();
    for (const [src, dst] of moves) {
        const srcWithRevision = taggedAtomId(src, revision);
        const dstWithRevision = taggedAtomId(dst, revision);
        setInNestedMap(srcToDst, srcWithRevision.revision, srcWithRevision.localId, dstWithRevision);
        setInNestedMap(dstToSrc, dstWithRevision.revision, dstWithRevision.localId, srcWithRevision);
    }
    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 taggedOptRegister(id, revision) {
    return id !== undefined ? taggedRegister(id, revision) : undefined;
}
export function taggedRegister(id, revision) {
    if (id === "self") {
        return id;
    }
    return taggedAtomId(id, revision);
}
export const 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) => {
        assert(index === 0, 0x404 /* Optional fields only support a single child node */);
        return {
            moves: [],
            childChanges: [["self", childChange]],
        };
    },
};
export function optionalFieldIntoDelta({ change, revision }, deltaFromChild) {
    const delta = {};
    let markIsANoop = true;
    const mark = { count: 1 };
    if (change.valueReplace !== undefined && isReplaceEffectful(change.valueReplace)) {
        if (!change.valueReplace.isEmpty) {
            mark.detach = nodeIdFromChangeAtom(change.valueReplace.dst, revision);
        }
        if (change.valueReplace.src !== undefined) {
            mark.attach = nodeIdFromChangeAtom(change.valueReplace.src, revision);
        }
        markIsANoop = false;
    }
    if (change.moves.length > 0) {
        delta.rename = change.moves.map(([src, dst]) => ({
            count: 1,
            oldId: nodeIdFromChangeAtom(src, revision),
            newId: nodeIdFromChangeAtom(dst, revision),
        }));
    }
    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 ?? 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;
}
export const optionalChangeHandler = {
    rebaser: optionalChangeRebaser,
    codecsFactory: makeOptionalFieldCodecFamily,
    editor: optionalFieldEditor,
    intoDelta: optionalFieldIntoDelta,
    relevantRemovedRoots,
    isEmpty: (change) => change.childChanges.length === 0 &&
        change.moves.length === 0 &&
        change.valueReplace === undefined,
    createEmpty: () => ({ moves: [], childChanges: [] }),
};
function* relevantRemovedRoots({ change, revision }, relevantRemovedRootsFromChild) {
    const alreadyYielded = new RegisterMap();
    for (const [src] of change.moves) {
        if (!alreadyYielded.has(src)) {
            alreadyYielded.set(src, true);
            yield nodeIdFromChangeAtom(src, revision);
        }
    }
    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 nodeIdFromChangeAtom(id, revision);
        }
        yield* relevantRemovedRootsFromChild(childChange);
    }
    const selfSrc = change.valueReplace?.src;
    if (selfSrc !== undefined && selfSrc !== "self" && !alreadyYielded.has(selfSrc)) {
        yield nodeIdFromChangeAtom(selfSrc, revision);
    }
}
//# sourceMappingURL=optionalField.js.map