"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.getEndpoint = exports.withRevision = exports.withNodeChange = exports.omitMarkEffect = exports.extractMarkEffect = exports.splitMarkEffect = exports.splitMark = exports.tryMergeMarks = exports.isRemoveMark = exports.isDetach = exports.compareCellsFromSameRevision = exports.areOverlappingIdRanges = exports.isNoopMark = exports.isTombstone = exports.isImpactful = exports.settleMark = exports.areOutputCellsEmpty = exports.areInputCellsEmpty = exports.isImpactfulCellRename = exports.isDetachOfRemovedNodes = exports.isAttachAndDetachEffect = exports.markHasCellEffect = exports.markFillsCells = exports.markEmptiesCells = exports.getInputLength = exports.getOutputLength = exports.cloneCellId = exports.cloneMarkEffect = exports.cloneMark = exports.asAttachAndDetach = exports.normalizeCellRename = exports.getDetachedNodeId = exports.getDetachOutputCellId = exports.compareCellPositionsUsingTombstones = exports.CellOrder = exports.cellSourcesFromMarks = exports.getOutputCellId = exports.getInputCellId = exports.areEqualCellIds = exports.isActiveReattach = exports.isReattachEffect = exports.isReattach = exports.isAttach = exports.isInsert = exports.isRename = exports.isNewAttachEffect = exports.isNewAttach = exports.getNestedChanges = exports.createEmpty = exports.isEmpty = void 0;
exports.getCrossFieldKeys = 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 index_js_3 = require("../modular-schema/index.js");
const types_js_1 = require("./types.js");
function isEmpty(change) {
    for (const mark of change) {
        if (mark.changes !== undefined || mark.type !== undefined) {
            return false;
        }
    }
    return true;
}
exports.isEmpty = isEmpty;
function createEmpty() {
    return [];
}
exports.createEmpty = createEmpty;
function getNestedChanges(change) {
    const output = [];
    let index = 0;
    for (const { changes, cellId, count } of change) {
        if (changes !== undefined) {
            output.push([changes, cellId === undefined ? index : undefined]);
        }
        if (cellId === undefined) {
            index += count;
        }
    }
    return output;
}
exports.getNestedChanges = getNestedChanges;
function isNewAttach(mark, revision) {
    return isNewAttachEffect(mark, mark.cellId, revision);
}
exports.isNewAttach = isNewAttach;
function isNewAttachEffect(effect, cellId, revision) {
    return ((isAttach(effect) &&
        cellId !== undefined &&
        (effect.revision ?? revision) === (cellId.revision ?? revision)) ||
        (isAttachAndDetachEffect(effect) && isNewAttachEffect(effect.attach, cellId, revision)));
}
exports.isNewAttachEffect = isNewAttachEffect;
function isRename(mark) {
    return mark.type === "Rename";
}
exports.isRename = isRename;
function isInsert(mark) {
    return mark.type === "Insert";
}
exports.isInsert = isInsert;
function isAttach(effect) {
    return effect.type === "Insert" || effect.type === "MoveIn";
}
exports.isAttach = isAttach;
function isReattach(mark) {
    return isReattachEffect(mark, mark.cellId);
}
exports.isReattach = isReattach;
function isReattachEffect(effect, cellId) {
    return isAttach(effect) && !isNewAttachEffect(effect, cellId);
}
exports.isReattachEffect = isReattachEffect;
function isActiveReattach(mark) {
    return isAttach(mark) && isReattachEffect(mark, mark.cellId) && mark.cellId !== undefined;
}
exports.isActiveReattach = isActiveReattach;
function areEqualCellIds(a, b) {
    if (a === undefined || b === undefined) {
        return a === b;
    }
    return (0, index_js_1.areEqualChangeAtomIds)(a, b);
}
exports.areEqualCellIds = areEqualCellIds;
function getInputCellId(mark) {
    return mark.cellId;
}
exports.getInputCellId = getInputCellId;
function getOutputCellId(mark) {
    if (isDetach(mark) || isRename(mark)) {
        return getDetachOutputCellId(mark);
    }
    else if (markFillsCells(mark)) {
        return undefined;
    }
    else if (isAttachAndDetachEffect(mark)) {
        return getDetachOutputCellId(mark.detach);
    }
    return getInputCellId(mark);
}
exports.getOutputCellId = getOutputCellId;
function cellSourcesFromMarks(marks, contextGetter) {
    const set = new Set();
    for (const mark of marks) {
        const cell = contextGetter(mark);
        if (cell !== undefined) {
            set.add(cell.revision);
        }
    }
    return set;
}
exports.cellSourcesFromMarks = cellSourcesFromMarks;
var CellOrder;
(function (CellOrder) {
    CellOrder[CellOrder["SameCell"] = 0] = "SameCell";
    CellOrder[CellOrder["OldThenNew"] = 1] = "OldThenNew";
    CellOrder[CellOrder["NewThenOld"] = 2] = "NewThenOld";
})(CellOrder || (exports.CellOrder = CellOrder = {}));
/**
 * Determines the order of two cells from two changesets.
 *
 * This function makes the following assumptions:
 * 1. The cells represent the same context.
 * 2. `oldMarkCell` is from a mark in a changeset that is older than the changeset that contains the mark that
 * `newMarkCell` is from.
 * 3. In terms of sequence index, all cells located before A are also located before B,
 * and all cells located before B are also located before A.
 * 4. If a changeset has a mark/tombstone that describes a cell named in some revision R,
 * then that changeset must contain marks/tombstones for all cells named in R as well as all cells named in later
 * revisions up to its own.
 * 5. If a changeset foo is rebased over a changeset bar, then the rebased version of foo must contain tombstones or
 * marks for all cells referenced or named in bar. It has yet to be determined whether this assumption is necessary
 * for the logic below.
 *
 * @param oldMarkCell - The cell referenced or named by a mark or tombstone from the older changeset.
 * @param newMarkCell - The cell referenced or named by a mark or tombstone from the newer changeset.
 * @param oldChangeKnowledge - The set of revisions that the older changeset has cell representations for.
 * @param newChangeKnowledge - The set of revisions that the newer changeset has cell representations for.
 * @param metadata - Revision metadata for the operation being carried out.
 * @returns a {@link CellOrder} which describes how the cells are ordered relative to one-another.
 */
