"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.rebase = void 0;
const internal_1 = require("@fluidframework/core-utils/internal");
const index_js_1 = require("../../util/index.js");
const index_js_2 = require("../modular-schema/index.js");
const markListFactory_js_1 = require("./markListFactory.js");
const markQueue_js_1 = require("./markQueue.js");
const moveEffectTable_js_1 = require("./moveEffectTable.js");
const types_js_1 = require("./types.js");
const utils_js_1 = require("./utils.js");
/**
 * Rebases `change` over `base` assuming they both apply to the same initial state.
 * @param change - The changeset to rebase.
 * @param base - The changeset to rebase over.
 * @returns A changeset that performs the changes in `change` but does so assuming `base` has been applied first.
 */
function rebase(change, base, rebaseChild, genId, manager, revisionMetadata) {
    return rebaseMarkList(change, base, revisionMetadata, rebaseChild, genId, manager);
}
exports.rebase = rebase;
function rebaseMarkList(currMarkList, baseMarkList, metadata, rebaseChild, genId, moveEffects) {
    const factory = new markListFactory_js_1.MarkListFactory();
    const queue = new RebaseQueue(baseMarkList, currMarkList, metadata, moveEffects);
    while (!queue.isEmpty()) {
        const { baseMark, newMark: currMark } = queue.pop();
        const rebasedMark = rebaseMark(currMark, baseMark, rebaseChild, moveEffects);
        factory.push(rebasedMark);
    }
    return factory.list;
}
/**
 * Generates a NoOp mark that targets the same cells as the input mark.
 * @param mark - The mark the NoOp should target.
 * @param revision - The revision, if available.
 * @returns A NoOp mark that targets the same cells as the input mark.
 */
function generateNoOpWithCellId(mark) {
    const length = mark.count;
    const cellId = (0, utils_js_1.getInputCellId)(mark);
    return cellId === undefined ? { count: length } : { count: length, cellId };
}
class RebaseQueue {
    constructor(baseMarks, newMarks, metadata, moveEffects) {
        this.metadata = metadata;
        this.moveEffects = moveEffects;
        this.baseMarks = new markQueue_js_1.MarkQueue(baseMarks, moveEffects);
        this.newMarks = new markQueue_js_1.MarkQueue(newMarks, moveEffects);
        this.baseMarksCellSources = (0, utils_js_1.cellSourcesFromMarks)(baseMarks, utils_js_1.getInputCellId);
        this.newMarksCellSources = (0, utils_js_1.cellSourcesFromMarks)(newMarks, utils_js_1.getInputCellId);
    }
    isEmpty() {
        return this.baseMarks.isEmpty() && this.newMarks.isEmpty();
    }
    pop() {
        const baseMark = this.baseMarks.peek();
        const newMark = this.newMarks.peek();
        (0, internal_1.assert)(!(baseMark === undefined && newMark === undefined), 0x722 /* Cannot pop from empty queue */);
        if (baseMark === undefined) {
            const dequeuedNewMark = this.newMarks.dequeue();
            return {
                baseMark: generateNoOpWithCellId(dequeuedNewMark),
                newMark: dequeuedNewMark,
            };
        }
        else if (newMark === undefined) {
            return this.dequeueBase();
        }
        else if ((0, utils_js_1.areInputCellsEmpty)(baseMark) && (0, utils_js_1.areInputCellsEmpty)(newMark)) {
            const baseId = (0, utils_js_1.getInputCellId)(baseMark);
            const newId = (0, utils_js_1.getInputCellId)(newMark);
            (0, internal_1.assert)(baseId !== undefined && newId !== undefined, 0x89f /* Both marks should have cell IDs */);
            const comparison = (0, utils_js_1.compareCellPositionsUsingTombstones)(baseId, newId, this.baseMarksCellSources, this.newMarksCellSources, this.metadata);
            switch (comparison) {
                case utils_js_1.CellOrder.SameCell:
                    return this.dequeueBoth();
                case utils_js_1.CellOrder.OldThenNew:
                    return this.dequeueBase();
                case utils_js_1.CellOrder.NewThenOld:
                    return this.dequeueNew();
                default:
                    (0, internal_1.unreachableCase)(comparison);
            }
        }
        else if ((0, utils_js_1.areInputCellsEmpty)(newMark)) {
            return this.dequeueNew();
        }
        else if ((0, utils_js_1.areInputCellsEmpty)(baseMark)) {
            return this.dequeueBase();
        }
        else {
            return this.dequeueBoth();
        }
    }
    dequeueBase(length) {
        const baseMark = length !== undefined ? this.baseMarks.dequeueUpTo(length) : this.baseMarks.dequeue();
        let newMark = generateNoOpWithCellId(baseMark);
        const movedEffect = getMovedEffectFromBaseMark(this.moveEffects, baseMark);
        if (movedEffect !== undefined) {
            newMark = addMovedMarkEffect(newMark, movedEffect);
        }
        return {
            baseMark,
            newMark,
        };
    }
    dequeueNew() {
        const newMark = this.newMarks.dequeue();
        return { newMark, baseMark: generateNoOpWithCellId(newMark) };
    }
    dequeueBoth() {
        const baseMark = this.baseMarks.peek();
        const newMark = this.newMarks.peek();
        (0, internal_1.assert)(baseMark !== undefined && newMark !== undefined, 0x69c /* Cannot dequeue both unless both mark queues are non-empty */);
        const length = Math.min(newMark.count, baseMark.count);
        const sizedBaseMark = this.baseMarks.dequeueUpTo(length);
        const sizedNewMark = this.newMarks.dequeueUpTo(length);
        const movedMark = getMovedEffectFromBaseMark(this.moveEffects, sizedBaseMark);
        return {
            baseMark: sizedBaseMark,
            newMark: movedMark === undefined ? sizedNewMark : addMovedMarkEffect(sizedNewMark, movedMark),
        };
    }
}
/**
 * Combines `mark` and `effect` into a single mark.
 * This function is only intended to handle cases where `mark` is part of a changeset being rebased
 * and `effect` is an effect from the same changeset whose target has been moved by the base changeset.
 * @returns a mark which has the composite effect of `mark` and `effect`.
 */
