/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { assert, unreachableCase } from "@fluidframework/core-utils/internal";
import { areEqualChangeAtomIds, } from "../../core/index.js";
import { NodeAttachState, } from "../modular-schema/index.js";
import { MarkListFactory } from "./markListFactory.js";
import { MarkQueue } from "./markQueue.js";
import { NoopMarkType, } from "./types.js";
import { CellOrder, areInputCellsEmpty, cellSourcesFromMarks, cloneCellId, cloneMark, compareCellPositionsUsingTombstones, extractMarkEffect, getAttachedRootId, getDetachOutputCellId, getDetachedRootId, getInputCellId, getOutputCellId, isAttach, isDetach, isNewAttach, isRename, isTombstone, markEmptiesCells, markFillsCells, withNodeChange, } from "./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.
 */
export function rebase(change, base, rebaseChild, genId, manager, revisionMetadata) {
    return rebaseMarkList(change, base, revisionMetadata, rebaseChild, genId, manager);
}
function rebaseMarkList(currMarkList, baseMarkList, metadata, rebaseChild, genId, moveEffects) {
    const factory = new 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 = 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 (isAttach(mark)) {
                return moveEffects.getNewChangesForBaseAttach(getAttachedRootId(mark), mark.count)
                    .length;
            }
            else if (isDetach(mark)) {
                return moveEffects.doesBaseAttachNodes(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(baseMarks, queryFunc);
        this.newMarks = new MarkQueue(newMarks, (mark) => mark.count);
        this.baseMarksCellSources = cellSourcesFromMarks(baseMarks, getInputCellId);
        this.newMarksCellSources = cellSourcesFromMarks(newMarks, getInputCellId);
    }
    isEmpty() {
        return this.baseMarks.isEmpty() && this.newMarks.isEmpty();
    }
    pop() {
        const baseMark = this.baseMarks.peek();
        const newMark = this.newMarks.peek();
        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 (areInputCellsEmpty(baseMark) && areInputCellsEmpty(newMark)) {
            const baseId = getInputCellId(baseMark);
            const newId = getInputCellId(newMark);
            assert(baseId !== undefined && newId !== undefined, 0x89f /* Both marks should have cell IDs */);
            const comparison = compareCellPositionsUsingTombstones(baseId, newId, this.baseMarksCellSources, this.newMarksCellSources, this.metadata);
            switch (comparison) {
                case CellOrder.SameCell:
                    return this.dequeueBoth();
                case CellOrder.OldThenNew:
                    return this.dequeueBase();
                case CellOrder.NewThenOld:
                    return this.dequeueNew();
                default:
                    unreachableCase(comparison);
            }
        }
        else if (areInputCellsEmpty(newMark)) {
            return this.dequeueNew();
        }
        else if (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();
        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 (isAttach(mark) && isDetach(effect)) {
        return { ...mark, type: "Insert" };
    }
    else if (isRename(mark) && isDetach(effect)) {
        const result = { ...effect, count: mark.count };
        if (!areEqualChangeAtomIds(mark.idOverride, getDetachedRootId(effect))) {
            result.detachCellId = mark.idOverride;
        }
        return result;
    }
    else if (isTombstone(mark)) {
        return { ...mark, ...effect };
    }
    assert(false, 0x818 /* Unexpected combination of mark effects at source and destination */);
}
function rebaseMark(currMark, baseMark, rebaseChild, moveEffects) {
    const rebasedMark = rebaseNodeChange(cloneMark(currMark), baseMark, rebaseChild);
    const movedNodeChanges = getMovedChangesFromBaseMark(moveEffects, baseMark);
    if (movedNodeChanges !== undefined) {
        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 (isDetach(baseMark)) {
        if (baseMark.cellId !== undefined) {
            // Detaches on empty cells have an implicit revive effect.
            delete currMark.cellId;
        }
        assert(!isNewAttach(currMark), 0x69d /* A new attach should not be rebased over its cell being emptied */);
        const baseCellId = getDetachOutputCellId(baseMark);
        const baseDetachId = getDetachedRootId(baseMark);
        const { remains, follows } = separateEffectsForMove(extractMarkEffect(currMark), baseMark, moveEffects);
        moveRebasedChanges(moveEffects, baseDetachId, baseMark.count, currMark.changes, follows);
        rebasedMark = { ...(remains ?? {}), count: baseMark.count };
        return makeDetachedMark(rebasedMark, cloneCellId(baseCellId));
    }
    else if (markFillsCells(baseMark)) {
        return withCellId(currMark, undefined);
    }
    else if (isRename(baseMark)) {
        if (currMark.type === "Rename") {
            const doesBaseMoveAndAttach = moveEffects.doesBaseAttachNodes(baseMark.idOverride, baseMark.count).value;
            assert(baseMark.cellId !== undefined, "Rename should target empty cells");
            const baseRootRename = moveEffects.getBaseRename(baseMark.cellId, baseMark.count).value;
            const doesBaseMoveAndDetach = baseRootRename !== undefined &&
                !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, 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 NoopMarkType:
            return {};
        case "Remove": {
            if (mark.cellRename !== undefined) {
                return { remains: { type: "Rename", idOverride: mark.cellRename }, follows: mark };
            }
            const baseDetachId = getDetachedRootId(baseMark);
            const outputCellId = getDetachOutputCellId(baseMark);
            const doesBaseMoveNodes = !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: 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:
            unreachableCase(type);
    }
}
function moveRebasedChanges(moveEffects, baseId, count, nodeChange, newDetach) {
    const newId = newDetach !== undefined ? 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 withNodeChange(currMark, nodeRebaser(currChange, baseChange, nodeState));
}
function nodeStateAfterMark(mark) {
    if (markEmptiesCells(mark)) {
        return NodeAttachState.Detached;
    }
    else if (markFillsCells(mark)) {
        return NodeAttachState.Attached;
    }
    else {
        return mark.cellId === undefined ? NodeAttachState.Attached : NodeAttachState.Detached;
    }
}
function makeDetachedMark(mark, cellId) {
    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 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);
    assert(entry.length === count, 0x6f3 /* Expected effect to cover entire mark */);
    const detachId = entry.value?.detachId;
    if (detachId === undefined) {
        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 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