function compareCellPositionsUsingTombstones(oldMarkCell, newMarkCell, oldChangeKnowledge, newChangeKnowledge, metadata) {
    if ((0, index_js_1.areEqualChangeAtomIds)(oldMarkCell, newMarkCell)) {
        return CellOrder.SameCell;
    }
    const oldChangeKnowsOfNewMarkCellRevision = oldChangeKnowledge.has(newMarkCell.revision);
    const newChangeKnowsOfOldMarkCellRevision = newChangeKnowledge.has(oldMarkCell.revision);
    if (oldChangeKnowsOfNewMarkCellRevision && newChangeKnowsOfOldMarkCellRevision) {
        // If both changesets know of both cells, but we've been asked to compare different cells,
        // Then either the changesets they originate from do not represent the same context,
        // or the ordering of their cells in inconsistent.
        // The only exception to this is when we're composing anonymous changesets in a transaction.
        (0, internal_1.assert)(oldMarkCell.revision === undefined && newMarkCell.revision === undefined, 0x8a0 /* Inconsistent cell ordering */);
        // We are composing anonymous changesets in a transaction. The new changeset is creating a cell in a gap
        // where the old changeset knows of some now empty cell. We order the new cell relative to the old cell in a
        // way that is consistent with its tie-breaking behavior should the old cell be concurrently re-filled.
        // Since only tie-break left is supported at the moment, the new cell comes first.
        return CellOrder.NewThenOld;
    }
    if (newChangeKnowsOfOldMarkCellRevision) {
        // The changeset that contains `newMarkCell` has tombstones for the revision that created `oldMarkCell`,
        // so a tombstone/mark matching `oldMarkCell` must occur later in the newer changeset.
        return CellOrder.NewThenOld;
    }
    else if (oldChangeKnowsOfNewMarkCellRevision) {
        // The changeset that contains `oldMarkCell` has tombstones for revision that created `newMarkCell`,
        // so a tombstone/mark matching `newMarkCell` must occur later in the older changeset.
        return CellOrder.OldThenNew;
    }
    else {
        // These cells are only ordered through tie-breaking.
        // Since tie-breaking is hard-coded to "merge left", the younger cell comes first.
        // In the context of compose, an undefined revision means we are composing anonymous changesets into
        // a transaction, which means the cell from the newer changeset is younger.
        if (newMarkCell.revision === undefined) {
            return CellOrder.NewThenOld;
        }
        // The only case where the old mark cell should have no revision is when composing anonymous changesets
        // into a transaction, in which case the new mark cell should also have no revision, which is handled above.
        // In all other cases, the old mark cell should have a revision.
        (0, internal_1.assert)(oldMarkCell.revision !== undefined, 0x8a1 /* Old mark cell should have a revision */);
        // Note that these indices are for ordering the revisions in which the cells were named, not the revisions
        // of the changesets in which the marks targeting these cells appear.
        const oldCellRevisionIndex = metadata.getIndex(oldMarkCell.revision);
        const newCellRevisionIndex = metadata.getIndex(newMarkCell.revision);
        // If the metadata defines an ordering for the revisions then the cell from the newer revision comes first.
        if (newCellRevisionIndex !== undefined && oldCellRevisionIndex !== undefined) {
            return newCellRevisionIndex > oldCellRevisionIndex
                ? CellOrder.NewThenOld
                : CellOrder.OldThenNew;
        }
        if (newCellRevisionIndex === undefined && oldCellRevisionIndex === undefined) {
            // While it is possible for both marks to refer to cells that were named in revisions that are outside
            // the scope of the metadata, such a scenario should be handled above due to the fact that one of the two
            // changesets should have tombstones or marks for both cells.
            //
            // To see this in the context of rebase, we must consider the lowest common ancestor (LCA) of each change's
            // original (i.e., unrebased) edit with the head of the branch they will both reside on after the rebase.
            // ...─(Ti)─...─(Tj)─...─(old')─(new') <- branch both change will reside on after rebase
            //        |        └─...─(new)
            //        └─...─(old)
            // In the diagram above we can see that by the time `new` is being rebased over `old`, both changesets have
            // been rebased over, and therefore have cell information for, changes `Tj` onwards. This means that one of
            // The two changesets (the `old` one in the diagram above) will have tombstones or marks for any cells that
            // `new` refers to so long as those cells were not created on `new`'s branch.
            // Note that the change that contains the superset of cells (again, ignoring cells created on the other
            // change's branch) is not always the older change. Consider the following scenario:
            // ...─(Ti)─...─(Tj)─...─(old')─(new')
            //        |        └─...─(old)
            //        └─...─(new)
            //
            // The same scenario can arise in the context of compose (just consider composing `old'` and `new'` from
            // the examples above) with the same resolution.
            (0, internal_1.assert)(false, 0x8a2 /* Invalid cell ordering scenario */);
        }
        // The absence of metadata for a cell with a defined revision means that the cell is from a revision that
        // predates the edits that are within the scope of the metadata. Such a cell is therefore older than the one
        // for which we do have metadata.
        return oldCellRevisionIndex === undefined ? CellOrder.NewThenOld : CellOrder.OldThenNew;
    }
}
exports.compareCellPositionsUsingTombstones = compareCellPositionsUsingTombstones;
/**
 * @returns the ID of the cell in the output context of the given detach `mark`.
 */
