"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, 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) {
    let nodeChange = composeChildChanges(baseMark.changes, newMark.changes === undefined ? undefined : (0, core_1.tagChange)(newMark.changes, newRev), composeChild);
    if (nodeChange !== undefined) {
        const baseSource = (0, moveEffectTable_1.getMoveIn)(baseMark);
        if (baseSource !== undefined) {
            setModifyAfter(moveEffects, (0, utils_1.getEndpoint)(baseSource, undefined), nodeChange, newRev, composeChild);
            nodeChange = undefined;
        }
    }
    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)) {
        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 */);
        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) && (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 }, nodeChange);
        }
        return (0, utils_1.normalizeCellRename)({
            type: "AttachAndDetach",
            cellId: baseMark.cellId,
            count: baseMark.count,
            attach,
            detach,
        }, nodeChange);
    }
    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, 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, composeChild) {
    if (newChange === undefined) {
        return baseChange;
    }
    else if (baseChange === undefined) {
        return composeChild([newChange]);
    }
    else {
        return composeChild([(0, core_1.makeAnonChange)(baseChange), newChange]);
    }
}
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, (a, b) => composeChildChanges(a, b, composeChild));
    while (!queue.isEmpty()) {
        let mark = queue.dequeue();
        switch (mark.type) {
            case "Placeholder": {
                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.revisionMetadata = revisionMetadata;
        this.baseMarks = new markQueue_1.MarkQueue(baseMarks, baseRevision, moveEffects, true, genId, composeChanges);
        this.newMarks = new markQueue_1.MarkQueue(newMarks, newRevision, moveEffects, true, genId, composeChanges);
    }
    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.count, newMark, this.newRevision, 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, baseCellCount, newMark, newIntention, 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) {
        const comparison = (0, utils_1.compareCellsFromSameRevision)(baseCellId, baseCellCount, newCellId, newMark.count);
        if (comparison !== undefined) {
            return comparison;
        }
    }
    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, baseCellCount);
    if (offsetInNew !== undefined) {
        return offsetInNew > 0 ? -offsetInNew : Infinity;
    }
    const cmp = (0, utils_1.compareLineages)(baseCellId, newCellId);
    if (cmp !== 0) {
        return Math.sign(cmp) * Infinity;
    }
    (0, core_utils_1.assert)(baseCellId.revision !== undefined && newCellId.revision !== undefined, 0x858 /* Cells should have defined revisions */);
    if (!(0, utils_1.isNewAttach)(newMark)) {
        // If `newMark` were targeting a cell older than the composition window
        // there would be lineage determining the relative order of `newCell` and `baseCell`.
        // TODO:6127: Enable this assert
        // assert(
        // 	newRevisionIndex !== undefined,
        // 	"Expected lineage to determine cell order",
        // );
        // `newCell` was detached by a change in this composition, so there will be a corresponding mark
        // later in the base changeset.
        return -Infinity;
    }
    const newRevisionIndex = metadata.getIndex(newCellId.revision);
    const baseRevisionIndex = metadata.getIndex(baseCellId.revision);
    (0, core_utils_1.assert)(newRevisionIndex !== undefined, 0x859 /* A cell from a new attach should have a defined revision index */);
    // We use the tiebreaking policy of the newer cell.
    return (baseRevisionIndex ?? -Infinity) > newRevisionIndex ? -Infinity : Infinity;
}
// TODO: Reduce the duplication between this and other MoveEffect helpers
function setModifyAfter(moveEffects, { revision, localId: id }, modifyAfter, modifyRevision, 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;
    if (effect.value !== undefined) {
        const nodeChange = effect.value.modifyAfter !== undefined
            ? composeChanges([effect.value.modifyAfter, (0, core_1.tagChange)(modifyAfter, modifyRevision)])
            : modifyAfter;
        newEffect = { ...effect.value, modifyAfter: (0, core_1.makeAnonChange)(nodeChange) };
    }
    else {
        newEffect = { modifyAfter: (0, core_1.tagChange)(modifyAfter, modifyRevision) };
    }
    (0, moveEffectTable_1.setMoveEffect)(moveEffects, target, revision, id, count, newEffect);
}
function setEndpoint(moveEffects, target, id, count, endpoint) {
    const effect = (0, moveEffectTable_1.getMoveEffect)(moveEffects, target, id.revision, id.localId, count);
    const newEffect = effect.value !== undefined ? { ...effect.value, endpoint } : { endpoint };
    (0, moveEffectTable_1.setMoveEffect)(moveEffects, target, id.revision, id.localId, effect.length, newEffect);
    const remainingCount = count - effect.length;
    if (remainingCount > 0) {
        setEndpoint(moveEffects, target, offsetChangeAtomId(id, effect.length), remainingCount, offsetChangeAtomId(endpoint, effect.length));
    }
}
function offsetChangeAtomId(id, offset) {
    return { ...id, localId: (0, util_1.brand)(id.localId + offset) };
}
//# sourceMappingURL=compose.js.map