"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("../../core/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 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;
        const queryFunc = (mark) => {
            if ((0, utils_js_1.isAttach)(mark)) {
                return moveEffects.getNewChangesForBaseAttach((0, utils_js_1.getAttachedRootId)(mark), mark.count)
                    .length;
            }
            else if ((0, utils_js_1.isDetach)(mark)) {
                return moveEffects.doesBaseAttachNodes((0, utils_js_1.getDetachedRootId)(mark), mark.count).length;
            }
            let count = mark.count;
            if (mark.type === "Rename") {
                count = moveEffects.getNewRenameForBaseRename(mark.idOverride, count).length;
            }
            return mark.cellId !== undefined
                ? moveEffects.doesBaseAttachNodes(mark.cellId, count).length
                : count;
        };
        this.baseMarks = new markQueue_js_1.MarkQueue(baseMarks, queryFunc);
        this.newMarks = new markQueue_js_1.MarkQueue(newMarks, (mark) => mark.count);
        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, utils_js_1.isAttach)(mark) && (0, utils_js_1.isDetach)(effect)) {
        return { ...mark, type: "Insert" };
    }
    else if ((0, utils_js_1.isRename)(mark) && (0, utils_js_1.isDetach)(effect)) {
        const result = { ...effect, count: mark.count };
        if (!(0, index_js_1.areEqualChangeAtomIds)(mark.idOverride, (0, utils_js_1.getDetachedRootId)(effect))) {
            result.detachCellId = mark.idOverride;
        }
        return result;
    }
    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);
        const baseDetachId = (0, utils_js_1.getDetachedRootId)(baseMark);
        const { remains, follows } = separateEffectsForMove((0, utils_js_1.extractMarkEffect)(currMark), baseMark, moveEffects);
        moveRebasedChanges(moveEffects, baseDetachId, baseMark.count, currMark.changes, follows);
        rebasedMark = { ...(remains ?? {}), count: baseMark.count };
        return makeDetachedMark(rebasedMark, (0, utils_js_1.cloneCellId)(baseCellId));
    }
    else if ((0, utils_js_1.markFillsCells)(baseMark)) {
        return withCellId(currMark, undefined);
    }
    else if ((0, utils_js_1.isRename)(baseMark)) {
        if (currMark.type === "Rename") {
            const doesBaseMoveAndAttach = moveEffects.doesBaseAttachNodes(baseMark.idOverride, baseMark.count).value;
            (0, internal_1.assert)(baseMark.cellId !== undefined, "Rename should target empty cells");
            const baseRootRename = moveEffects.getBaseRename(baseMark.cellId, baseMark.count).value;
            const doesBaseMoveAndDetach = baseRootRename !== undefined &&
                !(0, index_js_1.areEqualChangeAtomIds)(baseRootRename, baseMark.idOverride);
            if (doesBaseMoveAndAttach || doesBaseMoveAndDetach) {
                // `newMark` represents a node-targeting rename.
                // The base changeset has moved the node which was last detached from the cell,
                // so we should remove the rename from this location.
                return { cellId: baseMark.idOverride, count: baseMark.count };
            }
        }
        const newRenameId = moveEffects.getNewRenameForBaseRename(baseMark.idOverride, baseMark.count).value;
        if (newRenameId !== undefined && currMark.type !== "Insert") {
            return {
                type: "Rename",
                cellId: baseMark.idOverride,
                count: baseMark.count,
                idOverride: newRenameId,
            };
        }
        return withCellId(currMark, (0, utils_js_1.getOutputCellId)(baseMark));
    }
    else {
        return currMark;
    }
}
/**
 * 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, baseMark, nodeManager) {
    const type = mark.type;
    switch (type) {
        case types_js_1.NoopMarkType:
            return {};
        case "Remove": {
            if (mark.cellRename !== undefined) {
                return { remains: { type: "Rename", idOverride: mark.cellRename }, follows: mark };
            }
            const baseDetachId = (0, utils_js_1.getDetachedRootId)(baseMark);
            const outputCellId = (0, utils_js_1.getDetachOutputCellId)(baseMark);
            const doesBaseMoveNodes = !(0, index_js_1.areEqualChangeAtomIds)(outputCellId, baseDetachId) ||
                nodeManager.doesBaseAttachNodes(baseDetachId, baseMark.count).value;
            // We should rename these cells unless the nodes are reattached elsewhere,
            // in which case we will rename those cells instead.
            const remains = doesBaseMoveNodes
                ? {}
                : {
                    type: "Rename",
                    idOverride: (0, utils_js_1.getDetachOutputCellId)(mark),
                };
            return { remains, follows: mark };
        }
        case "Rename":
            return { remains: mark };
        case "Insert": {
            const follows = {
                type: "Remove",
                id: mark.id,
            };
            if (mark.detachCellId !== undefined) {
                follows.detachCellId = mark.detachCellId;
            }
            const remains = {
                type: "Insert",
                id: mark.id,
            };
            if (mark.revision !== undefined) {
                follows.revision = mark.revision;
                remains.revision = mark.revision;
            }
            return { remains, follows };
        }
        default:
            (0, internal_1.unreachableCase)(type);
    }
}
function moveRebasedChanges(moveEffects, baseId, count, nodeChange, newDetach) {
    const newId = newDetach !== undefined ? (0, utils_js_1.getDetachedRootId)(newDetach) : undefined;
    const detachCellId = newDetach?.detachCellId;
    moveEffects.rebaseOverDetach(baseId, count, newId, nodeChange, detachCellId);
}
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) {
    return (0, utils_js_1.isAttach)(baseMark)
        ? getMovedEffect(moveEffects, baseMark.revision, baseMark.id, baseMark.count)
        : 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, localId, count) {
    const entry = moveEffects.getNewChangesForBaseAttach({ revision, localId }, count);
    (0, internal_1.assert)(entry.length === count, 0x6f3 /* Expected effect to cover entire mark */);
    const detachId = entry.value?.detachId;
    if (detachId === undefined) {
        (0, internal_1.assert)(entry.value?.cellRename === undefined, "Cell detach should be accompanied by node detach");
        return undefined;
    }
    const detach = {
        type: "Remove",
        revision: detachId.revision,
        id: detachId.localId,
    };
    if (entry.value?.cellRename !== undefined) {
        detach.detachCellId = entry.value.cellRename;
    }
    return detach;
}
function getMovedChangesFromBaseMark(moveEffects, baseMark) {
    return (0, utils_js_1.isAttach)(baseMark)
        ? getMovedNodeChanges(moveEffects, baseMark.revision, baseMark.id)
        : undefined;
}
function getMovedNodeChanges(moveEffects, revision, localId) {
    return moveEffects.getNewChangesForBaseAttach({ revision, localId }, 1).value?.nodeChange;
}
//# sourceMappingURL=rebase.js.map