function getDetachOutputCellId(mark) {
    if (isRename(mark)) {
        return mark.idOverride;
    }
    if (mark.idOverride !== undefined) {
        return mark.idOverride;
    }
    return mark.revision === undefined
        ? { localId: mark.id }
        : { revision: mark.revision, localId: mark.id };
}
exports.getDetachOutputCellId = getDetachOutputCellId;
/**
 * @returns the ID of the detached node in the output context of the given detach `mark`.
 */
function getDetachedNodeId(mark) {
    switch (mark.type) {
        case "Rename":
        case "Remove": {
            return getDetachOutputCellId(mark);
        }
        case "MoveOut": {
            return (0, index_js_1.makeChangeAtomId)(mark.id, mark.revision);
        }
        default:
            (0, internal_1.unreachableCase)(mark);
    }
}
exports.getDetachedNodeId = getDetachedNodeId;
/**
 * Preserves the semantics of the given `mark` but repackages it into a `DetachOfRemovedNodes` when possible.
 */
function normalizeCellRename(cellId, count, attach, detach) {
    if (attach.type === "MoveIn") {
        if (detach.type === "MoveOut") {
            const outputId = getDetachOutputCellId(detach);
            // Note that the output ID may be the same as the cellId. In such a scenario,
            // we output an (impact-less) Rename mark anyway (as opposed to a Skip)
            // because the resulting Rename may be rebased over other changes that rename the input cell,
            // eventually leading to an impactful rename.
            return {
                type: "Rename",
                count,
                cellId,
                idOverride: outputId,
            };
        }
    }
    else {
        // Normalization: when the attach is an insert/revive, we rely on the implicit reviving semantics of the
        // detach instead of using an explicit revive effect in an AttachAndDetach
        return {
            ...detach,
            count,
            cellId,
        };
    }
    return {
        type: "AttachAndDetach",
        attach,
        detach,
        count,
        cellId,
    };
}
exports.normalizeCellRename = normalizeCellRename;
/**
 * Preserves the semantics of the given `mark` but repackages it into an `AttachAndDetach` mark if it is not already one.
 */
