"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.getMarkMoveId = exports.withRevision = exports.withNodeChange = exports.compareLineages = exports.splitMark = exports.newMoveEffectTable = exports.newCrossFieldTable = exports.areComposable = exports.areRebasable = exports.DetachedNodeTracker = exports.tryExtendMark = exports.isDeleteMark = exports.isDetachMark = exports.areOverlappingIdRanges = exports.getOffsetInCellRange = exports.isNoopMark = exports.areOutputCellsEmpty = exports.areInputCellsEmpty = exports.getEffectiveNodeChanges = exports.markIsTransient = exports.markHasCellEffect = exports.markFillsCells = exports.markEmptiesCells = exports.getInputLength = exports.getOutputLength = exports.cloneMark = exports.getCellId = exports.areEqualCellIds = exports.isReturnMuted = exports.isReattachConflicted = exports.isConflictedReattach = exports.isActiveReattach = exports.isReattach = exports.isAttach = exports.isGenerativeMark = exports.isNewAttach = exports.isEmpty = void 0;
const common_utils_1 = require("@fluidframework/common-utils");
const util_1 = require("../../util");
const modular_schema_1 = require("../modular-schema");
const format_1 = require("./format");
const markListFactory_1 = require("./markListFactory");
const moveEffectTable_1 = require("./moveEffectTable");
function isEmpty(change) {
    return change.length === 0;
}
exports.isEmpty = isEmpty;
function isNewAttach(mark) {
    return mark.type === "Insert" || mark.type === "MoveIn";
}
exports.isNewAttach = isNewAttach;
function isGenerativeMark(mark) {
    return mark.type === "Insert" || mark.type === "Revive";
}
exports.isGenerativeMark = isGenerativeMark;
function isAttach(mark) {
    return isNewAttach(mark) || isReattach(mark);
}
exports.isAttach = isAttach;
function isReattach(mark) {
    return mark.type === "Revive" || mark.type === "ReturnTo";
}
exports.isReattach = isReattach;
function isActiveReattach(mark) {
    // No need to check Reattach.lastDeletedBy because it can only be set if the mark is conflicted
    return isReattach(mark) && !isConflictedReattach(mark);
}
exports.isActiveReattach = isActiveReattach;
// TODO: Name is misleading
function isConflictedReattach(mark) {
    return isReattach(mark) && isReattachConflicted(mark);
}
exports.isConflictedReattach = isConflictedReattach;
// TODO: Name is misleading
function isReattachConflicted(mark) {
    return mark.cellId === undefined || isRevertOnlyReattachPreempted(mark);
}
exports.isReattachConflicted = isReattachConflicted;
/**
 * @returns true iff `mark` is a revert-only inverse that cannot be applied because the target cell was concurrently
 * populated (and possibly emptied) by unrelated changes. Here, "unrelated" specifically means they are not an
 * undo/redo pair of the change this this mark is the inverse of.
 */
function isRevertOnlyReattachPreempted(mark) {
    return mark.inverseOf !== undefined && mark.inverseOf !== mark.cellId?.revision;
}
function isReturnMuted(mark) {
    return mark.isSrcConflicted ?? isReattachConflicted(mark);
}
exports.isReturnMuted = isReturnMuted;
function areEqualCellIds(a, b) {
    if (a === undefined || b === undefined) {
        return a === b;
    }
    return (a.localId === b.localId && a.revision === b.revision && areSameLineage(a.lineage, b.lineage));
}
exports.areEqualCellIds = areEqualCellIds;
function getCellId(mark, revision) {
    const cellId = mark.cellId;
    if (cellId === undefined) {
        return undefined;
    }
    if (cellId.revision === undefined && revision !== undefined) {
        return { ...cellId, revision };
    }
    return cellId;
}
exports.getCellId = getCellId;
function cloneMark(mark) {
    const clone = { ...mark };
    if (clone.type === "Insert" || clone.type === "Revive") {
        clone.content = [...clone.content];
    }
    if (clone.cellId !== undefined) {
        clone.cellId = { ...clone.cellId };
        if (clone.cellId.lineage !== undefined) {
            clone.cellId.lineage = [...clone.cellId.lineage];
        }
    }
    return clone;
}
exports.cloneMark = cloneMark;
function areSameLineage(lineage1, lineage2) {
    if (lineage1 === undefined && lineage2 === undefined) {
        return true;
    }
    if (lineage1 === undefined || lineage2 === undefined) {
        return false;
    }
    if (lineage1.length !== lineage2.length) {
        return false;
    }
    for (let i = 0; i < lineage1.length; i++) {
        const event1 = lineage1[i];
        const event2 = lineage2[i];
        if (event1.revision !== event2.revision || event1.offset !== event2.offset) {
            return false;
        }
    }
    return true;
}
/**
 * @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 markIsTransient(mark) {
    return isGenerativeMark(mark) && mark.transientDetach !== undefined;
}
exports.markIsTransient = markIsTransient;
/**
 * @returns The nested changes from `mark` if they apply to the content the mark refers to.
 */
