"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.ComposeQueue = exports.amendCompose = exports.compose = void 0;
const core_utils_1 = require("@fluidframework/core-utils");
const core_1 = require("../../core");
const util_1 = require("../../util");
const modular_schema_1 = require("../modular-schema");
const types_1 = require("./types");
const markListFactory_1 = require("./markListFactory");
const markQueue_1 = require("./markQueue");
const moveEffectTable_1 = require("./moveEffectTable");
const utils_1 = require("./utils");
/**
 * Composes a sequence of changesets into a single changeset.
 * @param changes - The changesets to be applied.
 * Parts of the input may be reused in the output, but the input is not mutated.
 * Each changeset in the list is assumed to be applicable after the previous one.
 * @returns A changeset that is equivalent to applying each of the given `changes` in order.
 *
 * WARNING! This implementation is incomplete:
 * - Tombstone information is ignored.
 * - Support for moves is not implemented.
 * - Support for slices is not implemented.
 */
function compose(changes, composeChild, genId, manager, revisionMetadata) {
    let composed = [];
    for (const change of changes) {
        composed = composeMarkLists(composed, change.revision, change.change, composeChild, genId, manager, revisionMetadata);
    }
    return composed;
}
exports.compose = compose;
function composeMarkLists(baseMarkList, newRev, newMarkList, composeChild, genId, moveEffects, revisionMetadata) {
    const factory = new markListFactory_1.MarkListFactory();
    const queue = new ComposeQueue(undefined, baseMarkList, newRev, newMarkList, genId, moveEffects, revisionMetadata, (a, b) => composeChildChanges(a, b, newRev, composeChild));
    while (!queue.isEmpty()) {
        const { baseMark, newMark } = queue.pop();
        if (newMark === undefined) {
            (0, core_utils_1.assert)(baseMark !== undefined, 0x4db /* Non-empty queue should not return two undefined marks */);
            factory.push(baseMark);
        }
        else {
            // We only compose changesets that will not be further rebased.
            // It is therefore safe to remove any intentions that have no impact in the context they apply to.
            const settledNewMark = (0, utils_1.settleMark)(newMark, newRev, revisionMetadata);
            if (baseMark === undefined) {
                factory.push(composeMark(settledNewMark, newRev, composeChild));
            }
            else {
                // Past this point, we are guaranteed that `settledNewMark` and `baseMark` have the same length and
                // start at the same location in the revision after the base changes.
                // They therefore refer to the same range for that revision.
                const composedMark = composeMarks(baseMark, newRev, settledNewMark, composeChild, genId, moveEffects, revisionMetadata);
                factory.push(composedMark);
            }
        }
    }
    return factory.list;
}
/**
 * Composes two marks where `newMark` is based on the state produced by `baseMark`.
 * @param baseMark - The mark to compose with `newMark`.
 * Its output range should be the same as `newMark`'s input range.
 * @param newRev - The revision the new mark is part of.
 * @param newMark - The mark to compose with `baseMark`.
 * Its input range should be the same as `baseMark`'s output range.
 * @returns A mark that is equivalent to applying both `baseMark` and `newMark` successively.
 */