function asAttachAndDetach(mark) {
    if (mark.type === "AttachAndDetach") {
        return mark;
    }
    const { cellId, count, changes, revision, ...effect } = mark;
    const attachAndDetach = {
        type: "AttachAndDetach",
        count,
        cellId,
        attach: {
            type: "Insert",
            id: mark.id,
        },
        detach: effect,
    };
    if (changes !== undefined) {
        attachAndDetach.changes = changes;
    }
    if (revision !== undefined) {
        attachAndDetach.attach.revision = revision;
        attachAndDetach.detach.revision = revision;
    }
    return attachAndDetach;
}
exports.asAttachAndDetach = asAttachAndDetach;
function cloneMark(mark) {
    const clone = { ...cloneMarkEffect(mark), count: mark.count };
    if (mark.cellId !== undefined) {
        clone.cellId = cloneCellId(mark.cellId);
    }
    return clone;
}
exports.cloneMark = cloneMark;
function cloneMarkEffect(effect) {
    const clone = { ...effect };
    if (clone.type === "AttachAndDetach") {
        clone.attach = cloneMarkEffect(clone.attach);
        clone.detach = cloneMarkEffect(clone.detach);
    }
    return clone;
}
exports.cloneMarkEffect = cloneMarkEffect;
function cloneCellId(id) {
    const cloned = { ...id };
    return cloned;
}
exports.cloneCellId = cloneCellId;
/**
 * @param mark - The mark to get the length of.
 * @param ignorePairing - When true, the length of a paired mark (e.g. MoveIn/MoveOut) whose matching mark is not active
 * will be treated the same as if the matching mark were active.
 * @returns The number of nodes within the output context of the mark.
 */
function getOutputLength(mark, ignorePairing = false) {
    return areOutputCellsEmpty(mark) ? 0 : mark.count;
}
exports.getOutputLength = getOutputLength;
/**
 * @param mark - The mark to get the length of.
 * @returns The number of nodes within the input context of the mark.
 */