function getEffectiveNodeChanges(mark) {
    const changes = mark.changes;
    if (changes === undefined) {
        return undefined;
    }
    const type = mark.type;
    (0, common_utils_1.assert)(type !== "MoveIn" && type !== "ReturnTo", 0x72f /* MoveIn/ReturnTo marks should not have changes */);
    switch (type) {
        case "Insert":
            return changes;
        case "Revive":
            // So long as the input cell is populated, the nested changes are still effective
            // (even if the revive is preempted) because the nested changes can only target the node in the populated
            // cell.
            return areInputCellsEmpty(mark) && isRevertOnlyReattachPreempted(mark)
                ? undefined
                : changes;
        case format_1.NoopMarkType:
        case "Placeholder":
        case "Delete":
        case "MoveOut":
        case "ReturnFrom":
            return areInputCellsEmpty(mark) ? undefined : changes;
        default:
            (0, common_utils_1.unreachableCase)(type);
    }
}
exports.getEffectiveNodeChanges = getEffectiveNodeChanges;
function areInputCellsEmpty(mark) {
    return mark.cellId !== undefined;
}
exports.areInputCellsEmpty = areInputCellsEmpty;
function areOutputCellsEmpty(mark) {
    const type = mark.type;
    switch (type) {
        case format_1.NoopMarkType:
        case "Placeholder":
            return mark.cellId !== undefined;
        case "Insert":
            return mark.transientDetach !== undefined;
        case "MoveIn":
            return mark.isSrcConflicted ?? false;
        case "Delete":
        case "MoveOut":
            return true;
        case "ReturnFrom":
            return mark.cellId !== undefined || !mark.isDstConflicted;
        case "ReturnTo":
            return (mark.cellId !== undefined &&
                ((mark.isSrcConflicted ?? false) || isReattachConflicted(mark)));
        case "Revive":
            return ((mark.cellId !== undefined && isReattachConflicted(mark)) ||
                mark.transientDetach !== undefined);
        default:
            (0, common_utils_1.unreachableCase)(type);
    }
}
exports.areOutputCellsEmpty = areOutputCellsEmpty;
function isNoopMark(mark) {
    return mark.type === format_1.NoopMarkType;
}
exports.isNoopMark = isNoopMark;
/**
 * @returns The number of cells in the range which come before the position described by `lineage`.
 */
function getOffsetInCellRange(lineage, revision, id, count) {
    if (lineage === undefined || revision === undefined) {
        return undefined;
    }
    for (const event of lineage) {
        if (event.revision === revision &&
            areOverlappingIdRanges(id, count, event.id, event.count)) {
            return event.id + event.offset - id;
        }
    }
    return undefined;
}
exports.getOffsetInCellRange = getOffsetInCellRange;
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 isDetachMark(mark) {
    const type = mark?.type;
    return type === "Delete" || type === "MoveOut" || type === "ReturnFrom";
}
exports.isDetachMark = isDetachMark;
function isDeleteMark(mark) {
    return mark?.type === "Delete";
}
exports.isDeleteMark = isDeleteMark;
function areMergeableChangeAtoms(lhs, lhsCount, rhs) {
    if (lhs === undefined || rhs === undefined) {
        return lhs === undefined && rhs === undefined;
    }
    return lhs.revision === rhs.revision && lhs.localId + lhsCount === rhs.localId;
}
function areMergeableCellIds(lhs, lhsCount, rhs) {
    return (areMergeableChangeAtoms(lhs, lhsCount, rhs) && areSameLineage(lhs?.lineage, rhs?.lineage));
}
/**
 * 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 `true` iff the function was able to mutate `lhs` to include the effects of `rhs`.
 * When `false` is returned, `lhs` is left untouched.
 */
