"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.amendRebase = exports.rebase = void 0;
const common_utils_1 = require("@fluidframework/common-utils");
const util_1 = require("../../util");
const modular_schema_1 = require("../modular-schema");
const utils_1 = require("./utils");
const format_1 = require("./format");
const markListFactory_1 = require("./markListFactory");
const compose_1 = require("./compose");
const moveEffectTable_1 = require("./moveEffectTable");
const markQueue_1 = require("./markQueue");
/**
 * 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.
 *
 * WARNING! This implementation is incomplete:
 * - Some marks that affect existing content are removed instead of marked as conflicted when rebased over the deletion
 * of that content. This prevents us from then reinstating the mark when rebasing over the revive.
 * - Tombs are not added when rebasing an insert over a gap that is immediately left of deleted content.
 * This prevents us from being able to accurately track the position of the insert.
 * - Tiebreak ordering is not respected.
 * - Support for moves is not implemented.
 * - Support for slices is not implemented.
 */
function rebase(change, base, rebaseChild, genId, manager, revisionMetadata) {
    var _a;
    (0, common_utils_1.assert)(base.revision !== undefined, 0x69b /* Cannot rebase over changeset with no revision */);
    const baseInfo = base.revision === undefined ? undefined : revisionMetadata.getInfo(base.revision);
    const baseIntention = (_a = baseInfo === null || baseInfo === void 0 ? void 0 : baseInfo.rollbackOf) !== null && _a !== void 0 ? _a : base.revision;
    return rebaseMarkList(change, base.change, base.revision, baseIntention, rebaseChild, genId, manager);
}
exports.rebase = rebase;
function rebaseMarkList(currMarkList, baseMarkList, baseRevision, baseIntention, rebaseChild, genId, moveEffects) {
    var _a;
    const factory = new markListFactory_1.MarkListFactory(undefined, moveEffects, true);
    const queue = new RebaseQueue(baseRevision, baseIntention, baseMarkList, currMarkList, genId, moveEffects);
    // Each attach mark in `currMarkList` should have a lineage event added for `baseRevision` if a node adjacent to
    // the attach position was detached by `baseMarkList`.
    // At the time we process an attach we don't know whether the following node will be detached, so we record attach
    // marks which should have their lineage updated if we encounter a detach.
    const lineageRequests = [];
    let baseDetachOffset = 0;
    // The index of (i.e., number of nodes to the left of) the base mark in the input context of the base change.
    // This assumes the base changeset is not composite (and asserts if it is).
    let baseInputIndex = 0;
    while (!queue.isEmpty()) {
        const { baseMark, newMark: currMark } = queue.pop();
        if (baseMark !== undefined && "revision" in baseMark) {
            // TODO support rebasing over composite changeset
            (0, common_utils_1.assert)(baseMark.revision === baseRevision, 0x4f3 /* Unable to keep track of the base input offset in composite changeset */);
        }
        if (baseMark === undefined) {
            (0, common_utils_1.assert)(currMark !== undefined, 0x4f4 /* Non-empty queue should return at least one mark */);
            if ((0, utils_1.isAttach)(currMark)) {
                handleCurrAttach(currMark, factory, lineageRequests, baseDetachOffset, baseIntention);
            }
            else {
                if (baseDetachOffset > 0 && baseIntention !== undefined) {
                    updateLineage(lineageRequests, baseIntention);
                    baseDetachOffset = 0;
                }
                factory.push((0, utils_1.cloneMark)(currMark));
            }
        }
        else if (currMark === undefined) {
            // TODO: Do we need to handle rebasing over baseMark's changes in this case?
            if ((0, utils_1.isDetachMark)(baseMark)) {
                const detachLength = (0, utils_1.getInputLength)(baseMark);
                baseDetachOffset += detachLength;
                baseInputIndex += detachLength;
            }
            else if ((0, utils_1.isAttach)(baseMark)) {
                if (baseMark.type === "MoveIn" || baseMark.type === "ReturnTo") {
                    const effect = (0, moveEffectTable_1.getMoveEffect)(moveEffects, modular_schema_1.CrossFieldTarget.Destination, (_a = baseMark.revision) !== null && _a !== void 0 ? _a : baseRevision, baseMark.id);
                    if (effect.movedMark !== undefined) {
                        factory.push(effect.movedMark);
                        delete effect.movedMark;
                    }
                    else {
                        factory.pushOffset((0, utils_1.getOutputLength)(baseMark));
                    }
                }
                else {
                    factory.pushOffset((0, utils_1.getOutputLength)(baseMark));
                }
            }
        }
        else {
            (0, common_utils_1.assert)(!(0, utils_1.isNewAttach)(baseMark) && !(0, utils_1.isNewAttach)(currMark), 0x4f5 /* A new attach cannot be at the same position as another mark */);
            (0, common_utils_1.assert)((0, utils_1.getInputLength)(baseMark) === (0, utils_1.getInputLength)(currMark), 0x4f6 /* The two marks should be the same size */);
            const rebasedMark = rebaseMark(currMark, baseMark, baseRevision, baseIntention, baseInputIndex, rebaseChild, moveEffects);
            factory.push(rebasedMark);
            const detachLength = (0, utils_1.getInputLength)(baseMark);
            baseInputIndex += detachLength;
            if ((0, utils_1.isDetachMark)(baseMark)) {
                baseDetachOffset += detachLength;
            }
            else {
                if (baseDetachOffset > 0 && baseIntention !== undefined) {
                    updateLineage(lineageRequests, baseIntention);
                }
                lineageRequests.length = 0;
                baseDetachOffset = 0;
            }
        }
    }
    if (baseDetachOffset > 0 && baseIntention !== undefined) {
        updateLineage(lineageRequests, baseIntention);
    }
    return factory.list;
}
class RebaseQueue {
    constructor(baseRevision, baseIntention, baseMarks, newMarks, genId, moveEffects) {
        this.baseIntention = baseIntention;
        this.reattachOffset = 0;
        this.baseMarks = new markQueue_1.MarkQueue(baseMarks, baseRevision, moveEffects, false, genId);
        this.newMarks = new markQueue_1.MarkQueue(newMarks, undefined, moveEffects, true, genId);
    }
    isEmpty() {
        return this.baseMarks.isEmpty() && this.newMarks.isEmpty();
    }
    pop() {
        const baseMark = this.baseMarks.peek();
        const newMark = this.newMarks.peek();
        if (baseMark === undefined && newMark === undefined) {
            return {};
        }
        else if (baseMark === undefined) {
            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
            const length = (0, utils_1.getInputLength)(newMark);
            return {
                baseMark: length > 0 ? { count: length } : undefined,
                newMark: this.newMarks.tryDequeue(),
            };
        }
        else if (newMark === undefined) {
            const length = (0, utils_1.getInputLength)(baseMark);
            if ((0, utils_1.isAttach)(baseMark)) {
                this.reattachOffset += (0, utils_1.getOutputLength)(baseMark);
            }
            return {
                baseMark: this.baseMarks.tryDequeue(),
                newMark: length > 0 ? { count: length } : undefined,
            };
        }
        else if ((0, utils_1.areInputCellsEmpty)(baseMark) && (0, utils_1.areInputCellsEmpty)(newMark)) {
            const cmp = compareCellPositions(this.baseIntention, baseMark, newMark, this.reattachOffset);
            if (cmp < 0) {
                this.reattachOffset += Math.min((0, utils_1.getOutputLength)(baseMark), -cmp);
                return { baseMark: this.baseMarks.dequeueUpTo(-cmp) };
            }
            else if (cmp > 0) {
                return { newMark: this.newMarks.dequeueUpTo(cmp) };
            }
            else {
                const length = Math.min((0, utils_1.getMarkLength)(baseMark), (0, utils_1.getMarkLength)(newMark));
                if ((0, utils_1.markFillsCells)(baseMark)) {
                    this.reattachOffset += length;
                }
                return this.dequeueBoth();
            }
        }
        else if ((0, utils_1.areInputCellsEmpty)(newMark)) {
            return this.dequeueNew();
        }
        else if ((0, utils_1.areInputCellsEmpty)(baseMark)) {
            this.reattachOffset += (0, utils_1.getOutputLength)(baseMark);
            return this.dequeueBase();
        }
        else {
            this.reattachOffset = 0;
            return this.dequeueBoth();
        }
    }
    dequeueBase() {
        return { baseMark: this.baseMarks.dequeue() };
    }
    dequeueNew() {
        return { newMark: this.newMarks.dequeue() };
    }
    dequeueBoth() {
        const baseMark = this.baseMarks.peek();
        const newMark = this.newMarks.peek();
        (0, common_utils_1.assert)(baseMark !== undefined && newMark !== undefined, 0x69c /* Cannot dequeue both unless both mark queues are non-empty */);
        const length = Math.min((0, utils_1.getMarkLength)(newMark), (0, utils_1.getMarkLength)(baseMark));
        return {
            baseMark: this.baseMarks.dequeueUpTo(length),
            newMark: this.newMarks.dequeueUpTo(length),
        };
    }
}
function rebaseMark(currMark, baseMark, baseRevision, baseIntention, baseInputOffset, rebaseChild, moveEffects) {
    let rebasedMark = rebaseNodeChange((0, utils_1.cloneMark)(currMark), baseMark, rebaseChild);
    const baseMarkIntention = getMarkIntention(baseMark, baseIntention);
    if ((0, utils_1.markEmptiesCells)(baseMark)) {
        const moveId = (0, utils_1.getMarkMoveId)(baseMark);
        if (moveId !== undefined) {
            if (markFollowsMoves(rebasedMark)) {
                sendMarkToDest(rebasedMark, moveEffects, baseRevision, moveId);
                return { count: 0 };
            }
            const nodeChange = (0, utils_1.getNodeChange)(rebasedMark);
            if (nodeChange !== undefined) {
                rebasedMark = (0, utils_1.withNodeChange)(rebasedMark, undefined);
                const modify = { type: "Modify", changes: nodeChange };
                sendMarkToDest(modify, moveEffects, baseRevision, moveId);
            }
        }
        (0, common_utils_1.assert)(!(0, utils_1.isNewAttach)(rebasedMark), 0x69d /* A new attach should not be rebased over its cell being emptied */);
        if ((0, moveEffectTable_1.isMoveMark)(rebasedMark)) {
            if (rebasedMark.type === "MoveOut" || rebasedMark.type === "ReturnFrom") {
                (0, moveEffectTable_1.getOrAddEffect)(moveEffects, modular_schema_1.CrossFieldTarget.Destination, rebasedMark.revision, rebasedMark.id).pairedMarkStatus = moveEffectTable_1.PairedMarkUpdate.Deactivated;
            }
            else if (rebasedMark.type === "ReturnTo") {
                (0, moveEffectTable_1.getOrAddEffect)(moveEffects, modular_schema_1.CrossFieldTarget.Source, rebasedMark.revision, rebasedMark.id).pairedMarkStatus = moveEffectTable_1.PairedMarkUpdate.Reactivated;
            }
        }
        rebasedMark = makeDetachedMark(rebasedMark, baseMarkIntention, baseInputOffset);
    }
    else if ((0, utils_1.markFillsCells)(baseMark)) {
        (0, common_utils_1.assert)((0, utils_1.isExistingCellMark)(rebasedMark), 0x69e /* Only an ExistingCellMark can target an empty cell */);
        if ((0, moveEffectTable_1.isMoveMark)(rebasedMark)) {
            if (rebasedMark.type === "MoveOut" || rebasedMark.type === "ReturnFrom") {
                (0, moveEffectTable_1.getOrAddEffect)(moveEffects, modular_schema_1.CrossFieldTarget.Destination, rebasedMark.revision, rebasedMark.id).pairedMarkStatus = moveEffectTable_1.PairedMarkUpdate.Reactivated;
            }
            else if (rebasedMark.type === "ReturnTo") {
                (0, moveEffectTable_1.getOrAddEffect)(moveEffects, modular_schema_1.CrossFieldTarget.Source, rebasedMark.revision, rebasedMark.id).pairedMarkStatus = moveEffectTable_1.PairedMarkUpdate.Deactivated;
            }
        }
        rebasedMark = withoutDetachEvent(rebasedMark);
    }
    return rebasedMark;
}
function markFollowsMoves(mark) {
    const type = mark.type;
    switch (type) {
        case "Delete":
        case "Modify":
        case "MoveOut":
        case "Revive":
            return true;
        case format_1.NoopMarkType:
        case "ReturnFrom":
        case "Insert":
        case "MoveIn":
        case "ReturnTo":
            return false;
        default:
            (0, common_utils_1.unreachableCase)(type);
    }
}
function sendMarkToDest(mark, moveEffects, revision, moveId) {
    (0, moveEffectTable_1.getOrAddEffect)(moveEffects, modular_schema_1.CrossFieldTarget.Destination, revision, moveId).movedMark = mark;
}
function getMarkIntention(mark, intention) {
    // TODO: Use mark's revision tag when available.
    // Currently we never rebase over a composition of multiple revisions, so this isn't necessary.
    return intention;
}
function rebaseNodeChange(currMark, baseMark, nodeRebaser) {
    const baseChange = (0, utils_1.getNodeChange)(baseMark);
    const currChange = (0, utils_1.getNodeChange)(currMark);
    return (0, utils_1.withNodeChange)(currMark, nodeRebaser(currChange, baseChange));
}
function makeDetachedMark(mark, detachIntention, offset) {
    if ((0, utils_1.isNoopMark)(mark)) {
        return { count: 0 };
    }
    (0, common_utils_1.assert)(mark.detachEvent === undefined, 0x69f /* Expected mark to be attached */);
    return Object.assign(Object.assign({}, mark), { detachEvent: { revision: detachIntention, index: offset } });
}
function withoutDetachEvent(mark) {
    const newMark = Object.assign({}, mark);
    delete newMark.detachEvent;
    delete newMark.lineage;
    return newMark;
}
function amendRebase(rebasedMarks, baseMarks, rebaseChild, genId, crossFieldManager, revisionMetadata) {
    return amendRebaseI(baseMarks.revision, baseMarks.change, rebasedMarks, rebaseChild, crossFieldManager, revisionMetadata);
}
exports.amendRebase = amendRebase;
function amendRebaseI(baseRevision, baseMarks, rebasedMarks, rebaseChild, moveEffects, revisionMetadata) {
    var _a;
    // Is it correct to use ComposeQueue here?
    // If we used a special AmendRebaseQueue, we could ignore any base marks which don't have associated move-ins
    const queue = new compose_1.ComposeQueue(baseRevision, baseMarks, undefined, rebasedMarks, () => (0, util_1.fail)("Should not generate new IDs when applying move effects"), moveEffects, revisionMetadata);
    const factory = new markListFactory_1.MarkListFactory(undefined, moveEffects);
    while (!queue.isEmpty()) {
        const { baseMark, newMark } = queue.pop();
        if (baseMark !== undefined &&
            (baseMark.type === "MoveIn" || baseMark.type === "ReturnTo")) {
            const effect = (0, moveEffectTable_1.getMoveEffect)(moveEffects, modular_schema_1.CrossFieldTarget.Destination, (_a = baseMark.revision) !== null && _a !== void 0 ? _a : baseRevision, baseMark.id);
            if (effect.movedMark !== undefined) {
                factory.push(effect.movedMark);
                factory.pushOffset(-(0, utils_1.getInputLength)(effect.movedMark));
                delete effect.movedMark;
            }
        }
        if (newMark !== undefined) {
            let rebasedMark = newMark;
            // TODO: Handle all pairings of base and new mark types.
            if (baseMark !== undefined && (0, utils_1.isModify)(baseMark)) {
                switch (newMark.type) {
                    case format_1.NoopMarkType: {
                        const childChange = rebaseChild(undefined, baseMark.changes);
                        if (childChange !== undefined) {
                            rebasedMark = { type: "Modify", changes: childChange };
                        }
                        break;
                    }
                    case "Modify": {
                        const childChange = rebaseChild(newMark.changes, baseMark.changes);
                        if (childChange === undefined) {
                            rebasedMark = { count: 1 };
                        }
                        else {
                            newMark.changes = childChange;
                        }
                        break;
                    }
                    default:
                        break;
                }
            }
            factory.push(rebasedMark);
        }
    }
    // We may have discovered new mergeable marks while applying move effects, as we may have moved a MoveOut next to another MoveOut.
    // A second pass through MarkListFactory will handle any remaining merges.
    const factory2 = new markListFactory_1.MarkListFactory(undefined, moveEffects);
    for (const mark of factory.list) {
        factory2.push(mark);
    }
    return factory2.list;
}
function handleCurrAttach(currMark, factory, lineageRequests, offset, baseIntention) {
    const rebasedMark = (0, utils_1.cloneMark)(currMark);
    // If the changeset we are rebasing over has the same intention as an event in rebasedMark's lineage,
    // we assume that the base changeset is the inverse of the changeset in the lineage, so we remove the lineage event.
    // TODO: Handle cases where the base changeset is a composition of multiple revisions.
    // TODO: Don't remove the lineage event in cases where the event isn't actually inverted by the base changeset,
    // e.g., if the inverse of the lineage event is muted after rebasing.
    if (baseIntention !== undefined) {
        tryRemoveLineageEvent(rebasedMark, baseIntention);
    }
    factory.pushContent(rebasedMark);
    lineageRequests.push({ mark: rebasedMark, offset });
}
function updateLineage(requests, revision) {
    for (const request of requests) {
        const mark = request.mark;
        if (mark.lineage === undefined) {
            mark.lineage = [];
        }
        mark.lineage.push({ revision, offset: request.offset });
    }
}
function tryRemoveLineageEvent(mark, revisionToRemove) {
    if (mark.lineage === undefined) {
        return;
    }
    const index = mark.lineage.findIndex((event) => event.revision === revisionToRemove);
    if (index >= 0) {
        mark.lineage.splice(index, 1);
        if (mark.lineage.length === 0) {
            delete mark.lineage;
        }
    }
}
/**
 * Returns a number N which encodes how the cells of the two marks are aligned.
 * - If N is zero, then the first cell of `baseMark` is the same as the first cell of `newMark`.
 * - If N is positive, then the first N cells of `newMark` (or all its cells if N is greater than its length)
 * are before the first cell of `baseMark`.
 * - If N is negative, then the first N cells of `baseMark` (or all its cells if N is greater than its length)
 * are before the first cell of `newMark`.
 */