function getInputLength(mark) {
    return areInputCellsEmpty(mark) ? 0 : mark.count;
}
exports.getInputLength = getInputLength;
function markEmptiesCells(mark) {
    return !areInputCellsEmpty(mark) && areOutputCellsEmpty(mark);
}
exports.markEmptiesCells = markEmptiesCells;
function markFillsCells(mark) {
    return areInputCellsEmpty(mark) && !areOutputCellsEmpty(mark);
}
exports.markFillsCells = markFillsCells;
function markHasCellEffect(mark) {
    return areInputCellsEmpty(mark) !== areOutputCellsEmpty(mark);
}
exports.markHasCellEffect = markHasCellEffect;
function isAttachAndDetachEffect(effect) {
    return effect.type === "AttachAndDetach";
}
exports.isAttachAndDetachEffect = isAttachAndDetachEffect;
function isDetachOfRemovedNodes(mark) {
    return isDetach(mark) && mark.cellId !== undefined;
}
exports.isDetachOfRemovedNodes = isDetachOfRemovedNodes;
function isImpactfulCellRename(mark) {
    return (isAttachAndDetachEffect(mark) || isDetachOfRemovedNodes(mark)) && isImpactful(mark);
}
exports.isImpactfulCellRename = isImpactfulCellRename;
function areInputCellsEmpty(mark) {
    return mark.cellId !== undefined;
}
exports.areInputCellsEmpty = areInputCellsEmpty;
function areOutputCellsEmpty(mark) {
    const type = mark.type;
    switch (type) {
        case types_js_1.NoopMarkType:
            return mark.cellId !== undefined;
        case "Remove":
        case "Rename":
        case "MoveOut":
        case "AttachAndDetach":
            return true;
        case "MoveIn":
        case "Insert":
            return false;
        default:
            (0, internal_1.unreachableCase)(type);
    }
}
exports.areOutputCellsEmpty = areOutputCellsEmpty;
/**
 * Creates a mark that is equivalent to the given `mark` but with effects removed if those have no impact in the input
 * context of that mark.
 *
 * @param mark - The mark to settle. Never mutated.
 * @returns either the original mark or a shallow clone of it with effects stripped out.
 */
function settleMark(mark) {
    if (isImpactful(mark)) {
        return mark;
    }
    return omitMarkEffect(mark);
}
exports.settleMark = settleMark;
/**
 * @returns true, iff the given `mark` would have impact on the field when applied.
 * Ignores the impact of nested changes.
 * CellRename effects are considered impactful if they actually change the ID of the cells.
 */