function composeMarks(baseMark, newRev, newMark, composeChild, genId, moveEffects, revisionMetadata) {
    const nodeChange = composeChildChanges(baseMark.changes, newMark.changes, newRev, composeChild);
    if ((0, utils_1.isImpactfulCellRename)(newMark, newRev, revisionMetadata)) {
        const newAttachAndDetach = (0, utils_1.asAttachAndDetach)(newMark);
        const newDetachRevision = newAttachAndDetach.detach.revision ?? newRev;
        if ((0, utils_1.markEmptiesCells)(baseMark)) {
            if ((0, moveEffectTable_1.isMoveIn)(newAttachAndDetach.attach) && (0, moveEffectTable_1.isMoveOut)(newAttachAndDetach.detach)) {
                (0, core_utils_1.assert)((0, moveEffectTable_1.isMoveOut)(baseMark), 0x808 /* Unexpected mark type */);
                // The base changeset and new changeset both move these nodes.
                // Call the original position of the nodes A, the position after the base changeset is applied B,
                // and the position after the new changeset is applied C.
                // The new changeset moves the nodes from B, temporarily returns them to A, and then moves them to C.
                // The composition of the base and new changesets will be a move directly from A to C,
                // since the move from A to B cancels out with the return from B to A.
                // This if-block is handling marks at A.
                // When we compose the marks at B we will link the start of the base move (A to B)
                // with the end of the new move (B to C).
                // Because we are replacing the mark representing the start of the move with the new changeset's
                // move-out from A, we update the base move-in at B to consider that its start point.
                setEndpoint(moveEffects, modular_schema_1.CrossFieldTarget.Destination, (0, utils_1.getEndpoint)(baseMark, undefined), baseMark.count, { revision: newDetachRevision, localId: newAttachAndDetach.detach.id });
            }
            // baseMark is a detach which cancels with the attach portion of the AttachAndDetach,
            // so we are just left with the detach portion of the AttachAndDetach.
            return (0, utils_1.withRevision)((0, utils_1.withNodeChange)({ ...newAttachAndDetach.detach, count: baseMark.count }, nodeChange), newDetachRevision);
        }
        if ((0, utils_1.isImpactfulCellRename)(baseMark, undefined, revisionMetadata)) {
            const baseAttachAndDetach = (0, utils_1.asAttachAndDetach)(baseMark);
            const newOutputId = (0, utils_1.getOutputCellId)(newAttachAndDetach, newRev, revisionMetadata);
            if ((0, utils_1.areEqualCellIds)(newOutputId, baseAttachAndDetach.cellId)) {
                return (0, utils_1.withNodeChange)({ count: baseAttachAndDetach.count, cellId: baseAttachAndDetach.cellId }, nodeChange);
            }
            // `newMark`'s attach portion cancels with `baseMark`'s detach portion.
            const originalAttach = { ...baseAttachAndDetach.attach };
            const finalDetach = { ...newAttachAndDetach.detach };
            const detachRevision = finalDetach.revision ?? newRev;
            if (detachRevision !== undefined) {
                finalDetach.revision = detachRevision;
            }
            return (0, utils_1.normalizeCellRename)({
                type: "AttachAndDetach",
                cellId: baseMark.cellId,
                count: baseMark.count,
                attach: originalAttach,
                detach: finalDetach,
            }, nodeChange);
        }
        return (0, utils_1.withRevision)((0, utils_1.normalizeCellRename)(newAttachAndDetach, nodeChange), newRev);
    }
    if ((0, utils_1.isImpactfulCellRename)(baseMark, undefined, revisionMetadata)) {
        const baseAttachAndDetach = (0, utils_1.asAttachAndDetach)(baseMark);
        if ((0, utils_1.markFillsCells)(newMark)) {
            if ((0, moveEffectTable_1.isMoveIn)(baseAttachAndDetach.attach) && (0, moveEffectTable_1.isMoveOut)(baseAttachAndDetach.detach)) {
                (0, core_utils_1.assert)((0, moveEffectTable_1.isMoveIn)(newMark), 0x809 /* Unexpected mark type */);
                setEndpoint(moveEffects, modular_schema_1.CrossFieldTarget.Source, (0, utils_1.getEndpoint)(newMark, newRev), baseAttachAndDetach.count, {
                    revision: baseAttachAndDetach.attach.revision,
                    localId: baseAttachAndDetach.attach.id,
                });
            }
            const originalAttach = (0, utils_1.withRevision)((0, utils_1.withNodeChange)({
                ...baseAttachAndDetach.attach,
                cellId: baseAttachAndDetach.cellId,
                count: baseAttachAndDetach.count,
            }, nodeChange), baseAttachAndDetach.attach.revision);
            return originalAttach;
        }
        else {
            // Other mark types have been handled by previous conditional branches.
            (0, core_utils_1.assert)(newMark.type === types_1.NoopMarkType || newMark.type === "Placeholder", 0x80a /* Unexpected mark type */);
            return (0, utils_1.withNodeChange)(baseMark, nodeChange);
        }
    }
    if (!(0, utils_1.markHasCellEffect)(baseMark) && !(0, utils_1.markHasCellEffect)(newMark)) {
        if ((0, utils_1.isNoopMark)(baseMark)) {
            return (0, utils_1.withNodeChange)(newMark, nodeChange);
        }
        else if ((0, utils_1.isNoopMark)(newMark)) {
            return (0, utils_1.withNodeChange)(baseMark, nodeChange);
        }
        return createNoopMark(newMark.count, nodeChange, (0, utils_1.getInputCellId)(baseMark, undefined, undefined));
    }
    else if (!(0, utils_1.markHasCellEffect)(baseMark)) {
        return (0, utils_1.withRevision)((0, utils_1.withNodeChange)(newMark, nodeChange), newRev);
    }
    else if (!(0, utils_1.markHasCellEffect)(newMark)) {
        if ((0, moveEffectTable_1.isMoveIn)(baseMark) && nodeChange !== undefined) {
            setModifyAfter(moveEffects, (0, utils_1.getEndpoint)(baseMark, undefined), nodeChange, composeChild);
            return baseMark;
        }
        return (0, utils_1.withNodeChange)(baseMark, nodeChange);
    }
    else if ((0, utils_1.areInputCellsEmpty)(baseMark)) {
        (0, core_utils_1.assert)((0, utils_1.isDetach)(newMark), 0x71c /* Unexpected mark type */);
        (0, core_utils_1.assert)((0, utils_1.isAttach)(baseMark), 0x71d /* Expected generative mark */);
        let localNodeChange = nodeChange;
        const attach = (0, utils_1.extractMarkEffect)(baseMark);
        const detach = (0, utils_1.extractMarkEffect)((0, utils_1.withRevision)(newMark, newRev));
        if ((0, moveEffectTable_1.isMoveIn)(attach) && nodeChange !== undefined) {
            setModifyAfter(moveEffects, (0, utils_1.getEndpoint)(attach, undefined), nodeChange, composeChild);
            localNodeChange = undefined;
        }
        if ((0, moveEffectTable_1.isMoveIn)(attach) && (0, moveEffectTable_1.isMoveOut)(detach)) {
            const finalSource = (0, utils_1.getEndpoint)(attach, undefined);
            const finalDest = (0, utils_1.getEndpoint)(detach, newRev);
            setEndpoint(moveEffects, modular_schema_1.CrossFieldTarget.Source, finalSource, baseMark.count, finalDest);
            setEndpoint(moveEffects, modular_schema_1.CrossFieldTarget.Destination, finalDest, baseMark.count, finalSource);
            // The `finalEndpoint` field of AttachAndDetach move effect pairs is not used,
            // so we remove it as a normalization.
            delete attach.finalEndpoint;
            delete detach.finalEndpoint;
        }
        if ((0, utils_1.areEqualCellIds)((0, utils_1.getOutputCellId)(newMark, newRev, revisionMetadata), baseMark.cellId)) {
            // The output and input cell IDs are the same, so this mark has no effect.
            return (0, utils_1.withNodeChange)({ count: baseMark.count, cellId: baseMark.cellId }, localNodeChange);
        }
        return (0, utils_1.normalizeCellRename)({
            type: "AttachAndDetach",
            cellId: baseMark.cellId,
            count: baseMark.count,
            attach,
            detach,
        }, localNodeChange);
    }
    else {
        if ((0, moveEffectTable_1.isMoveMark)(baseMark) && (0, moveEffectTable_1.isMoveMark)(newMark)) {
            // The marks must be inverses, since `newMark` is filling the cells which `baseMark` emptied.
            const nodeChanges = (0, moveEffectTable_1.getModifyAfter)(moveEffects, baseMark.revision, baseMark.id, baseMark.count);
            // We return a placeholder instead of a noop because there may be more node changes on `newMark`'s source mark
            // which need to be included here.
            // We will remove the placeholder during `amendCompose`.
            return {
                type: "Placeholder",
                count: baseMark.count,
                revision: baseMark.revision,
                id: baseMark.id,
                changes: composeChildChanges(nodeChange, nodeChanges, undefined, composeChild),
            };
        }
        const length = baseMark.count;
        return createNoopMark(length, nodeChange);
    }
}
function createNoopMark(length, nodeChange, cellId) {
    const mark = { count: length };
    if (nodeChange !== undefined) {
        (0, core_utils_1.assert)(length === 1, 0x692 /* A mark with a node change must have length one */);
        mark.changes = nodeChange;
    }
    if (cellId !== undefined) {
        mark.cellId = cellId;
    }
    return mark;
}
function composeChildChanges(baseChange, newChange, newRevision, composeChild) {
    if (newChange === undefined) {
        return baseChange;
    }
    else if (baseChange === undefined) {
        return composeChild([(0, core_1.tagChange)(newChange, newRevision)]);
    }
    else {
        return composeChild([(0, core_1.makeAnonChange)(baseChange), (0, core_1.tagChange)(newChange, newRevision)]);
    }
}
function composeMark(mark, revision, composeChild) {
    const cloned = (0, utils_1.cloneMark)(mark);
    if (cloned.cellId !== undefined &&
        cloned.cellId.revision === undefined &&
        revision !== undefined) {
        (0, util_1.asMutable)(cloned.cellId).revision = revision;
    }
    (0, utils_1.addRevision)(cloned, revision);
    if (cloned.changes !== undefined) {
        cloned.changes = composeChild([(0, core_1.tagChange)(cloned.changes, revision)]);
        return cloned;
    }
    return cloned;
}
function amendCompose(marks, composeChild, genId, manager) {
    return amendComposeI(marks, composeChild, manager);
}
exports.amendCompose = amendCompose;
function amendComposeI(marks, composeChild, moveEffects) {
    const factory = new markListFactory_1.MarkListFactory();
    const queue = new markQueue_1.MarkQueue(marks, undefined, moveEffects, true, util_1.fakeIdAllocator, 
    // TODO: Should pass in revision for new changes
    (a, b) => composeChildChanges(a, b, undefined, composeChild));
    while (!queue.isEmpty()) {
        let mark = queue.dequeue();
        switch (mark.type) {
            case "Placeholder": {
                const modifyAfter = (0, moveEffectTable_1.getModifyAfter)(moveEffects, mark.revision, mark.id, mark.count);
                if (modifyAfter !== undefined) {
                    const changes = composeChildChanges(mark.changes, modifyAfter, undefined, composeChild);
                    mark = createNoopMark(mark.count, changes);
                }
                else {
                    mark = createNoopMark(mark.count, mark.changes);
                }
            }
            default:
                break;
        }
        factory.push(mark);
    }
    return factory.list;
}
class ComposeQueue {
    constructor(baseRevision, baseMarks, newRevision, newMarks, genId, moveEffects, revisionMetadata, composeChanges) {
        this.newRevision = newRevision;
        this.moveEffects = moveEffects;
        this.revisionMetadata = revisionMetadata;
        this.cancelledInserts = new Set();
        this.baseMarks = new markQueue_1.MarkQueue(baseMarks, baseRevision, moveEffects, true, genId, composeChanges);
        this.newMarks = new markQueue_1.MarkQueue(newMarks, newRevision, moveEffects, true, genId, composeChanges);
        // Detect all inserts in the new marks that will be cancelled by deletes in the base marks
        const deletes = new Set();
        for (const mark of baseMarks) {
            if ((0, utils_1.isDeleteMark)(mark)) {
                const baseIntention = (0, modular_schema_1.getIntention)(mark.revision, revisionMetadata);
                if (baseIntention !== undefined) {
                    deletes.add(baseIntention);
                }
            }
        }
        for (const mark of newMarks) {
            if (mark.type === "Insert") {
                const newRev = mark.revision ?? this.newRevision;
                const newIntention = (0, modular_schema_1.getIntention)(newRev, revisionMetadata);
                if (newIntention !== undefined && deletes.has(newIntention)) {
                    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                    this.cancelledInserts.add(newRev);
                }
            }
        }
    }
    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 this.dequeueNew(length);
        }
        else if (newMark === undefined) {
            const length = (0, utils_1.getOutputLength)(baseMark);
            return this.dequeueBase(length);
        }
        else if ((0, utils_1.areOutputCellsEmpty)(baseMark) && (0, utils_1.areInputCellsEmpty)(newMark)) {
            const baseCellId = (0, utils_1.getOutputCellId)(baseMark, this.baseMarks.revision, this.revisionMetadata) ??
                (0, util_1.fail)("Expected defined output ID");
            if ((0, utils_1.markEmptiesCells)(baseMark) && baseCellId.revision === undefined) {
                // The base revision should always be defined except when squashing changes into a transaction.
                // In the future, we want to support reattaches in the new change here.
                // We will need to be able to order the base mark relative to the new mark by looking at the lineage of the new mark
                // (which will be obtained by rebasing the reattach over interim changes
                // (which requires the local changes to have a revision tag))
                (0, core_utils_1.assert)((0, utils_1.isNewAttach)(newMark), 0x695 /* TODO: Assign revision tags to each change in a transaction */);
                return this.dequeueNew();
            }
            const cmp = compareCellPositions(baseCellId, baseMark, newMark, this.newRevision, this.cancelledInserts, this.revisionMetadata);
            if (cmp < 0) {
                return { baseMark: this.baseMarks.dequeueUpTo(-cmp) };
            }
            else if (cmp > 0) {
                return { newMark: this.newMarks.dequeueUpTo(cmp) };
            }
            else {
                return this.dequeueBoth();
            }
        }
        else if ((0, utils_1.areOutputCellsEmpty)(baseMark)) {
            return this.dequeueBase();
        }
        else if ((0, utils_1.areInputCellsEmpty)(newMark)) {
            return this.dequeueNew();
        }
        else {
            return this.dequeueBoth();
        }
    }
    dequeueBase(length = 0) {
        const baseMark = this.baseMarks.dequeue();
        return { baseMark, newMark: length > 0 ? { count: length } : undefined };
    }
    dequeueNew(length = 0) {
        return {
            baseMark: length > 0 ? { count: length } : undefined,
            newMark: this.newMarks.dequeue(),
        };
    }
    dequeueBoth() {
        const baseMark = this.baseMarks.peek();
        const newMark = this.newMarks.peek();
        (0, core_utils_1.assert)(baseMark !== undefined && newMark !== undefined, 0x697 /* Cannot dequeue both unless both mark queues are non-empty */);
        const length = Math.min(newMark.count, baseMark.count);
        return {
            baseMark: this.baseMarks.dequeueUpTo(length),
            newMark: this.newMarks.dequeueUpTo(length),
        };
    }
}
exports.ComposeQueue = ComposeQueue;
// TODO: Try to share more logic with the version in rebase.ts.
/**
 * 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(baseCellId, baseMark, newMark, newIntention, cancelledInserts, metadata) {
    const newCellId = (0, utils_1.getInputCellId)(newMark, newIntention, metadata);
    (0, core_utils_1.assert)(newCellId !== undefined, 0x71f /* Should have cell ID */);
    if (baseCellId.revision === newCellId.revision) {
        if ((0, utils_1.isNewAttach)(newMark)) {
            // There is some change foo that is being cancelled out as part of a rebase sandwich.
            // The marks that make up this change (and its inverse) may be broken up differently between the base
            // changeset and the new changeset because either changeset may have been composed with other changes
            // whose marks may now be interleaved with the marks that represent foo/its inverse.
            // This means that the base and new marks may not be of the same length.
            // We do however know that the all of the marks for foo will appear in the base changeset and all of the
            // marks for the inverse of foo will appear in the new changeset, so we can be confident that whenever
            // we encounter such pairs of marks, they do line up such that they describe changes to the same first
            // cell. This means we can safely treat them as inverses of one another.
            return 0;
        }
        if ((0, utils_1.areOverlappingIdRanges)(baseCellId.localId, baseMark.count, newCellId.localId, newMark.count)) {
            return baseCellId.localId - newCellId.localId;
        }
    }
    const offsetInBase = (0, utils_1.getOffsetInCellRange)(baseCellId.lineage, newCellId.revision, newCellId.localId, newMark.count);
    if (offsetInBase !== undefined) {
        return offsetInBase > 0 ? offsetInBase : -Infinity;
    }
    const offsetInNew = (0, utils_1.getOffsetInCellRange)(newCellId.lineage, baseCellId.revision, baseCellId.localId, baseMark.count);
    if (offsetInNew !== undefined) {
        return offsetInNew > 0 ? -offsetInNew : Infinity;
    }
    const cmp = (0, utils_1.compareLineages)(baseCellId, newCellId, metadata);
    if (cmp !== 0) {
        return Math.sign(cmp) * Infinity;
    }
    if (newIntention !== undefined &&
        newMark.type === "Insert" &&
        cancelledInserts.has(newIntention)) {
        // We know the new insert is getting cancelled out so we need to delay returning it.
        // The base mark that cancels the insert must appear later in the base marks.
        return -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;
    }
    // We know `newMark` points to cells which were emptied before `baseMark` was created,
    // because otherwise `baseMark` would have lineage refering to the emptying of the cell.
    // 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.
    if ((0, utils_1.isNewAttach)(baseMark)) {
        return -Infinity;
    }
    // If `newMark`'s lineage does not overlap with `baseMark`'s,
    // then `newMark` must be referring to cells which were created after `baseMark` was applied.
    // The creation of those cells should happen in this composition, so they must be later in the base mark list.
    // This is true because there may be any number of changesets between the base and new changesets, which the new changeset might be refering to the cells of.
    return -Infinity;
}
// 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 setModifyAfter(moveEffects, { revision, localId: id }, modifyAfter, composeChanges) {
    const target = modular_schema_1.CrossFieldTarget.Source;
    const count = 1;
    const effect = (0, moveEffectTable_1.getMoveEffect)(moveEffects, target, revision, id, count, false);
    let newEffect;
    (0, core_utils_1.assert)(effect.length === count, 0x6ec /* Expected effect to cover entire mark */);
    if (effect.value !== undefined) {
        const nodeChange = effect.value.modifyAfter !== undefined
            ? composeChanges([
                (0, core_1.makeAnonChange)(effect.value.modifyAfter),
                (0, core_1.tagChange)(modifyAfter, revision),
            ])
            : modifyAfter;
        newEffect = { ...effect.value, modifyAfter: nodeChange };
    }
    else {
        newEffect = { modifyAfter };
    }
    (0, moveEffectTable_1.setMoveEffect)(moveEffects, target, revision, id, count, newEffect);
}
function setEndpoint(moveEffects, target, { revision, localId: id }, count, endpoint) {
    const effect = (0, moveEffectTable_1.getMoveEffect)(moveEffects, target, revision, id, count);
    (0, core_utils_1.assert)(effect.length === count, 0x80b /* Expected effect to cover entire mark */);
    const newEffect = effect.value !== undefined ? { ...effect.value, endpoint } : { endpoint };
    (0, moveEffectTable_1.setMoveEffect)(moveEffects, target, revision, id, count, newEffect);
}
//# sourceMappingURL=compose.js.map