/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { assert, unreachableCase, fail } from "@fluidframework/core-utils/internal";
import { areEqualChangeAtomIdOpts, areEqualChangeAtomIds, } from "../../core/index.js";
import { MarkListFactory } from "./markListFactory.js";
import { MarkQueue } from "./markQueue.js";
import { CellOrder, areEqualCellIds, areInputCellsEmpty, areOutputCellsEmpty, cellSourcesFromMarks, compareCellPositionsUsingTombstones, extractMarkEffect, getAttachedRootId, getDetachOutputCellId, getDetachedRootId, getInputCellId, getMovedNodeId, getOutputCellId, isAttach, isDetach, isNewAttach, isNoopMark, isPin, isRename, markEmptiesCells, markHasCellEffect, normalizeCellRename, withNodeChange, } from "./utils.js";
/**
 * 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.
 */
export function compose(change1, change2, composeChild, _genId, manager, revisionMetadata) {
    return composeMarkLists(change1, change2, composeChild, manager, revisionMetadata);
}
function composeMarkLists(baseMarkList, newMarkList, composeChild, moveEffects, revisionMetadata) {
    const factory = new MarkListFactory();
    const queue = new ComposeQueue(baseMarkList, newMarkList, moveEffects, revisionMetadata);
    while (!queue.isEmpty()) {
        const { baseMark, newMark } = queue.pop();
        const composedMark = composeMarks(baseMark, newMark, composeChild, moveEffects);
        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, newMark, composeChild, moveEffects) {
    const nodeChange = handleNodeChanges(baseMark, newMark, composeChild, moveEffects);
    return withNodeChange(composeMarksIgnoreChild(baseMark, newMark, moveEffects), nodeChange);
}
function composeMarksIgnoreChild(baseMark, newMark, moveEffects) {
    if (isNoopMark(baseMark)) {
        return newMark;
    }
    else if (isNoopMark(newMark)) {
        return updateBaseMarkId(moveEffects, baseMark);
    }
    if (isRename(baseMark) && isRename(newMark)) {
        if (areEqualChangeAtomIdOpts(baseMark.cellId, newMark.idOverride)) {
            return createNoopMark(baseMark.count, undefined, baseMark.cellId);
        }
        return { ...baseMark, idOverride: newMark.idOverride };
    }
    else if (isRename(baseMark)) {
        assert(isAttach(newMark), 0x9f1 /* Unexpected mark type */);
        assert(baseMark.cellId !== undefined && newMark.cellId !== undefined, "Expected marks to target an empty cell");
        return { ...newMark, cellId: baseMark.cellId };
    }
    else if (isRename(newMark)) {
        assert(isDetach(baseMark), 0x9f2 /* Unexpected mark type */);
        return updateBaseMarkId(moveEffects, { ...baseMark, cellRename: newMark.idOverride });
    }
    if (!markHasCellEffect(baseMark)) {
        assert(baseMark.type === "Insert", "Expected baseMark to be a pin");
        // `newMark` can be either a remove or another pin.
        // A pin is treated as a detach and attach, so we call `composeAttachDetach` in either case.
        moveEffects.composeAttachDetach(getAttachedRootId(baseMark), {
            revision: newMark.revision,
            localId: newMark.id,
        }, baseMark.count);
        const pinId = getAttachedRootId(baseMark);
        return newMark.type === "Remove"
            ? {
                ...newMark,
                detachCellId: baseMark.detachCellId ?? pinId,
                cellRename: getDetachOutputCellId(newMark),
            }
            : newMark;
    }
    else if (!markHasCellEffect(newMark)) {
        if (isAttach(newMark) && isAttach(baseMark)) {
            // When composing two inserts, the second insert (which is a pin) should take precedence.
            // We treat the pin as a detach and reattach.
            moveEffects.composeAttachDetach(getAttachedRootId(baseMark), getAttachedRootId(newMark), baseMark.count);
            const composed = { cellId: baseMark.cellId, ...newMark };
            delete composed.detachCellId;
            return composed;
        }
        return updateBaseMarkId(moveEffects, baseMark);
    }
    else if (areInputCellsEmpty(baseMark)) {
        assert(isDetach(newMark), 0x71c /* Unexpected mark type */);
        assert(isAttach(baseMark), 0x71d /* Expected generative mark */);
        const attach = extractMarkEffect(baseMark);
        const detach = extractMarkEffect(newMark);
        moveEffects.composeAttachDetach(getAttachedRootId(baseMark), getDetachedRootId(newMark), baseMark.count);
        if (areEqualCellIds(getOutputCellId(newMark), baseMark.cellId)) {
            // The output and input cell IDs are the same, so this mark has no effect.
            return { count: baseMark.count, cellId: baseMark.cellId };
        }
        return normalizeCellRename(baseMark.cellId, baseMark.count, attach, detach);
    }
    else {
        assert(baseMark.type === "Remove", "Unexpected mark type");
        assert(newMark.type === "Insert", "Unexpected mark type");
        const detachId = getDetachedRootId(baseMark);
        const attachId = getAttachedRootId(newMark);
        // Note that we cannot assert that this returns true,
        // as it may not be until a second pass that MCF can tell that this is a reattach of the same node.
        moveEffects.composeDetachAttach(detachId, attachId, baseMark.count, true);
        // The composition has no net effect but we preserve the second change's intention to pin the nodes here.
        const composedMark = { ...newMark };
        delete composedMark.cellId;
        const baseDetachCellId = baseMark.detachCellId ?? detachId;
        if (!areEqualChangeAtomIds(baseDetachCellId, attachId)) {
            composedMark.detachCellId = baseDetachCellId;
        }
        return composedMark;
    }
}
function updateBaseMarkId(moveEffects, baseMark) {
    if (isDetach(baseMark)) {
        const baseDetachId = getDetachedRootId(baseMark);
        const updatedDetachId = getUpdatedDetachId(moveEffects, baseMark);
        if (updatedDetachId !== undefined &&
            !areEqualChangeAtomIds(updatedDetachId, baseDetachId)) {
            return {
                ...baseMark,
                revision: updatedDetachId.revision,
                id: updatedDetachId.localId,
                detachCellId: baseMark.detachCellId ?? baseDetachId,
            };
        }
    }
    return baseMark;
}
function createNoopMark(length, nodeChange, cellId) {
    const mark = { count: length };
    if (nodeChange !== undefined) {
        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 handleNodeChanges(baseMark, newMark, composeChild, moveEffects) {
    if (newMark.changes !== undefined) {
        if (baseMark.type === "Insert" && baseMark.cellId !== undefined) {
            moveEffects.sendNewChangesToBaseSourceLocation(getAttachedRootId(baseMark), newMark.changes);
            return undefined;
        }
    }
    // TODO: Make sure composeChild is not called twice on the node changes.
    return composeChildChanges(baseMark.changes, newMark.changes, composeChild);
}
function composeChildChanges(baseChange, newChange, composeChild) {
    if (baseChange === undefined && newChange === undefined) {
        return undefined;
    }
    return composeChild(baseChange, newChange);
}
export class ComposeQueue {
    constructor(baseMarks, newMarks, moveEffects, revisionMetadata) {
        this.moveEffects = moveEffects;
        this.revisionMetadata = revisionMetadata;
        const queryFunc = (mark) => isDetach(mark) || isPin(mark)
            ? moveEffects.getNewChangesForBaseDetach(getMovedNodeId(mark), mark.count).length
            : mark.count;
        this.baseMarks = new MarkQueue(baseMarks, queryFunc);
        this.newMarks = new MarkQueue(newMarks, (mark) => mark.count);
        this.baseMarksCellSources = cellSourcesFromMarks(baseMarks, getOutputCellId);
        this.newMarksCellSources = cellSourcesFromMarks(newMarks, getInputCellId);
    }
    isEmpty() {
        return this.baseMarks.isEmpty() && this.newMarks.isEmpty();
    }
    pop() {
        const baseMark = this.baseMarks.peek();
        const newMark = this.newMarks.peek();
        if (baseMark === undefined && newMark === undefined) {
            fail("Should not pop when queue is empty");
        }
        else if (baseMark === undefined) {
            return this.dequeueNew();
        }
        else if (newMark === undefined) {
            return this.dequeueBase();
        }
        else if (areOutputCellsEmpty(baseMark) && areInputCellsEmpty(newMark)) {
            const baseCellId = getOutputCellId(baseMark) ?? fail(0xb29 /* Expected defined output ID */);
            if (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
                // (which requires the local changes to have a revision tag))
                assert(isNewAttach(newMark), 0x695 /* TODO: Assign revision tags to each change in a transaction */);
                return this.dequeueNew();
            }
            const newCellId = getInputCellId(newMark);
            assert(newCellId !== undefined, 0x89d /* Both marks should have cell IDs */);
            const comparison = compareCellPositionsUsingTombstones(baseCellId, newCellId, this.baseMarksCellSources, this.newMarksCellSources, this.revisionMetadata);
            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 (areOutputCellsEmpty(baseMark)) {
            return this.dequeueBase();
        }
        else if (areInputCellsEmpty(newMark)) {
            return this.dequeueNew();
        }
        else {
            return this.dequeueBoth();
        }
    }
    dequeueBase(length = Number.POSITIVE_INFINITY) {
        const baseMark = this.baseMarks.dequeueUpTo(length);
        const movedChanges = getMovedChangesFromMark(this.moveEffects, baseMark);
        const newMark = createNoopMark(baseMark.count, movedChanges, getOutputCellId(baseMark));
        return { baseMark, newMark };
    }
    dequeueNew(length = Number.POSITIVE_INFINITY) {
        const newMark = this.newMarks.dequeueUpTo(length);
        const baseMark = createNoopMark(newMark.count, undefined, getInputCellId(newMark));
        return {
            baseMark,
            newMark,
        };
    }
    dequeueBoth() {
        const length = this.peekMinLength();
        const baseMark = this.baseMarks.dequeueUpTo(length);
        let newMark = this.newMarks.dequeueUpTo(length);
        const movedChanges = getMovedChangesFromMark(this.moveEffects, baseMark);
        if (movedChanges !== undefined) {
            assert(newMark.changes === undefined, 0x8da /* Unexpected node changeset collision */);
            newMark = withNodeChange(newMark, movedChanges);
        }
        return {
            baseMark,
            newMark,
        };
    }
    peekMinLength() {
        const baseMark = this.baseMarks.peek();
        const newMark = this.newMarks.peek();
        assert(baseMark !== undefined && newMark !== undefined, 0x8db /* Cannot peek length unless both mark queues are non-empty */);
        return Math.min(newMark.count, baseMark.count);
    }
}
function getMovedChangesFromMark(moveEffects, markEffect) {
    if (!isDetach(markEffect)) {
        return undefined;
    }
    return moveEffects.getNewChangesForBaseDetach(getDetachedRootId(markEffect), 1).value
        ?.nodeChange;
}
function getUpdatedDetachId(manager, mark) {
    return manager.getNewChangesForBaseDetach(getDetachedRootId(mark), mark.count).value
        ?.detachId;
}
//# sourceMappingURL=compose.js.map