function isImpactful(mark) {
    const type = mark.type;
    switch (type) {
        case types_js_1.NoopMarkType:
            return false;
        case "Rename":
            return true;
        case "Remove": {
            const inputId = getInputCellId(mark);
            if (inputId === undefined) {
                return true;
            }
            const outputId = getOutputCellId(mark);
            (0, internal_1.assert)(outputId !== undefined, 0x824 /* Remove marks must have an output cell ID */);
            return !(0, index_js_1.areEqualChangeAtomIds)(inputId, outputId);
        }
        case "AttachAndDetach":
        case "MoveOut":
            return true;
        case "MoveIn":
            // MoveIn marks always target an empty cell.
            (0, internal_1.assert)(mark.cellId !== undefined, 0x825 /* MoveIn marks should target empty cells */);
            return true;
        case "Insert":
            // A Revive has no impact if the nodes are already in the document.
            return mark.cellId !== undefined;
        default:
            (0, internal_1.unreachableCase)(type);
    }
}
exports.isImpactful = isImpactful;
function isTombstone(mark) {
    return mark.type === types_js_1.NoopMarkType && mark.cellId !== undefined && mark.changes === undefined;
}
exports.isTombstone = isTombstone;
function isNoopMark(mark) {
    return mark.type === types_js_1.NoopMarkType;
}
exports.isNoopMark = isNoopMark;
function areOverlappingIdRanges(id1, count1, id2, count2) {
    const lastId1 = id1 + count1 - 1;
    const lastId2 = id2 + count2 - 1;
    return (id2 <= id1 && id1 <= lastId2) || (id1 <= id2 && id2 <= lastId1);
}
exports.areOverlappingIdRanges = areOverlappingIdRanges;
function compareCellsFromSameRevision(cell1, count1, cell2, count2) {
    (0, internal_1.assert)(cell1.revision === cell2.revision, 0x85b /* Expected cells to have the same revision */);
    if (areOverlappingIdRanges(cell1.localId, count1, cell2.localId, count2)) {
        return cell1.localId - cell2.localId;
    }
    return undefined;
}
exports.compareCellsFromSameRevision = compareCellsFromSameRevision;
function isDetach(mark) {
    const type = mark?.type;
    return type === "Remove" || type === "MoveOut";
}
exports.isDetach = isDetach;
function isRemoveMark(mark) {
    return mark?.type === "Remove";
}
exports.isRemoveMark = isRemoveMark;
function areMergeableChangeAtoms(lhs, lhsCount, rhs) {
    if (lhs === undefined || rhs === undefined) {
        return lhs === undefined && rhs === undefined;
    }
    return (lhs.revision === rhs.revision && areAdjacentIdRanges(lhs.localId, lhsCount, rhs.localId));
}
function areAdjacentIdRanges(firstStart, firstLength, secondStart) {
    return firstStart + firstLength === secondStart;
}
function haveMergeableIdOverrides(lhs, lhsCount, rhs) {
    if (lhs.idOverride !== undefined && rhs.idOverride !== undefined) {
        return areMergeableCellIds(lhs.idOverride, lhsCount, rhs.idOverride);
    }
    return (lhs.idOverride === undefined) === (rhs.idOverride === undefined);
}
function areMergeableCellIds(lhs, lhsCount, rhs) {
    return areMergeableChangeAtoms(lhs, lhsCount, rhs);
}
/**
 * Attempts to extend `lhs` to include the effects of `rhs`.
 * @param lhs - The mark to extend.
 * @param rhs - The effect so extend `rhs` with.
 * @returns `lhs` iff the function was able to mutate `lhs` to include the effects of `rhs`.
 * When `undefined` is returned, `lhs` is left untouched.
 */
function tryMergeMarks(lhs, rhs) {
    if (rhs.type !== lhs.type) {
        return undefined;
    }
    if (!areMergeableCellIds(lhs.cellId, lhs.count, rhs.cellId)) {
        return undefined;
    }
    if (rhs.changes !== undefined || lhs.changes !== undefined) {
        return undefined;
    }
    const mergedEffect = tryMergeEffects(lhs, rhs, lhs.count);
    if (mergedEffect === undefined) {
        return undefined;
    }
    return { ...lhs, ...mergedEffect, count: lhs.count + rhs.count };
}
exports.tryMergeMarks = tryMergeMarks;
function tryMergeEffects(lhs, rhs, lhsCount) {
    if (lhs.type !== rhs.type) {
        return undefined;
    }
    if (rhs.type === types_js_1.NoopMarkType) {
        return lhs;
    }
    if (rhs.type === "AttachAndDetach") {
        const lhsAttachAndDetach = lhs;
        const attach = tryMergeEffects(lhsAttachAndDetach.attach, rhs.attach, lhsCount);
        const detach = tryMergeEffects(lhsAttachAndDetach.detach, rhs.detach, lhsCount);
        if (attach === undefined || detach === undefined) {
            return undefined;
        }
        (0, internal_1.assert)(isAttach(attach) && isDetach(detach), 0x826 /* Merged marks should be same type as input marks */);
        return { ...lhsAttachAndDetach, attach, detach };
    }
    if (lhs.revision !== rhs.revision) {
        return undefined;
    }
    if (isDetach(lhs) && isDetach(rhs) && !haveMergeableIdOverrides(lhs, lhsCount, rhs)) {
        return undefined;
    }
    const type = rhs.type;
    switch (type) {
        case "MoveIn": {
            const lhsMoveIn = lhs;
            if (lhsMoveIn.id + lhsCount === rhs.id &&
                areMergeableChangeAtoms(lhsMoveIn.finalEndpoint, lhsCount, rhs.finalEndpoint)) {
                return lhsMoveIn;
            }
            break;
        }
        case "Remove": {
            const lhsDetach = lhs;
            if (lhsDetach.id + lhsCount === rhs.id &&
                haveMergeableIdOverrides(lhsDetach, lhsCount, rhs)) {
                return lhsDetach;
            }
            break;
        }
        case "Rename": {
            const lhsDetach = lhs;
            if (haveMergeableIdOverrides(lhsDetach, lhsCount, rhs)) {
                return lhsDetach;
            }
            break;
        }
        case "MoveOut": {
            const lhsMoveOut = lhs;
            if (lhsMoveOut.id + lhsCount === rhs.id &&
                haveMergeableIdOverrides(lhsMoveOut, lhsCount, rhs) &&
                areMergeableChangeAtoms(lhsMoveOut.finalEndpoint, lhsCount, rhs.finalEndpoint)) {
                return lhsMoveOut;
            }
            break;
        }
        case "Insert": {
            const lhsInsert = lhs;
            if (lhsInsert.id + lhsCount === rhs.id) {
                return lhsInsert;
            }
            break;
        }
        default:
            (0, internal_1.unreachableCase)(type);
    }
    return undefined;
}
/**
 * Splits the `mark` into two marks such that the first returned mark has length `length`.
 * @param mark - The mark to split.
 * @param revision - The revision of the changeset the mark is part of.
 * @param length - The desired length for the first of the two returned marks.
 * @param genId - An ID allocator
 * @param moveEffects - The table in which to record splitting of move marks
 * @param recordMoveEffect - Whether when splitting a move an entry should be added to `moveEffects` indicating that the mark should be split (in case we process this mark again).
 * An entry is always added to `moveEffects` indicating that the opposite end of the move should be split.
 * @returns A pair of marks equivalent to the original `mark`
 * such that the first returned mark has input length `length`.
 */