function tryExtendMark(lhs, rhs) {
    if (rhs.type !== lhs.type) {
        return false;
    }
    if (!areMergeableCellIds(lhs.cellId, lhs.count, rhs.cellId)) {
        return false;
    }
    if (rhs.changes !== undefined || lhs.changes !== undefined) {
        return false;
    }
    const type = rhs.type;
    if (type === format_1.NoopMarkType) {
        lhs.count += rhs.count;
        return true;
    }
    if (rhs.revision !== lhs.revision) {
        return false;
    }
    switch (type) {
        case "Insert": {
            const lhsInsert = lhs;
            if (areMergeableChangeAtoms(lhsInsert.transientDetach, lhs.count, rhs.transientDetach)) {
                lhsInsert.content.push(...rhs.content);
                lhsInsert.count += rhs.count;
                return true;
            }
            break;
        }
        case "MoveIn": {
            const lhsMoveIn = lhs;
            if (lhsMoveIn.isSrcConflicted === rhs.isSrcConflicted &&
                lhsMoveIn.id + lhsMoveIn.count === rhs.id) {
                lhsMoveIn.count += rhs.count;
                return true;
            }
            break;
        }
        case "ReturnTo": {
            const lhsReturnTo = lhs;
            if (lhsReturnTo.inverseOf === rhs.inverseOf &&
                lhsReturnTo.isSrcConflicted === rhs.isSrcConflicted &&
                lhsReturnTo.id + lhsReturnTo.count === rhs.id) {
                lhsReturnTo.count += rhs.count;
                return true;
            }
            break;
        }
        case "Delete": {
            const lhsDetach = lhs;
            if (lhsDetach.id + lhsDetach.count === rhs.id) {
                lhsDetach.count += rhs.count;
                return true;
            }
            break;
        }
        case "MoveOut":
        case "ReturnFrom": {
            const lhsMoveOut = lhs;
            if (lhsMoveOut.id + lhsMoveOut.count === rhs.id) {
                lhsMoveOut.count += rhs.count;
                return true;
            }
            break;
        }
        case "Revive": {
            const lhsRevive = lhs;
            if (lhsRevive.inverseOf === rhs.inverseOf &&
                areMergeableChangeAtoms(lhsRevive.transientDetach, lhs.count, rhs.transientDetach)) {
                lhsRevive.content.push(...rhs.content);
                lhsRevive.count += rhs.count;
                return true;
            }
            break;
        }
        default:
            break;
    }
    return false;
}
exports.tryExtendMark = tryExtendMark;
/**
 * Keeps track of the different ways detached nodes may be referred to.
 * Allows updating changesets so they refer to a detached node by the details
 * of the last detach that affected them.
 *
 * WARNING: this code consumes O(N) space and time for marks that affect N nodes.
 * This is code is currently meant for usage in tests.
 * It should be tested and made more efficient before production use.
 */