function addMovedMarkEffect(mark, effect) {
    if ((0, moveEffectTable_js_1.isMoveIn)(mark) && (0, moveEffectTable_js_1.isMoveOut)(effect)) {
        return { ...mark, type: "Insert" };
    }
    else if ((0, utils_js_1.isAttachAndDetachEffect)(mark) && (0, moveEffectTable_js_1.isMoveIn)(mark.attach) && (0, moveEffectTable_js_1.isMoveOut)(effect)) {
        return { ...mark.detach, count: mark.count };
    }
    else if ((0, utils_js_1.isTombstone)(mark)) {
        return { ...mark, ...effect };
    }
    (0, internal_1.assert)(false, 0x818 /* Unexpected combination of mark effects at source and destination */);
}
function rebaseMark(currMark, baseMark, rebaseChild, moveEffects) {
    const rebasedMark = rebaseNodeChange((0, utils_js_1.cloneMark)(currMark), baseMark, rebaseChild);
    const movedNodeChanges = getMovedChangesFromBaseMark(moveEffects, baseMark);
    if (movedNodeChanges !== undefined) {
        (0, internal_1.assert)(rebasedMark.changes === undefined, 0x8dc /* Unexpected collision of new node changes */);
        rebasedMark.changes = movedNodeChanges;
    }
    return rebaseMarkIgnoreChild(rebasedMark, baseMark, moveEffects);
}
function rebaseMarkIgnoreChild(currMark, baseMark, moveEffects) {
    let rebasedMark;
    if ((0, utils_js_1.isDetach)(baseMark)) {
        if (baseMark.cellId !== undefined) {
            // Detaches on empty cells have an implicit revive effect.
            delete currMark.cellId;
        }
        (0, internal_1.assert)(!(0, utils_js_1.isNewAttach)(currMark), 0x69d /* A new attach should not be rebased over its cell being emptied */);
        const baseCellId = (0, utils_js_1.getDetachOutputCellId)(baseMark);
        if ((0, moveEffectTable_js_1.isMoveOut)(baseMark)) {
            (0, internal_1.assert)((0, moveEffectTable_js_1.isMoveMark)(baseMark), 0x6f0 /* Only move marks have move IDs */);
            (0, internal_1.assert)(!(0, utils_js_1.isNewAttach)(currMark), 0x819 /* New attaches should not be rebased over moves */);
            const { remains, follows } = separateEffectsForMove((0, utils_js_1.extractMarkEffect)(currMark));
            if (follows !== undefined) {
                sendEffectToDest(follows, moveEffects, (0, utils_js_1.getEndpoint)(baseMark), baseMark.count);
            }
            if (currMark.changes !== undefined) {
                moveRebasedChanges(currMark.changes, moveEffects, (0, utils_js_1.getEndpoint)(baseMark));
            }
            rebasedMark = { ...(remains ?? {}), count: baseMark.count };
        }
        else {
            rebasedMark = currMark;
        }
        rebasedMark = makeDetachedMark(rebasedMark, (0, utils_js_1.cloneCellId)(baseCellId));
    }
    else if ((0, utils_js_1.markFillsCells)(baseMark)) {
        rebasedMark = (0, utils_js_1.isAttachAndDetachEffect)(currMark)
            ? (0, utils_js_1.withNodeChange)({ ...currMark.detach, count: currMark.count }, currMark.changes)
            : withCellId(currMark, undefined);
    }
    else if ((0, utils_js_1.isAttachAndDetachEffect)(baseMark)) {
        (0, internal_1.assert)(baseMark.cellId !== undefined, 0x81a /* AttachAndDetach mark should target an empty cell */);
        if ((0, moveEffectTable_js_1.isMoveIn)(baseMark.attach) && (0, moveEffectTable_js_1.isMoveOut)(baseMark.detach)) {
            // Orphaned moves are effectively cell renames.
            return withCellId(currMark, (0, utils_js_1.getDetachOutputCellId)(baseMark.detach));
        }
        const halfRebasedMark = rebaseMarkIgnoreChild(currMark, { ...baseMark.attach, cellId: (0, utils_js_1.cloneCellId)(baseMark.cellId), count: baseMark.count }, moveEffects);
        rebasedMark = rebaseMarkIgnoreChild(halfRebasedMark, { ...baseMark.detach, count: baseMark.count }, moveEffects);
    }
    else {
        rebasedMark = currMark;
    }
    return rebasedMark;
}
/**
 * @returns A pair of marks that represent the effects which should remain in place in the face of concurrent move,
 * and the effects that should be sent to the move destination.
 */