function splitMark(mark, length) {
    const markLength = mark.count;
    const remainder = markLength - length;
    if (length < 1 || remainder < 1) {
        (0, index_js_2.fail)("Unable to split mark due to lengths");
    }
    const [effect1, effect2] = splitMarkEffect(mark, length);
    const mark1 = { ...mark, ...effect1, count: length };
    const mark2 = { ...mark, ...effect2, count: remainder };
    if (mark2.cellId !== undefined) {
        mark2.cellId = splitDetachEvent(mark2.cellId, length);
    }
    return [mark1, mark2];
}
exports.splitMark = splitMark;
function splitMarkEffect(effect, length) {
    const type = effect.type;
    switch (type) {
        case types_js_1.NoopMarkType:
            return [effect, effect];
        case "Insert": {
            const effect1 = {
                ...effect,
            };
            const effect2 = {
                ...effect,
                id: effect.id + length,
            };
            return [effect1, effect2];
        }
        case "MoveIn": {
            const effect2 = { ...effect, id: effect.id + length };
            const move2 = effect2;
            if (move2.finalEndpoint !== undefined) {
                move2.finalEndpoint = splitDetachEvent(move2.finalEndpoint, length);
            }
            return [effect, effect2];
        }
        case "Remove": {
            const effect1 = { ...effect };
            const id2 = (0, index_js_2.brand)(effect.id + length);
            const effect2 = { ...effect, id: id2 };
            const effect2Remove = effect2;
            if (effect2Remove.idOverride !== undefined) {
                effect2Remove.idOverride = splitDetachEvent(effect2Remove.idOverride, length);
            }
            return [effect1, effect2];
        }
        case "Rename": {
            const effect1 = { ...effect };
            const effect2 = { ...effect };
            const effect2Rename = effect2;
            if (effect2Rename.idOverride !== undefined) {
                effect2Rename.idOverride = splitDetachEvent(effect2Rename.idOverride, length);
            }
            return [effect1, effect2];
        }
        case "MoveOut": {
            const effect2 = {
                ...effect,
                id: effect.id + length,
            };
            const return2 = effect2;
            if (return2.idOverride !== undefined) {
                return2.idOverride = splitDetachEvent(return2.idOverride, length);
            }
            if (return2.finalEndpoint !== undefined) {
                return2.finalEndpoint = splitDetachEvent(return2.finalEndpoint, length);
            }
            return [effect, effect2];
        }
        case "AttachAndDetach": {
            const [attach1, attach2] = splitMarkEffect(effect.attach, length);
            const [detach1, detach2] = splitMarkEffect(effect.detach, length);
            const effect1 = {
                ...effect,
                attach: attach1,
                detach: detach1,
            };
            const effect2 = {
                ...effect,
                attach: attach2,
                detach: detach2,
            };
            return [effect1, effect2];
        }
        default:
            (0, internal_1.unreachableCase)(type);
    }
}
exports.splitMarkEffect = splitMarkEffect;
function splitDetachEvent(detachEvent, length) {
    return { ...detachEvent, localId: (0, index_js_2.brand)(detachEvent.localId + length) };
}
// TODO: Refactor MarkEffect into a field of CellMark so this function isn't necessary.
function extractMarkEffect(mark) {
    const { cellId: _cellId, count: _count, changes: _changes, ...effect } = mark;
    return effect;
}
exports.extractMarkEffect = extractMarkEffect;
// TODO: Refactor MarkEffect into a field of CellMark so this function isn't necessary.
function omitMarkEffect(mark) {
    const { cellId, count, changes } = mark;
    const noopMark = { count };
    if (cellId !== undefined) {
        noopMark.cellId = cellId;
    }
    if (changes !== undefined) {
        noopMark.changes = changes;
    }
    return noopMark;
}
exports.omitMarkEffect = omitMarkEffect;
function withNodeChange(mark, changes) {
    const newMark = { ...mark };
    if (changes !== undefined) {
        newMark.changes = changes;
    }
    else {
        delete newMark.changes;
    }
    return newMark;
}
exports.withNodeChange = withNodeChange;
function withRevision(mark, revision) {
    if (revision === undefined) {
        return mark;
    }
    const cloned = cloneMark(mark);
    addRevision(cloned, revision);
    if (cloned.cellId !== undefined &&
        cloned.cellId.revision === undefined &&
        revision !== undefined) {
        cloned.cellId.revision = revision;
    }
    return cloned;
}
exports.withRevision = withRevision;
function addRevision(effect, revision) {
    if (effect.type === types_js_1.NoopMarkType || isRename(effect)) {
        return;
    }
    if (effect.type === "AttachAndDetach") {
        addRevision(effect.attach, revision);
        addRevision(effect.detach, revision);
        return;
    }
    (0, internal_1.assert)(effect.revision === undefined || effect.revision === revision, 0x829 /* Should not overwrite mark revision */);
    effect.revision = revision;
}
function getEndpoint(effect) {
    return effect.finalEndpoint ?? { revision: effect.revision, localId: effect.id };
}
exports.getEndpoint = getEndpoint;
function getCrossFieldKeys(change) {
    const keys = [];
    for (const mark of change) {
        keys.push(...getCrossFieldKeysForMarkEffect(mark, mark.count));
    }
    return keys;
}
exports.getCrossFieldKeys = getCrossFieldKeys;
function getCrossFieldKeysForMarkEffect(effect, count) {
    switch (effect.type) {
        case "Insert":
            // An insert behaves like a move where the source and destination are at the same location.
            // An insert can become a move when after rebasing.
            return [
                [index_js_3.CrossFieldTarget.Source, effect.revision, effect.id, count],
                [index_js_3.CrossFieldTarget.Destination, effect.revision, effect.id, count],
            ];
        case "MoveOut":
            return [[index_js_3.CrossFieldTarget.Source, effect.revision, effect.id, count]];
        case "MoveIn":
            return [[index_js_3.CrossFieldTarget.Destination, effect.revision, effect.id, count]];
        case "AttachAndDetach":
            return [
                ...getCrossFieldKeysForMarkEffect(effect.attach, count),
                ...getCrossFieldKeysForMarkEffect(effect.detach, count),
            ];
        default:
            return [];
    }
}
//# sourceMappingURL=utils.js.map