class DetachedNodeTracker {
    constructor() {
        // Maps the index for a node to its last characterization as a reattached node.
        this.nodes = new Map();
        this.equivalences = [];
    }
    /**
     * Updates the internals of this instance to account for `change` having been applied.
     * @param change - The change that is being applied. Not mutated.
     * Must be applicable (i.e., `isApplicable(change)` must be true).
     */
    apply(change) {
        let index = 0;
        for (const mark of change.change) {
            const inputLength = getInputLength(mark);
            if (markEmptiesCells(mark)) {
                (0, common_utils_1.assert)(isDetachMark(mark), 0x70d /* Only detach marks should empty cells */);
                const newNodes = new Map();
                const after = index + inputLength;
                for (const [k, v] of this.nodes) {
                    if (k >= index) {
                        if (k >= after) {
                            newNodes.set(k - inputLength, v);
                        }
                        else {
                            // The node is removed
                            this.equivalences.push({
                                old: v,
                                new: {
                                    revision: change.rollbackOf ??
                                        mark.revision ??
                                        change.revision ??
                                        (0, util_1.fail)("Unable to track detached nodes"),
                                    localId: (0, util_1.brand)(mark.id + (k - index)),
                                },
                            });
                        }
                    }
                    else {
                        newNodes.set(k, v);
                    }
                }
                this.nodes = newNodes;
            }
            index += inputLength;
        }
        index = 0;
        for (const mark of change.change) {
            const inputLength = getInputLength(mark);
            if (isActiveReattach(mark)) {
                const newNodes = new Map();
                for (const [k, v] of this.nodes) {
                    if (k >= index) {
                        newNodes.set(k + inputLength, v);
                    }
                    else {
                        newNodes.set(k, v);
                    }
                }
                const detachEvent = mark.cellId ?? (0, util_1.fail)("Unable to track detached nodes");
                for (let i = 0; i < mark.count; ++i) {
                    newNodes.set(index + i, {
                        revision: detachEvent.revision,
                        localId: (0, util_1.brand)(detachEvent.localId + i),
                    });
                }
                this.nodes = newNodes;
            }
            if (!markEmptiesCells(mark)) {
                index += inputLength;
            }
        }
    }
    /**
     * Checks whether the given `change` is applicable based on previous changes.
     * @param change - The change to verify the applicability of. Not mutated.
     * @returns false iff `change`'s description of detached nodes is inconsistent with that of changes applied
     * earlier. Returns true otherwise.
     */
    isApplicable(change) {
        for (const mark of change) {
            if (isActiveReattach(mark)) {
                const detachEvent = mark.cellId ?? (0, util_1.fail)("Unable to track detached nodes");
                const revision = detachEvent.revision;
                for (let i = 0; i < mark.count; ++i) {
                    const localId = (0, util_1.brand)(detachEvent.localId + i);
                    const original = { revision, localId };
                    const updated = this.getUpdatedDetach(original);
                    for (const detached of this.nodes.values()) {
                        if (updated.revision === detached.revision &&
                            updated.localId === detached.localId) {
                            // The new change is attempting to reattach nodes in a location that has already been
                            // filled by a prior reattach.
                            return false;
                        }
                    }
                }
            }
        }
        return true;
    }
    /**
     * Creates an updated representation of the given `change` so that it refers to detached nodes using the revision
     * that last detached them.
     * @param change - The change to update. Not mutated.
     * Must be applicable (i.e., `isApplicable(change)` must be true).
     * @param genId - An ID allocator that produces ID unique within this changeset.
     * @returns A change equivalent to `change` that refers to detached nodes using the revision that last detached
     * them. May reuse parts of the input `change` structure.
     */
    update(change) {
        const factory = new markListFactory_1.MarkListFactory();
        for (const mark of change.change) {
            const cloned = cloneMark(mark);
            if (areInputCellsEmpty(cloned) && !isNewAttach(cloned)) {
                let remainder = cloned;
                while (remainder.count > 1) {
                    const [head, tail] = splitMark(remainder, 1);
                    this.updateMark(head);
                    factory.push(head);
                    remainder = tail;
                }
                this.updateMark(remainder);
                factory.push(remainder);
            }
            else {
                factory.push(cloned);
            }
        }
        return {
            ...change,
            change: factory.list,
        };
    }
    updateMark(mark) {
        const detachEvent = mark.cellId;
        const original = { revision: detachEvent.revision, localId: detachEvent.localId };
        const updated = this.getUpdatedDetach(original);
        if (updated.revision !== original.revision || updated.localId !== original.localId) {
            mark.cellId = { ...updated };
        }
    }
    getUpdatedDetach(detach) {
        let curr = detach;
        for (const eq of this.equivalences) {
            if (curr.revision === eq.old.revision && curr.localId === eq.old.localId) {
                curr = eq.new;
            }
        }
        return curr;
    }
}
exports.DetachedNodeTracker = DetachedNodeTracker;
/**
 * Checks whether `branch` changeset is consistent with a `target` changeset that is may be rebased over.
 *
 * WARNING: this code consumes O(N) space and time for marks that affect N nodes.
 * This is code is currently meant for usage in tests.
 * It should be tested and made more efficient before production use.
 *
 * @param branch - The changeset that would be rebased over `target`.
 * @param target - The changeset that `branch` would be rebased over.
 * @returns false iff `branch`'s description of detached nodes is inconsistent with that of `target`.
 * Returns true otherwise.
 */
