"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.tryGetEncodedCellRename = exports.encodeRevisionWithContext = exports.encodeSequenceChangeset = exports.makeV2Codec = exports.decodeSequenceChangeset = exports.makeV2CodecHelpers = void 0;
const internal_1 = require("@fluidframework/core-utils/internal");
const index_js_1 = require("../../codec/index.js");
const index_js_2 = require("../../core/index.js");
const index_js_3 = require("../../util/index.js");
const changeAtomIdCodec_js_1 = require("../changeAtomIdCodec.js");
const index_js_4 = require("../modular-schema/index.js");
const formatV2_js_1 = require("./formatV2.js");
const types_js_1 = require("./types.js");
const utils_js_1 = require("./utils.js");
function makeV2CodecHelpers(revisionTagCodec) {
    const changeAtomIdCodec = (0, changeAtomIdCodec_js_1.makeChangeAtomIdCodec)(revisionTagCodec);
    function decodeRevision(encodedRevision, context) {
        if (encodedRevision === undefined) {
            (0, internal_1.assert)(context.revision !== undefined, 0x996 /* Implicit revision should be provided */);
            return context.revision;
        }
        return revisionTagCodec.decode(encodedRevision, context);
    }
    const decoderLibrary = makeMarkEffectDecoder(changeAtomIdCodec, decodeRevision);
    const decoderDispatcher = new index_js_1.DiscriminatedUnionDispatcher(decoderLibrary);
    return {
        changeAtomIdCodec,
        decoderLibrary,
        encodeMarkEffect(mark, context) {
            return encodeMarkEffectV2(mark, context, (revision) => encodeRevisionWithContext(revision, context.baseContext, revisionTagCodec), changeAtomIdCodec);
        },
        decodeRevision: (encoded, context) => encoded !== undefined
            ? revisionTagCodec.decode(encoded, context)
            : (context.revision ?? (0, internal_1.fail)("Expected a default revision")),
        decodeMarkEffect: (encoded, count, cellId, context) => {
            return decoderDispatcher.dispatch(encoded, count, cellId, context);
        },
    };
}
exports.makeV2CodecHelpers = makeV2CodecHelpers;
function decodeSequenceChangeset(changeset, context, changeAtomIdCodec, decodeMarkEffect) {
    const marks = [];
    for (const mark of changeset) {
        const decodedMark = {
            count: mark.count,
        };
        if (mark.cellId !== undefined) {
            decodedMark.cellId = changeAtomIdCodec.decode(mark.cellId, context.baseContext);
        }
        if (mark.effect !== undefined) {
            Object.assign(decodedMark, decodeMarkEffect(mark.effect, mark.count, decodedMark.cellId, context));
        }
        if (mark.changes !== undefined) {
            if (decodedMark.cellId !== undefined) {
                context.decodeRootNodeChange(decodedMark.cellId, mark.changes);
            }
            else {
                decodedMark.changes = context.decodeNode(mark.changes);
            }
        }
        marks.push(decodedMark);
    }
    return marks;
}
exports.decodeSequenceChangeset = decodeSequenceChangeset;
function makeMarkEffectDecoder(changeAtomIdCodec, decodeRevision) {
    function decodeMoveIn(encoded, context) {
        const { id, revision } = encoded;
        const endpoint = encoded.finalEndpoint !== undefined
            ? changeAtomIdCodec.decode(encoded.finalEndpoint, context.baseContext)
            : undefined;
        const mark = {
            type: "Insert",
            id: endpoint?.localId ?? id,
            revision: endpoint?.revision ?? decodeRevision(revision, context.baseContext),
        };
        return mark;
    }
    const decoderLibrary = {
        moveIn(encoded, count, cellId, context) {
            return decodeMoveIn(encoded, context);
        },
        insert(encoded, count, cellId, context) {
            const { id, revision } = encoded;
            const mark = {
                type: "Insert",
                id,
            };
            mark.revision = decodeRevision(revision, context.baseContext);
            const attachId = (0, utils_js_1.getAttachedRootId)(mark);
            if (cellId !== undefined && !(0, index_js_2.areEqualChangeAtomIds)(cellId, attachId)) {
                context.decodeRootRename(cellId, attachId, count);
            }
            return mark;
        },
        remove(encoded, count, cellId, context) {
            const { id, revision, idOverride } = encoded;
            const cellRename = idOverride !== undefined
                ? changeAtomIdCodec.decode(idOverride, context.baseContext)
                : undefined;
            return decodeDetach(cellId, count, decodeRevision(revision, context.baseContext), id, cellRename, undefined, context);
        },
        moveOut(encoded, count, cellId, context) {
            const { id, idOverride, revision, finalEndpoint } = encoded;
            const cellRename = idOverride !== undefined
                ? changeAtomIdCodec.decode(idOverride, context.baseContext)
                : undefined;
            const decodedRevision = decodeRevision(revision, context.baseContext);
            if (finalEndpoint !== undefined) {
                const decodedEndpoint = changeAtomIdCodec.decode(finalEndpoint, context.baseContext);
                return decodeDetach(cellId, count, decodedEndpoint.revision ?? (0, internal_1.fail)("Revision should be defined"), decodedEndpoint.localId, cellRename, { revision: decodedRevision, localId: id }, context);
            }
            return decodeDetach(cellId, count, decodedRevision, id, cellRename, undefined, context);
        },
        attachAndDetach(encoded, count, cellId, context) {
            // In documents generated by clients on release >=2.2 (i.e., running the code from the PR that added this comment),
            // renames are encoded as AttachAndDetach with a special id.
            // This ensures forward-compatibility of clients on release <=2.1 with documents/ops generated by clients on release >=2.2.
            const encodedRenameId = tryGetEncodedCellRenameFromAttachAndDetach(encoded);
            if (encodedRenameId !== undefined) {
                return {
                    type: "Rename",
                    idOverride: changeAtomIdCodec.decode(encodedRenameId, context.baseContext),
                };
            }
            (0, internal_1.assert)(encoded.detach.remove !== undefined, "Attach and detach should always contains a remove");
            const detachId = encoded.detach.remove.idOverride !== undefined
                ? changeAtomIdCodec.decode(encoded.detach.remove.idOverride, context.baseContext)
                : {
                    revision: decodeRevision(encoded.detach.remove.revision, context.baseContext),
                    localId: encoded.detach.remove.id,
                };
            (0, internal_1.assert)(cellId !== undefined, "Attach and detach should target an empty cell");
            if (encoded.attach.moveIn !== undefined) {
                context.decodeMoveAndDetach(detachId, count);
            }
            else {
                context.decodeRootRename(cellId, detachId, count);
            }
            return {
                type: "Rename",
                idOverride: detachId,
            };
        },
    };
    return decoderLibrary;
}
function decodeDetach(cellId, count, revision, localId, cellRename, detachCellId, context) {
    const detachId = { revision, localId };
    if (cellId !== undefined) {
        context.decodeRootRename(cellId, detachId, count);
        return {
            type: "Rename",
            idOverride: cellRename ?? detachId,
        };
    }
    const mark = {
        type: "Remove",
        revision,
        id: localId,
    };
    if (cellRename !== undefined) {
        mark.cellRename = cellRename;
    }
    if (detachCellId !== undefined) {
        mark.detachCellId = detachCellId;
    }
    return mark;
}
function makeV2Codec(revisionTagCodec) {
    const { decodeMarkEffect, changeAtomIdCodec } = makeV2CodecHelpers(revisionTagCodec);
    return {
        encode: (changeset, context) => encodeSequenceChangeset(changeset, context, (revision) => encodeRevisionWithContext(revision, context.baseContext, revisionTagCodec), changeAtomIdCodec, encodeMarkEffectV2),
        decode: (changeset, context) => decodeSequenceChangeset(changeset, context, changeAtomIdCodec, decodeMarkEffect),
        encodedSchema: (0, formatV2_js_1.Changeset)(index_js_4.EncodedNodeChangeset),
    };
}
exports.makeV2Codec = makeV2Codec;
function encodeSequenceChangeset(changeset, context, encodeRevision, changeAtomIdCodec, encodeMarkEffect) {
    const jsonMarks = [];
    for (const mark of changeset) {
        jsonMarks.push(...encodeMark(mark, context, encodeRevision, changeAtomIdCodec, encodeMarkEffect));
    }
    return jsonMarks;
}
exports.encodeSequenceChangeset = encodeSequenceChangeset;
function encodeMark(mark, context, encodeRevision, changeAtomIdCodec, encodeMarkEffect) {
    const splitLength = getLengthToSplitMark(mark, context);
    if (splitLength < mark.count) {
        const [mark1, mark2] = (0, utils_js_1.splitMark)(mark, splitLength);
        return [
            encodeSplitMark(mark1, context, encodeRevision, changeAtomIdCodec, encodeMarkEffect),
            ...encodeMark(mark2, context, encodeRevision, changeAtomIdCodec, encodeMarkEffect),
        ];
    }
    return [encodeSplitMark(mark, context, encodeRevision, changeAtomIdCodec, encodeMarkEffect)];
}
function encodeRename(mark, context, changeAtomIdCodec, encodeRevision) {
    (0, internal_1.assert)(mark.cellId !== undefined, "Rename should target empty cell");
    const inputDetachId = context.getInputRootId(mark.idOverride, mark.count).value;
    const isMoveInAndDetach = !context.isAttachId(mark.idOverride, mark.count).value &&
        (context.isDetachId(mark.idOverride, mark.count).value ||
            (inputDetachId !== undefined && !(0, index_js_2.areEqualChangeAtomIds)(inputDetachId, mark.cellId)));
    if (isMoveInAndDetach) {
        // These cells are the final detach location of moved nodes.
        const encodedRevision = encodeRevision(mark.idOverride.revision);
        // XXX: Use finalEndpoint if there is an associated cellDetachId.
        return {
            attachAndDetach: {
                attach: {
                    moveIn: {
                        revision: encodedRevision,
                        id: mark.idOverride.localId,
                    },
                },
                detach: {
                    remove: {
                        revision: encodedRevision,
                        id: mark.idOverride.localId,
                    },
                },
            },
        };
    }
    const renamedRootId = context.rootRenames.getFirst(mark.cellId, mark.count).value;
    const outputRootId = renamedRootId ?? mark.cellId;
    const isMoveOutAndAttach = context.isAttachId(outputRootId, mark.count).value;
    const isMoveOutAndDetach = renamedRootId !== undefined && !(0, index_js_2.areEqualChangeAtomIds)(renamedRootId, mark.idOverride);
    if (isMoveOutAndAttach || isMoveOutAndDetach) {
        // Detached nodes which were last at this cell location have been moved.
        // XXX: mark.idOverride represents detachCellId, so we should represent it using the moveOut's ID and use outputRootId as the finalDetachId.
        // Is that always true?
        return {
            moveOut: {
                revision: encodeRevision(outputRootId.revision),
                id: outputRootId.localId,
                idOverride: changeAtomIdCodec.encode(mark.idOverride, context.baseContext),
            },
        };
    }
    if (renamedRootId !== undefined) {
        return {
            remove: {
                revision: encodeRevision(outputRootId.revision),
                id: outputRootId.localId,
            },
        };
    }
    // In documents generated by clients on release >=2.2 (i.e., running the code from the PR that added this comment),
    // renames are encoded as AttachAndDetach with a special id.
    // This ensures forward-compatibility of clients on release <=2.1 with documents/ops generated by clients on release >=2.2.
    return {
        attachAndDetach: {
            attach: { insert: { id: renameLocalId } },
            detach: {
                remove: {
                    id: renameLocalId,
                    idOverride: changeAtomIdCodec.encode(mark.idOverride, context.baseContext),
                },
            },
        },
    };
}
function getLengthToSplitMark(mark, context) {
    let count = mark.cellId !== undefined
        ? (0, index_js_4.rangeQueryChangeAtomIdMap)(context.rootNodeChanges, mark.cellId, mark.count).length
        : mark.count;
    if (mark.cellId !== undefined) {
        count = context.getInputRootId(mark.cellId, count).length;
    }
    switch (mark.type) {
        case "Insert":
            count = context.isDetachId((0, utils_js_1.getAttachedRootId)(mark), count).length;
            break;
        case "Remove":
            count = context.isAttachId((0, utils_js_1.getDetachedRootId)(mark), count).length;
            break;
        case "Rename": {
            count = context.getInputRootId(mark.idOverride, count).length;
            count = context.isAttachId(mark.idOverride, count).length;
            count = context.isDetachId(mark.idOverride, count).length;
            const cellId = mark.cellId ?? (0, internal_1.fail)("Rename should have cell ID");
            const renameEntry = context.rootRenames.getFirst(cellId, count);
            count = renameEntry.length;
            count = context.isAttachId(renameEntry.value ?? cellId, count).length;
            break;
        }
        default:
            break;
    }
    return count;
}
function encodeSplitMark(mark, context, encodeRevision, changeAtomIdCodec, encodeMarkEffect) {
    const encodedMark = {
        count: mark.count,
    };
    if (!(0, utils_js_1.isNoopMark)(mark)) {
        encodedMark.effect = encodeMarkEffect(mark, context, encodeRevision, changeAtomIdCodec);
    }
    if (mark.cellId !== undefined) {
        (0, internal_1.assert)(mark.changes === undefined, "Empty cells should not have node changes");
        encodedMark.cellId = changeAtomIdCodec.encode(mark.cellId, context.baseContext);
        const nodeId = (0, index_js_4.getFromChangeAtomIdMap)(context.rootNodeChanges, mark.cellId);
        if (nodeId !== undefined) {
            encodedMark.changes = context.encodeNode(nodeId);
        }
    }
    else if (mark.changes !== undefined) {
        encodedMark.changes = context.encodeNode(mark.changes);
    }
    return encodedMark;
}
function encodeMarkEffectV2(mark, context, encodeRevision, changeAtomIdCodec) {
    const type = mark.type;
    switch (type) {
        case "Insert": {
            const attachId = (0, utils_js_1.getAttachedRootId)(mark);
            const isMove = context.isDetachId(attachId, 1).value;
            // If the input context ID for these nodes is not the cell ID,
            // then these nodes are being moved from the location at which they were last detached.
            const inputId = context.getInputRootId(attachId, mark.count).value ?? attachId;
            const isInitialAttachLocation = mark.cellId === undefined || (0, index_js_2.areEqualChangeAtomIds)(mark.cellId, inputId);
            return isMove || !isInitialAttachLocation
                ? {
                    moveIn: { revision: encodeRevision(mark.revision), id: mark.id },
                }
                : {
                    insert: {
                        revision: encodeRevision(mark.revision),
                        id: mark.id,
                    },
                };
        }
        case "Remove": {
            const encodedIdOverride = mark.cellRename !== undefined
                ? changeAtomIdCodec.encode(mark.cellRename, context.baseContext)
                : undefined;
            const detachId = (0, utils_js_1.getDetachedRootId)(mark);
            const isMove = context.isAttachId(detachId, 1).value;
            const outputCellId = mark.cellRename ?? mark.detachCellId ?? detachId;
            // If the final detach location for the nodes were here,
            // then the output cell ID would be the same as the detach ID.
            // So if the cell ID is different from the detach ID, the nodes must have been moved.
            const isFinalDetachLocation = (0, index_js_2.areEqualChangeAtomIds)(detachId, outputCellId);
            const isMoveOrDetachedMove = isMove || !isFinalDetachLocation;
            if (mark.detachCellId !== undefined) {
                (0, internal_1.assert)(isMoveOrDetachedMove, "Only detaches representing a move out should specify a detach cell ID");
                // XXX: Set final endpoint on the move in.
                return {
                    moveOut: {
                        revision: encodeRevision(mark.detachCellId.revision),
                        id: mark.detachCellId.localId,
                        idOverride: encodedIdOverride,
                        finalEndpoint: changeAtomIdCodec.encode({ revision: mark.revision, localId: mark.id }, context.baseContext),
                    },
                };
            }
            const encodedRevision = encodeRevision(mark.revision);
            return isMoveOrDetachedMove
                ? {
                    moveOut: {
                        revision: encodedRevision,
                        id: mark.id,
                        idOverride: encodedIdOverride,
                    },
                }
                : {
                    remove: {
                        revision: encodedRevision,
                        idOverride: encodedIdOverride,
                        id: mark.id,
                    },
                };
        }
        case "Rename":
            return encodeRename(mark, context, changeAtomIdCodec, encodeRevision);
        case types_js_1.NoopMarkType:
            (0, internal_1.fail)(0xb2c /* Mark type: NoopMarkType should not be encoded. */);
        default:
            (0, internal_1.unreachableCase)(type);
    }
}
function encodeRevisionWithContext(revision, context, revisionTagCodec) {
    return revision === undefined || revision === context.revision
        ? undefined
        : revisionTagCodec.encode(revision, context);
}
exports.encodeRevisionWithContext = encodeRevisionWithContext;
/**
 * Arbitrary ID that is used to indicate a Rename effect.
 */
const renameLocalId = (0, index_js_3.brand)(-1);
/**
 * If the encoded mark effect represents a simple cell rename (with no effect on root nodes),
 * returns the encoded form of the ID the cell is being renamed to,
 * and otherwise returns undefined.
 */
function tryGetEncodedCellRename(encoded) {
    const attachAndDetach = encoded.attachAndDetach;
    if (attachAndDetach === undefined) {
        return undefined;
    }
}
exports.tryGetEncodedCellRename = tryGetEncodedCellRename;
function tryGetEncodedCellRenameFromAttachAndDetach(encoded) {
    // In documents generated by clients on release >=2.2 (i.e., running the code from the PR that added this comment),
    // renames are encoded as AttachAndDetach with a special id.
    // This ensures forward-compatibility of clients on release <=2.1 with documents/ops generated by clients on release >=2.2.
    if (encoded.attach.insert?.id === renameLocalId) {
        (0, internal_1.assert)(encoded.detach.remove?.idOverride !== undefined, 0x9f8 /* Rename must have idOverride */);
        return encoded.detach.remove.idOverride;
    }
}
//# sourceMappingURL=sequenceFieldCodecV2.js.map