function compareCellPositions(baseIntention, baseMark, newMark, gapOffsetInBase) {
    const baseId = (0, utils_1.getCellId)(baseMark, baseIntention);
    (0, common_utils_1.assert)(baseId !== undefined, 0x6a0 /* baseMark should have cell ID */);
    const newId = (0, utils_1.getCellId)(newMark, undefined);
    if (baseId.revision === (newId === null || newId === void 0 ? void 0 : newId.revision)) {
        return baseId.index - newId.index;
    }
    if (newId !== undefined) {
        const baseOffset = (0, utils_1.getOffsetAtRevision)(baseMark.lineage, newId.revision);
        if (baseOffset !== undefined) {
            // BUG: Cell offsets are not comparable to cell indices.
            return baseOffset > newId.index ? baseOffset - newId.index : -Infinity;
        }
    }
    const newOffset = (0, utils_1.getOffsetAtRevision)(newMark.lineage, baseId.revision);
    if (newOffset !== undefined) {
        if ((0, utils_1.isAttach)(baseMark)) {
            // BUG: This logic assumes that `baseId.revision` is the revision of the baseMark,
            // so this block should be gated on `isNewAttach(baseMark)`.
            // However, this logic happens to work in more cases than the logic after this block.
            // This should be fixed by changing LineageEvent to refer to cell IDs instead of offsets.
            const offset = newOffset - gapOffsetInBase;
            return offset === 0 ? Infinity : -offset;
        }
        // BUG: Cell offsets are not comparable to cell indices.
        return newOffset > baseId.index ? baseId.index - newOffset : Infinity;
    }
    const cmp = (0, utils_1.compareLineages)(baseMark.lineage, newMark.lineage);
    if (cmp !== 0) {
        return Math.sign(cmp) * Infinity;
    }
    if ((0, utils_1.isNewAttach)(newMark)) {
        // When the marks are at the same position, we use the tiebreak of `newMark`.
        // TODO: Use specified tiebreak instead of always tiebreaking left.
        return Infinity;
    }
    (0, common_utils_1.assert)((0, utils_1.isNewAttach)(baseMark), 0x6a1 /* Lineage should determine order of marks unless one is a new attach */);
    // `newMark` points to cells which were emptied before `baseMark` was created.
    // We use `baseMark`'s tiebreak policy as if `newMark`'s cells were created concurrently and before `baseMark`.
    // TODO: Use specified tiebreak instead of always tiebreaking left.
    return -Infinity;
}
//# sourceMappingURL=rebase.js.map