"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.splitMarkOnOutput = exports.splitMarkOnInput = exports.newMoveEffectTable = exports.newCrossFieldTable = exports.areComposable = exports.areRebasable = exports.DetachedNodeTracker = exports.tryExtendMark = exports.isOutputSpanningMark = exports.isInputSpanningMark = exports.isObjMark = exports.isDeleteMark = exports.isDetachMark = exports.dequeueRelatedReattaches = exports.getOffsetAtRevision = exports.isSkipMark = exports.isNetZeroNodeCountChange = exports.getInputLength = exports.getOutputLength = exports.isEqualPlace = exports.getAttachLength = exports.cloneMark = exports.isConflicted = exports.isBlockedReattach = exports.isSkipLikeDetach = exports.isSkipLikeReattach = exports.isConflictedDetach = exports.isConflictedReattach = exports.isActiveDetach = exports.isActiveReattach = exports.isReattach = exports.isAttachInGap = exports.isAttach = exports.isNewAttach = exports.isModify = void 0;
const common_utils_1 = require("@fluidframework/common-utils");
const util_1 = require("../../util");
const modular_schema_1 = require("../modular-schema");
const markListFactory_1 = require("./markListFactory");
const moveEffectTable_1 = require("./moveEffectTable");
function isModify(mark) {
    return isObjMark(mark) && mark.type === "Modify";
}
exports.isModify = isModify;
function isNewAttach(mark) {
    return isObjMark(mark) && (mark.type === "Insert" || mark.type === "MoveIn");
}
exports.isNewAttach = isNewAttach;
function isAttach(mark) {
    return isNewAttach(mark) || isReattach(mark);
}
exports.isAttach = isAttach;
function isAttachInGap(mark) {
    return isNewAttach(mark) || isActiveReattach(mark) || isBlockedReattach(mark);
}
exports.isAttachInGap = isAttachInGap;
function isReattach(mark) {
    return isObjMark(mark) && (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) && !isConflicted(mark);
}
exports.isActiveReattach = isActiveReattach;
function isActiveDetach(mark) {
    return isDetachMark(mark) && !isConflicted(mark);
}
exports.isActiveDetach = isActiveDetach;
function isConflictedReattach(mark) {
    return isReattach(mark) && isConflicted(mark);
}
exports.isConflictedReattach = isConflictedReattach;
function isConflictedDetach(mark) {
    return isDetachMark(mark) && isConflicted(mark);
}
exports.isConflictedDetach = isConflictedDetach;
function isSkipLikeReattach(mark) {
    return isConflictedReattach(mark) && mark.lastDetachedBy === undefined;
}
exports.isSkipLikeReattach = isSkipLikeReattach;
function isSkipLikeDetach(mark) {
    return isDetachMark(mark) && mark.type !== "Delete" && mark.isDstConflicted === true;
}
exports.isSkipLikeDetach = isSkipLikeDetach;
function isBlockedReattach(mark) {
    return isConflictedReattach(mark) && mark.lastDetachedBy !== undefined;
}
exports.isBlockedReattach = isBlockedReattach;
function isConflicted(mark) {
    return mark.conflictsWith !== undefined;
}
exports.isConflicted = isConflicted;
function cloneMark(mark) {
    if (isSkipMark(mark)) {
        return mark;
    }
    const objMark = mark;
    const clone = Object.assign({}, objMark);
    if (clone.type === "Insert" || clone.type === "Revive") {
        clone.content = [...clone.content];
    }
    if (isAttach(clone) && clone.lineage !== undefined) {
        clone.lineage = [...clone.lineage];
    }
    return clone;
}
exports.cloneMark = cloneMark;
function getAttachLength(attach) {
    const type = attach.type;
    switch (type) {
        case "Insert":
            return attach.content.length;
        case "MoveIn":
        case "Revive":
        case "ReturnTo":
            return attach.count;
        default:
            (0, common_utils_1.unreachableCase)(type);
    }
}
exports.getAttachLength = getAttachLength;
/**
 * @returns `true` iff `lhs` and `rhs`'s `HasTiebreakPolicy` fields are structurally equal.
 */