function separateEffectsForMove(mark) {
    const type = mark.type;
    switch (type) {
        case "Remove":
        case "MoveOut":
            return { follows: mark };
        case "AttachAndDetach":
            return { follows: mark.detach, remains: mark.attach };
        case "MoveIn":
            return { remains: mark };
        case types_js_1.NoopMarkType:
            return {};
        case "Insert": {
            const follows = {
                type: "MoveOut",
                id: mark.id,
            };
            const remains = {
                type: "MoveIn",
                id: mark.id,
            };
            if (mark.revision !== undefined) {
                follows.revision = mark.revision;
                remains.revision = mark.revision;
            }
            return { remains, follows };
        }
        default:
            (0, internal_1.unreachableCase)(type);
    }
}
// TODO: Reduce the duplication between this and other MoveEffect helpers
function sendEffectToDest(markEffect, moveEffects, { revision, localId: id }, count) {
    const effect = (0, moveEffectTable_js_1.getMoveEffect)(moveEffects, index_js_2.CrossFieldTarget.Destination, revision, id, count, false);
    if (effect.length < count) {
        const [markEffect1, markEffect2] = (0, utils_js_1.splitMarkEffect)(markEffect, effect.length);
        const newEffect = effect.value !== undefined
            ? { ...effect.value, movedMark: markEffect1 }
            : { movedMark: markEffect1 };
        (0, moveEffectTable_js_1.setMoveEffect)(moveEffects, index_js_2.CrossFieldTarget.Destination, revision, id, effect.length, newEffect);
        sendEffectToDest(markEffect2, moveEffects, { revision, localId: (0, index_js_1.brand)(id + effect.length) }, count - effect.length);
    }
    else {
        const newEffect = effect.value !== undefined
            ? { ...effect.value, movedEffect: markEffect }
            : { movedEffect: markEffect };
        (0, moveEffectTable_js_1.setMoveEffect)(moveEffects, index_js_2.CrossFieldTarget.Destination, revision, id, count, newEffect);
    }
}
function moveRebasedChanges(nodeChange, moveEffects, { revision, localId: id }) {
    const effect = (0, moveEffectTable_js_1.getMoveEffect)(moveEffects, index_js_2.CrossFieldTarget.Destination, revision, id, 1, false).value;
    const newEffect = effect !== undefined
        ? { ...effect, rebasedChanges: nodeChange }
        : { rebasedChanges: nodeChange };
    (0, moveEffectTable_js_1.setMoveEffect)(moveEffects, index_js_2.CrossFieldTarget.Destination, revision, id, 1, newEffect);
}
function rebaseNodeChange(currMark, baseMark, nodeRebaser) {
    const baseChange = baseMark.changes;
    const currChange = currMark.changes;
    if (baseChange === undefined && currChange === undefined) {
        return currMark;
    }
    const nodeState = nodeStateAfterMark(baseMark);
    return (0, utils_js_1.withNodeChange)(currMark, nodeRebaser(currChange, baseChange, nodeState));
}
function nodeStateAfterMark(mark) {
    if ((0, utils_js_1.markEmptiesCells)(mark)) {
        return index_js_2.NodeAttachState.Detached;
    }
    else if ((0, utils_js_1.markFillsCells)(mark)) {
        return index_js_2.NodeAttachState.Attached;
    }
    else {
        return mark.cellId === undefined ? index_js_2.NodeAttachState.Attached : index_js_2.NodeAttachState.Detached;
    }
}
function makeDetachedMark(mark, cellId) {
    (0, internal_1.assert)(mark.cellId === undefined, 0x69f /* Expected mark to be attached */);
    return { ...mark, cellId };
}
function withCellId(mark, cellId) {
    const newMark = { ...mark, cellId };
    if (cellId === undefined) {
        delete newMark.cellId;
    }
    return newMark;
}
function getMovedEffectFromBaseMark(moveEffects, baseMark) {
    if ((0, moveEffectTable_js_1.isMoveIn)(baseMark)) {
        return getMovedEffect(moveEffects, baseMark.revision, baseMark.id, baseMark.count);
    }
    else if ((0, utils_js_1.isAttachAndDetachEffect)(baseMark) && (0, moveEffectTable_js_1.isMoveIn)(baseMark.attach)) {
        return getMovedEffect(moveEffects, baseMark.attach.revision, baseMark.attach.id, baseMark.count);
    }
    else {
        return undefined;
    }
}
// It is expected that the range from `id` to `id + count - 1` has the same move effect.
// The call sites to this function are making queries about a mark which has already been split by a `MarkQueue`
// to match the ranges in `moveEffects`.
// TODO: Reduce the duplication between this and other MoveEffect helpers
function getMovedEffect(moveEffects, revision, id, count) {
    const effect = (0, moveEffectTable_js_1.getMoveEffect)(moveEffects, index_js_2.CrossFieldTarget.Destination, revision, id, count);
    (0, internal_1.assert)(effect.length === count, 0x6f3 /* Expected effect to cover entire mark */);
    return effect.value?.movedEffect;
}
function getMovedChangesFromBaseMark(moveEffects, baseMark) {
    if ((0, moveEffectTable_js_1.isMoveIn)(baseMark)) {
        return getMovedNodeChanges(moveEffects, baseMark.revision, baseMark.id);
    }
    else if ((0, utils_js_1.isAttachAndDetachEffect)(baseMark) && (0, moveEffectTable_js_1.isMoveIn)(baseMark.attach)) {
        return getMovedNodeChanges(moveEffects, baseMark.attach.revision, baseMark.attach.id);
    }
    else {
        return undefined;
    }
}
function getMovedNodeChanges(moveEffects, revision, id) {
    return (0, moveEffectTable_js_1.getMoveEffect)(moveEffects, index_js_2.CrossFieldTarget.Destination, revision, id, 1).value
        ?.rebasedChanges;
}
//# sourceMappingURL=rebase.js.map