function areRebasable(branch, target) {
    const indexToReattach = new Map();
    const reattachToIndex = new Map();
    let index = 0;
    for (const mark of branch) {
        if (isActiveReattach(mark)) {
            const list = (0, util_1.getOrAddEmptyToMap)(indexToReattach, index);
            for (let i = 0; i < mark.count; ++i) {
                const detachEvent = mark.cellId ?? (0, util_1.fail)("Unable to track detached nodes");
                const entry = {
                    ...detachEvent,
                    localId: (0, util_1.brand)(detachEvent.localId + i),
                };
                const key = `${entry.revision}|${entry.localId}`;
                (0, common_utils_1.assert)(!reattachToIndex.has(key), 0x506 /* First changeset as inconsistent characterization of detached nodes */);
                list.push(key);
                reattachToIndex.set(key, index);
            }
        }
        index += getInputLength(mark);
    }
    index = 0;
    let listIndex = 0;
    for (const mark of target) {
        if (isActiveReattach(mark)) {
            const list = (0, util_1.getOrAddEmptyToMap)(indexToReattach, index);
            for (let i = 0; i < mark.count; ++i) {
                const detachEvent = mark.cellId ?? (0, util_1.fail)("Unable to track detached nodes");
                const entry = {
                    ...detachEvent,
                    localId: (0, util_1.brand)(detachEvent.localId + i),
                };
                const key = `${entry.revision}|${entry.localId}`;
                const indexInA = reattachToIndex.get(key);
                if (indexInA !== undefined && indexInA !== index) {
                    // change b tries to reattach the same content as change a but in a different location
                    return false;
                }
                if (list.includes(key)) {
                    while (list[listIndex] !== undefined && list[listIndex] !== key) {
                        ++listIndex;
                    }
                    if (list.slice(0, listIndex).includes(key)) {
                        // change b tries to reattach the same content as change a but in a different order
                        return false;
                    }
                }
            }
        }
        const inputLength = getInputLength(mark);
        if (inputLength > 0) {
            listIndex = 0;
        }
        index += inputLength;
    }
    return true;
}
exports.areRebasable = areRebasable;
/**
 * Checks whether sequential changesets are consistent.
 *
 * WARNING: this code consumes O(N) space and time for marks that affect N nodes.
 * This is code is currently meant for usage in tests.
 * It should be tested and made more efficient before production use.
 *
 * @param changes - The changesets that would be composed together.
 * @returns false iff the changesets in `changes` are inconsistent/incompatible in their description of detached nodes.
 * Returns true otherwise.
 */
function areComposable(changes) {
    const tracker = new DetachedNodeTracker();
    for (const change of changes) {
        if (!tracker.isApplicable(change.change)) {
            return false;
        }
        tracker.apply(change);
    }
    return true;
}
exports.areComposable = areComposable;
/**
 * @alpha
 */
function newCrossFieldTable() {
    const srcQueries = new Map();
    const dstQueries = new Map();
    const mapSrc = new Map();
    const mapDst = new Map();
    const getMap = (target) => target === modular_schema_1.CrossFieldTarget.Source ? mapSrc : mapDst;
    const getQueries = (target) => target === modular_schema_1.CrossFieldTarget.Source ? srcQueries : dstQueries;
    const table = {
        srcQueries,
        dstQueries,
        isInvalidated: false,
        mapSrc,
        mapDst,
        get: (target, revision, id, count, addDependency) => {
            if (addDependency) {
                (0, modular_schema_1.addCrossFieldQuery)(getQueries(target), revision, id, count);
            }
            return (0, util_1.getFirstFromRangeMap)(getMap(target).get(revision) ?? [], id, count);
        },
        set: (target, revision, id, count, value, invalidateDependents) => {
            if (invalidateDependents &&
                (0, util_1.getFirstFromRangeMap)(getQueries(target).get(revision) ?? [], id, count) !==
                    undefined) {
                table.isInvalidated = true;
            }
            (0, modular_schema_1.setInCrossFieldMap)(getMap(target), revision, id, count, value);
        },
        reset: () => {
            table.isInvalidated = false;
            table.srcQueries.clear();
            table.dstQueries.clear();
        },
    };
    return table;
}
exports.newCrossFieldTable = newCrossFieldTable;
/**
 * @alpha
 */