function isEqualPlace(lhs, rhs) {
    var _a, _b;
    return (lhs.heed === rhs.heed &&
        lhs.tiebreak === rhs.tiebreak &&
        areSameLineage((_a = lhs.lineage) !== null && _a !== void 0 ? _a : [], (_b = rhs.lineage) !== null && _b !== void 0 ? _b : []));
}
exports.isEqualPlace = isEqualPlace;
function areSameLineage(lineage1, lineage2) {
    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) {
    if (isSkipMark(mark)) {
        return mark;
    }
    const type = mark.type;
    switch (type) {
        case "ReturnTo":
            return mark.isSrcConflicted && !ignorePairing ? 0 : mark.count;
        case "Revive":
        case "MoveIn":
            return mark.count;
        case "Insert":
            return mark.content.length;
        case "Modify":
            return 1;
        case "ReturnFrom":
            return mark.isDstConflicted && !ignorePairing ? mark.count : 0;
        case "Delete":
        case "MoveOut":
            return 0;
        default:
            (0, common_utils_1.unreachableCase)(type);
    }
}
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) {
    if (isSkipMark(mark)) {
        return mark;
    }
    if (isAttach(mark)) {
        return isSkipLikeReattach(mark) ? mark.count : 0;
    }
    const type = mark.type;
    switch (type) {
        case "Delete":
        case "MoveOut":
        case "ReturnFrom":
            return isConflicted(mark) ? 0 : mark.count;
        case "Modify":
            return 1;
        default:
            (0, common_utils_1.unreachableCase)(type);
    }
}
exports.getInputLength = getInputLength;
function isNetZeroNodeCountChange(mark) {
    return isSkipMark(mark) || isModify(mark) || isSkipLikeDetach(mark) || isSkipLikeReattach(mark);
}
exports.isNetZeroNodeCountChange = isNetZeroNodeCountChange;
function isSkipMark(mark) {
    return typeof mark === "number";
}
exports.isSkipMark = isSkipMark;
function getOffsetAtRevision(lineage, reattachRevision) {
    if (lineage === undefined || reattachRevision === undefined) {
        return undefined;
    }
    for (const event of lineage) {
        if (event.revision === reattachRevision) {
            return event.offset;
        }
    }
    return undefined;
}
exports.getOffsetAtRevision = getOffsetAtRevision;
function dequeueRelatedReattaches(newMarks, baseMarks) {
    const newMark = newMarks.peek();
    const baseMark = baseMarks.peek();
    (0, common_utils_1.assert)(newMark !== undefined && isReattach(newMark), 0x504 /* No new reattach mark to line up */);
    (0, common_utils_1.assert)(baseMark !== undefined && isReattach(baseMark), 0x505 /* No base reattach mark to line up */);
    const newMarkLength = newMark.count;
    const baseMarkLength = baseMark.count;
    if (newMark.detachIndex === baseMark.detachIndex) {
        if (newMarkLength < baseMarkLength) {
            return {
                baseMark: baseMarks.dequeueOutput(newMarkLength),
                newMark: newMarks.dequeue(),
            };
        }
        else if (newMarkLength > baseMarkLength) {
            return {
                baseMark: baseMarks.dequeue(),
                newMark: newMarks.dequeueOutput(baseMarkLength, true),
            };
        }
        else {
            return {
                baseMark: baseMarks.dequeue(),
                newMark: newMarks.dequeue(),
            };
        }
    }
    else if (newMark.detachIndex < baseMark.detachIndex) {
        if (newMark.detachIndex + newMarkLength <= baseMark.detachIndex) {
            return { newMark: newMarks.dequeue() };
        }
        return {
            newMark: newMarks.dequeueOutput(baseMark.detachIndex - newMark.detachIndex, true),
        };
    }
    else {
        if (baseMark.detachIndex + baseMarkLength <= newMark.detachIndex) {
            return { baseMark: baseMarks.dequeue() };
        }
        return {
            baseMark: baseMarks.dequeueOutput(newMark.detachIndex - baseMark.detachIndex),
        };
    }
}
exports.dequeueRelatedReattaches = dequeueRelatedReattaches;
function isDetachMark(mark) {
    if (isObjMark(mark)) {
        const type = mark.type;
        return type === "Delete" || type === "MoveOut" || type === "ReturnFrom";
    }
    return false;
}
exports.isDetachMark = isDetachMark;
function isDeleteMark(mark) {
    return isObjMark(mark) && mark.type === "Delete";
}
exports.isDeleteMark = isDeleteMark;
function isObjMark(mark) {
    return typeof mark === "object";
}
exports.isObjMark = isObjMark;
function isInputSpanningMark(mark) {
    return (isSkipMark(mark) ||
        mark.type === "Modify" ||
        (isDetachMark(mark) && !isConflicted(mark)) ||
        (isConflictedReattach(mark) && !isBlockedReattach(mark)));
}
exports.isInputSpanningMark = isInputSpanningMark;
function isOutputSpanningMark(mark) {
    return (isSkipMark(mark) ||
        mark.type === "Modify" ||
        isNewAttach(mark) ||
        (isReattach(mark) && !isBlockedReattach(mark)));
}
exports.isOutputSpanningMark = isOutputSpanningMark;
/**
 * 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, revision, moveEffects, recordMerges) {
    if (rhs.type !== lhs.type) {
        return false;
    }
    const type = rhs.type;
    if (type !== "Modify" && rhs.revision !== lhs.revision) {
        return false;
    }
    if ((type !== "MoveIn" && type !== "ReturnTo" && rhs.changes !== undefined) ||
        lhs.changes !== undefined) {
        return false;
    }
    switch (type) {
        case "Insert": {
            const lhsInsert = lhs;
            if (isEqualPlace(lhsInsert, rhs) &&
                lhsInsert.id + lhsInsert.content.length === rhs.id) {
                lhsInsert.content.push(...rhs.content);
                return true;
            }
            break;
        }
        case "MoveIn":
        case "ReturnTo": {
            const lhsMoveIn = lhs;
            if (isEqualPlace(lhsMoveIn, rhs) &&
                moveEffects !== undefined &&
                lhsMoveIn.isSrcConflicted === rhs.isSrcConflicted &&
                tryMergeMoves(modular_schema_1.CrossFieldTarget.Destination, lhsMoveIn, rhs, revision, moveEffects, recordMerges)) {
                return true;
            }
            break;
        }
        case "Delete": {
            const lhsDetach = lhs;
            lhsDetach.count += rhs.count;
            return true;
        }
        case "MoveOut": {
            const lhsMoveOut = lhs;
            if (moveEffects !== undefined &&
                tryMergeMoves(modular_schema_1.CrossFieldTarget.Source, lhsMoveOut, rhs, revision, moveEffects, recordMerges)) {
                return true;
            }
            break;
        }
        case "ReturnFrom": {
            const lhsReturn = lhs;
            if (areMergeableReturnFrom(lhsReturn, rhs) &&
                moveEffects !== undefined &&
                tryMergeMoves(modular_schema_1.CrossFieldTarget.Source, lhsReturn, rhs, revision, moveEffects, recordMerges)) {
                return true;
            }
            break;
        }
        case "Revive": {
            const lhsReattach = lhs;
            if (rhs.detachedBy === lhsReattach.detachedBy &&
                rhs.conflictsWith === lhsReattach.conflictsWith &&
                rhs.isIntention === lhsReattach.isIntention &&
                rhs.lastDetachedBy === lhsReattach.lastDetachedBy &&
                lhsReattach.detachIndex + lhsReattach.count === rhs.detachIndex) {
                lhsReattach.content.push(...rhs.content);
                lhsReattach.count += rhs.count;
                return true;
            }
            break;
        }
        default:
            break;
    }
    return false;
}
exports.tryExtendMark = tryExtendMark;
function tryMergeMoves(target, left, right, revision, moveEffects, recordMerges) {
    var _a;
    if (left.conflictsWith !== right.conflictsWith) {
        return false;
    }
    const rev = (_a = left.revision) !== null && _a !== void 0 ? _a : revision;
    const oppEnd = target === modular_schema_1.CrossFieldTarget.Source ? modular_schema_1.CrossFieldTarget.Destination : modular_schema_1.CrossFieldTarget.Source;
    const prevMergeId = (0, moveEffectTable_1.getMoveEffect)(moveEffects, oppEnd, rev, left.id, false).mergeRight;
    if (prevMergeId !== undefined && prevMergeId !== right.id) {
        (0, moveEffectTable_1.makeMergeable)(moveEffects, oppEnd, rev, prevMergeId, right.id);
    }
    else {
        (0, moveEffectTable_1.makeMergeable)(moveEffects, oppEnd, rev, left.id, right.id);
    }
    const leftEffect = (0, moveEffectTable_1.getMoveEffect)(moveEffects, target, rev, left.id);
    if (leftEffect.mergeRight === right.id) {
        const rightEffect = (0, moveEffectTable_1.getMoveEffect)(moveEffects, target, rev, right.id);
        (0, common_utils_1.assert)(rightEffect.mergeLeft === left.id, 0x56d /* Inconsistent merge info */);
        const nextId = rightEffect.mergeRight;
        if (nextId !== undefined) {
            (0, moveEffectTable_1.makeMergeable)(moveEffects, target, rev, left.id, nextId);
        }
        else {
            leftEffect.mergeRight = undefined;
        }
        if (recordMerges) {
            (0, moveEffectTable_1.splitMove)(moveEffects, target, revision, left.id, right.id, left.count, right.count);
            // TODO: This breaks the nextId mergeability, and would also be overwritten if we merged again
            (0, moveEffectTable_1.makeMergeable)(moveEffects, target, revision, left.id, right.id);
        }
        left.count += right.count;
        return true;
    }
    return false;
}
function areMergeableReturnFrom(lhs, rhs) {
    if (lhs.detachedBy !== rhs.detachedBy ||
        lhs.conflictsWith !== rhs.conflictsWith ||
        lhs.isDstConflicted !== rhs.isDstConflicted ||
        lhs.revision !== rhs.revision) {
        return false;
    }
    if (lhs.detachIndex !== undefined) {
        return lhs.detachIndex + lhs.count === rhs.detachIndex;
    }
    return rhs.detachIndex === undefined;
}
/**
 * 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) {
        var _a, _b, _c, _d;
        let index = 0;
        for (const mark of change.change) {
            const inputLength = getInputLength(mark);
            if (isDetachMark(mark)) {
                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: {
                                    rev: (_c = (_b = (_a = change.rollbackOf) !== null && _a !== void 0 ? _a : mark.revision) !== null && _b !== void 0 ? _b : change.revision) !== null && _c !== void 0 ? _c : (0, util_1.fail)("Unable to track detached nodes"),
                                    index: k,
                                },
                            });
                        }
                    }
                    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);
                    }
                }
                for (let i = 0; i < mark.count; ++i) {
                    newNodes.set(index + i, {
                        rev: (_d = mark.detachedBy) !== null && _d !== void 0 ? _d : (0, util_1.fail)("Unable to track detached nodes"),
                        index: mark.detachIndex + i,
                    });
                }
                this.nodes = newNodes;
            }
            if (!isDetachMark(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) {
        var _a;
        for (const mark of change) {
            if (isActiveReattach(mark)) {
                const rev = (_a = mark.detachedBy) !== null && _a !== void 0 ? _a : (0, util_1.fail)("Unable to track detached nodes");
                for (let i = 0; i < mark.count; ++i) {
                    const index = mark.detachIndex + i;
                    const original = { rev, index };
                    const updated = this.getUpdatedDetach(original);
                    for (const detached of this.nodes.values()) {
                        if (updated.rev === detached.rev && updated.index === detached.index) {
                            // 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, genId) {
        const moveEffects = newMoveEffectTable();
        const factory = new markListFactory_1.MarkListFactory(change.revision, moveEffects);
        const iter = new util_1.StackyIterator(change.change);
        while (!iter.done) {
            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
            const preSplit = iter.pop();
            const splitMarks = (0, moveEffectTable_1.applyMoveEffectsToMark)(preSplit, change.revision, moveEffects, true);
            const mark = splitMarks[0];
            for (let i = splitMarks.length - 1; i > 0; i--) {
                iter.push(splitMarks[i]);
            }
            const cloned = cloneMark(mark);
            if (isReattach(cloned)) {
                let remainder = cloned;
                for (let i = 1; i < cloned.count; ++i) {
                    const [head, tail] = splitMarkOnOutput(remainder, change.revision, 1, genId, moveEffects, false, true);
                    this.updateMark(head, change.revision, moveEffects);
                    factory.push(head);
                    remainder = tail;
                }
                this.updateMark(remainder, change.revision, moveEffects);
                factory.push(remainder);
            }
            else {
                factory.push(cloned);
            }
        }
        // We may need to apply the effects of updateMoveSrcDetacher for some marks if those were located
        // before their corresponding detach mark.
        const factory2 = new markListFactory_1.MarkListFactory(change.revision, moveEffects);
        for (const mark of factory.list) {
            const splitMarks = (0, moveEffectTable_1.applyMoveEffectsToMark)(mark, change.revision, moveEffects, true);
            factory2.push(...splitMarks);
        }
        return Object.assign(Object.assign({}, change), { change: factory2.list });
    }
    updateMark(mark, revision, moveEffects) {
        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
        const original = { rev: mark.detachedBy, index: mark.detachIndex };
        const updated = this.getUpdatedDetach(original);
        if (updated.rev !== original.rev || updated.index !== original.index) {
            if (mark.isIntention === true) {
                mark.detachedBy = updated.rev;
            }
            else if (updated.rev !== mark.detachedBy) {
                mark.lastDetachedBy = updated.rev;
            }
            mark.detachIndex = updated.index;
        }
    }
    getUpdatedDetach(detach) {
        let curr = detach;
        for (const eq of this.equivalences) {
            if (curr.rev === eq.old.rev && curr.index === eq.old.index) {
                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) {
    var _a, _b;
    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 entry = {
                    rev: (_a = mark.detachedBy) !== null && _a !== void 0 ? _a : (0, util_1.fail)("Unable to track detached nodes"),
                    index: mark.detachIndex + i,
                };
                const key = `${entry.rev}|${entry.index}`;
                (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 entry = {
                    rev: (_b = mark.detachedBy) !== null && _b !== void 0 ? _b : (0, util_1.fail)("Unable to track detached nodes"),
                    index: mark.detachIndex + i,
                };
                const key = `${entry.rev}|${entry.index}`;
                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, addDependency) => {
            if (addDependency) {
                (0, util_1.addToNestedSet)(getQueries(target), revision, id);
            }
            return (0, util_1.tryGetFromNestedMap)(getMap(target), revision, id);
        },
        getOrCreate: (target, revision, id, defaultValue, invalidateDependents) => {
            if (invalidateDependents && (0, util_1.nestedSetContains)(getQueries(target), revision, id)) {
                table.isInvalidated = true;
            }
            return (0, util_1.getOrAddInNestedMap)(getMap(target), revision, id, defaultValue);
        },
        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 input 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 splitMarkOnInput(mark, revision, length, genId, moveEffects, recordMoveEffect = false) {
    var _a, _b, _c, _d;
    const markLength = getInputLength(mark);
    const remainder = markLength - length;
    if (length < 1 || remainder < 1) {
        (0, util_1.fail)("Unable to split mark due to lengths");
    }
    if (isSkipMark(mark)) {
        return [length, remainder];
    }
    const markObj = mark;
    const type = mark.type;
    switch (type) {
        case "Modify":
            (0, util_1.fail)("Unable to split Modify mark of length 1");
        case "ReturnTo": {
            const newId = genId();
            (0, moveEffectTable_1.splitMove)(moveEffects, modular_schema_1.CrossFieldTarget.Source, (_a = mark.revision) !== null && _a !== void 0 ? _a : revision, mark.id, newId, length, remainder);
            if (recordMoveEffect) {
                (0, moveEffectTable_1.splitMove)(moveEffects, modular_schema_1.CrossFieldTarget.Destination, (_b = mark.revision) !== null && _b !== void 0 ? _b : revision, mark.id, newId, length, remainder);
            }
            return [
                Object.assign(Object.assign({}, markObj), { count: length }),
                Object.assign(Object.assign({}, markObj), { id: newId, count: remainder, detachIndex: mark.detachIndex + length }),
            ];
        }
        case "Revive":
            return [
                Object.assign(Object.assign({}, markObj), { content: mark.content.slice(0, length), count: length }),
                Object.assign(Object.assign({}, markObj), { content: mark.content.slice(length), count: remainder, detachIndex: mark.detachIndex + length }),
            ];
        case "Delete":
            return [
                Object.assign(Object.assign({}, markObj), { count: length }),
                Object.assign(Object.assign({}, markObj), { count: remainder }),
            ];
        case "MoveOut":
        case "ReturnFrom": {
            // TODO: Handle detach index for ReturnFrom
            const newId = genId();
            (0, moveEffectTable_1.splitMove)(moveEffects, modular_schema_1.CrossFieldTarget.Destination, (_c = mark.revision) !== null && _c !== void 0 ? _c : revision, mark.id, newId, length, remainder);
            if (recordMoveEffect) {
                (0, moveEffectTable_1.splitMove)(moveEffects, modular_schema_1.CrossFieldTarget.Source, (_d = mark.revision) !== null && _d !== void 0 ? _d : revision, mark.id, newId, length, remainder);
            }
            const mark1 = Object.assign(Object.assign({}, markObj), { count: length });
            const mark2 = Object.assign(Object.assign({}, markObj), { id: newId, count: remainder });
            if (mark.type === "ReturnFrom" && mark.detachIndex !== undefined) {
                mark2.detachIndex = mark.detachIndex + length;
            }
            return [mark1, mark2];
        }
        default:
            (0, common_utils_1.unreachableCase)(type);
    }
}
exports.splitMarkOnInput = splitMarkOnInput;
/**
 * Splits the `mark` into two marks such that the first returned mark has output 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 output length `length`.
 */
function splitMarkOnOutput(mark, revision, length, genId, moveEffects, recordMoveEffect = false, ignorePairing = false) {
    var _a, _b;
    const markLength = getOutputLength(mark, ignorePairing);
    const remainder = markLength - length;
    if (length < 1 || remainder < 1) {
        (0, util_1.fail)("Unable to split mark due to lengths");
    }
    if (isSkipMark(mark)) {
        return [length, remainder];
    }
    const markObj = mark;
    const type = markObj.type;
    switch (type) {
        case "Modify":
            (0, util_1.fail)(`Unable to split ${type} mark of length 1`);
        case "Insert":
            return [
                Object.assign(Object.assign({}, markObj), { content: markObj.content.slice(0, length) }),
                Object.assign(Object.assign({}, markObj), { content: markObj.content.slice(length), id: markObj.id + length }),
            ];
        case "MoveIn":
        case "ReturnTo": {
            const newId = genId();
            (0, moveEffectTable_1.splitMove)(moveEffects, modular_schema_1.CrossFieldTarget.Source, (_a = markObj.revision) !== null && _a !== void 0 ? _a : revision, markObj.id, newId, length, remainder);
            if (recordMoveEffect) {
                (0, moveEffectTable_1.splitMove)(moveEffects, modular_schema_1.CrossFieldTarget.Destination, (_b = markObj.revision) !== null && _b !== void 0 ? _b : revision, markObj.id, newId, length, remainder);
            }
            return [
                Object.assign(Object.assign({}, markObj), { count: length }),
                type === "MoveIn"
                    ? Object.assign(Object.assign({}, markObj), { id: newId, count: remainder }) : Object.assign(Object.assign({}, markObj), { id: newId, count: remainder, detachIndex: markObj.detachIndex + length }),
            ];
        }
        case "Revive":
            return [
                Object.assign(Object.assign({}, markObj), { content: markObj.content.slice(0, length), count: length }),
                Object.assign(Object.assign({}, markObj), { content: markObj.content.slice(length), count: remainder, detachIndex: markObj.detachIndex + length }),
            ];
        default:
            (0, common_utils_1.unreachableCase)(type);
    }
}
exports.splitMarkOnOutput = splitMarkOnOutput;
//# sourceMappingURL=utils.js.map