function newMoveEffectTable() {
    return newCrossFieldTable();
}
exports.newMoveEffectTable = newMoveEffectTable;
/**
 * 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, util_1.fail)("Unable to split mark due to lengths");
    }
    const type = mark.type;
    switch (type) {
        case format_1.NoopMarkType: {
            const mark1 = { ...mark, count: length };
            const mark2 = { ...mark, count: remainder };
            if (mark.cellId !== undefined) {
                mark2.cellId = splitDetachEvent(mark.cellId, length);
            }
            return [mark1, mark2];
        }
        case "Insert": {
            const mark1 = { ...mark, content: mark.content.slice(0, length), count: length };
            const mark2 = {
                ...mark,
                content: mark.content.slice(length),
                count: remainder,
            };
            if (mark2.cellId !== undefined) {
                mark2.cellId = splitDetachEvent(mark2.cellId, length);
            }
            if (mark.transientDetach !== undefined) {
                mark2.transientDetach = {
                    revision: mark.transientDetach.revision,
                    localId: (0, util_1.brand)(mark.transientDetach.localId + length),
                };
            }
            return [mark1, mark2];
        }
        case "MoveIn":
        case "ReturnTo": {
            const mark1 = { ...mark, count: length };
            const mark2 = { ...mark, id: mark.id + length, count: remainder };
            if (mark.cellId !== undefined) {
                mark2.cellId = splitDetachEvent(mark.cellId, length);
            }
            return [mark1, mark2];
        }
        case "Revive": {
            const mark1 = { ...mark, content: mark.content.slice(0, length), count: length };
            const mark2 = {
                ...mark,
                content: mark.content.slice(length),
                count: remainder,
            };
            if (mark.cellId !== undefined) {
                mark2.cellId = splitDetachEvent(mark.cellId, length);
            }
            if (mark.transientDetach !== undefined) {
                mark2.transientDetach = {
                    revision: mark.transientDetach.revision,
                    localId: (0, util_1.brand)(mark.transientDetach.localId + length),
                };
            }
            return [mark1, mark2];
        }
        case "Delete": {
            const mark1 = { ...mark, count: length };
            const id2 = (0, util_1.brand)(mark.id + length);
            const mark2 = mark.cellId !== undefined
                ? {
                    ...mark,
                    id: id2,
                    count: remainder,
                    cellId: splitDetachEvent(mark.cellId, length),
                }
                : {
                    ...mark,
                    id: id2,
                    count: remainder,
                };
            return [mark1, mark2];
        }
        case "MoveOut":
        case "ReturnFrom": {
            // TODO: Handle detach index for ReturnFrom
            const mark1 = { ...mark, count: length };
            const mark2 = {
                ...mark,
                id: mark.id + length,
                count: remainder,
            };
            if (mark.cellId !== undefined) {
                mark2.cellId = splitDetachEvent(mark.cellId, length);
            }
            return [mark1, mark2];
        }
        case "Placeholder":
            (0, util_1.fail)("TODO");
        default:
            (0, common_utils_1.unreachableCase)(type);
    }
}
exports.splitMark = splitMark;
function splitDetachEvent(detachEvent, length) {
    return { ...detachEvent, localId: (0, util_1.brand)(detachEvent.localId + length) };
}
function compareLineages(lineage1, lineage2) {
    if (lineage1 === undefined || lineage2 === undefined) {
        return 0;
    }
    const lineage1Offsets = new Map();
    for (const event of lineage1) {
        lineage1Offsets.set(event.revision, event.offset);
    }
    for (let i = lineage2.length - 1; i >= 0; i--) {
        const event2 = lineage2[i];
        const offset1 = lineage1Offsets.get(event2.revision);
        if (offset1 !== undefined) {
            const offset2 = event2.offset;
            if (offset1 < offset2) {
                return -1;
            }
            else if (offset1 > offset2) {
                return 1;
            }
        }
    }
    return 0;
}
exports.compareLineages = compareLineages;
function withNodeChange(mark, changes) {
    const newMark = { ...mark };
    if (changes !== undefined) {
        (0, common_utils_1.assert)(mark.type !== "MoveIn" && mark.type !== "ReturnTo", 0x6a7 /* Cannot have a node change on a MoveIn or ReturnTo mark */);
        newMark.changes = changes;
    }
    else {
        delete newMark.changes;
    }
    return newMark;
}
exports.withNodeChange = withNodeChange;
function withRevision(mark, revision) {
    if (revision === undefined) {
        return mark;
    }
    if (isNoopMark(mark)) {
        return mark;
    }
    const cloned = cloneMark(mark);
    cloned.revision = revision;
    return cloned;
}
exports.withRevision = withRevision;
function getMarkMoveId(mark) {
    if ((0, moveEffectTable_1.isMoveMark)(mark)) {
        return mark.id;
    }
    return undefined;
}
exports.getMarkMoveId = getMarkMoveId;
//# sourceMappingURL=utils.js.map