/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { assert, unreachableCase } from "@fluidframework/core-utils";
import { CrossFieldTarget } from "../modular-schema";
import { brand } from "../../util";
import { areEqualCellIds, cloneMark, isAttachAndDetachEffect, splitMark } from "./utils";
import { tryGetVestigialEndpoint } from "./helperTypes";
export var MoveEnd;
(function (MoveEnd) {
    MoveEnd[MoveEnd["Source"] = 0] = "Source";
    MoveEnd[MoveEnd["Dest"] = 1] = "Dest";
})(MoveEnd || (MoveEnd = {}));
export function setMoveEffect(moveEffects, target, revision, id, count, effect, invalidate = true) {
    effect.basis = id;
    moveEffects.set(target, revision, id, count, effect, invalidate);
}
export function getMoveEffect(moveEffects, target, revision, id, count, addDependency = true) {
    const result = moveEffects.get(target, revision, id, count, addDependency);
    return result.value !== undefined
        ? { ...result, value: adjustMoveEffectBasis(result.value, id) }
        : result;
}
export function isMoveMark(effect) {
    return isMoveOut(effect) || isMoveIn(effect);
}
export function isMoveOut(effect) {
    return effect.type === "MoveOut";
}
export function isMoveIn(effect) {
    return effect.type === "MoveIn";
}
export function getMoveIn(effect) {
    switch (effect.type) {
        case "MoveIn":
            return effect;
        case "AttachAndDetach":
            return getMoveIn(effect.attach);
        default:
            return undefined;
    }
}
function adjustMoveEffectBasis(effect, newBasis) {
    if (effect.basis === newBasis) {
        return effect;
    }
    const adjusted = { ...effect, basis: newBasis };
    const basisShift = newBasis - effect.basis;
    assert(basisShift > 0, 0x812 /* Expected basis shift to be positive */);
    if (effect.endpoint !== undefined) {
        adjusted.endpoint = {
            ...effect.endpoint,
            localId: brand(effect.endpoint.localId + basisShift),
        };
    }
    if (effect.movedMark !== undefined) {
        const [_mark1, mark2] = splitMark(effect.movedMark, basisShift);
        adjusted.movedMark = mark2;
    }
    return adjusted;
}
function applyMoveEffectsToDest(markEffect, count, revision, effects, consumeEffects) {
    updateEndpoint(markEffect, count, CrossFieldTarget.Destination, revision, effects, consumeEffects);
}
function applyMoveEffectsToSource(mark, endpoint, updateFinalEndpoint, revision, effects, consumeEffect, composeChildren) {
    let nodeChange = mark.changes;
    const modifyAfter = getModifyAfter(effects, endpoint.revision ?? revision, endpoint.localId, mark.count, consumeEffect);
    if (modifyAfter !== undefined) {
        assert(composeChildren !== undefined, 0x569 /* Must provide a change composer if modifying moves */);
        nodeChange = composeChildren(mark.changes, modifyAfter);
    }
    const newMark = cloneMark(mark);
    if (updateFinalEndpoint && isMoveOut(newMark)) {
        applySourceEffects(newMark, mark.count, revision, effects, consumeEffect);
    }
    if (nodeChange !== undefined) {
        newMark.changes = nodeChange;
    }
    else {
        delete newMark.changes;
    }
    return newMark;
}
function applySourceEffects(markEffect, count, revision, effects, consumeEffects) {
    updateEndpoint(markEffect, count, CrossFieldTarget.Source, revision, effects, consumeEffects);
}
function updateEndpoint(markEffect, count, target, revision, effects, consumeEffects) {
    const markRevision = markEffect.revision ?? revision;
    const finalDest = getEndpoint(effects, target, markRevision, markEffect.id, count, consumeEffects);
    if (finalDest !== undefined) {
        if (areEqualCellIds(finalDest, { revision: markRevision, localId: markEffect.id })) {
            delete markEffect.finalEndpoint;
        }
        else {
            markEffect.finalEndpoint = finalDest;
        }
    }
}
export function applyMoveEffectsToMark(mark, revision, effects, consumeEffect, composeChildren) {
    return applyMoveEffectsToActiveMarks(applyMoveEffectsToVestigialMarks([mark], effects, revision, consumeEffect, composeChildren), revision, effects, consumeEffect, composeChildren);
}
function applyMoveEffectsToVestigialMarks(inputQueue, effects, revision, consumeEffect, composeChildren) {
    const outputQueue = [];
    let mark = inputQueue.shift();
    while (mark !== undefined) {
        const vestige = tryGetVestigialEndpoint(mark);
        if (vestige !== undefined) {
            const effect = getMoveEffect(effects, CrossFieldTarget.Source, vestige.revision ?? revision, vestige.localId, mark.count);
            if (effect.length < mark.count) {
                const [firstMark, secondMark] = splitMark(mark, effect.length);
                mark = firstMark;
                inputQueue.unshift(secondMark);
            }
            outputQueue.push(applyMoveEffectsToSource(mark, vestige, false, revision, effects, consumeEffect, composeChildren));
        }
        else {
            outputQueue.push(mark);
        }
        mark = inputQueue.shift();
    }
    return outputQueue;
}
function applyMoveEffectsToActiveMarks(inputQueue, revision, effects, consumeEffect, composeChildren) {
    const outputQueue = [];
    let mark = inputQueue.shift();
    while (mark !== undefined) {
        if (isAttachAndDetachEffect(mark)) {
            if (isMoveIn(mark.attach)) {
                if (isMoveOut(mark.detach)) {
                    // Move effects should not be applied to intermediate move locations.
                    outputQueue.push(mark);
                }
                else {
                    const attachRevision = mark.attach.revision ?? revision;
                    const effect = getMoveEffect(effects, CrossFieldTarget.Destination, attachRevision, mark.attach.id, mark.count);
                    let updatedAttach;
                    if (effect.length < mark.count) {
                        const [firstMark, secondMark] = splitMark(mark, effect.length);
                        mark = firstMark;
                        updatedAttach = firstMark.attach;
                        inputQueue.unshift(secondMark);
                    }
                    else {
                        updatedAttach = { ...mark.attach };
                    }
                    applyMoveEffectsToDest(updatedAttach, mark.count, attachRevision, effects, consumeEffect);
                    outputQueue.push({
                        ...mark,
                        attach: updatedAttach,
                    });
                }
            }
            else if (isMoveOut(mark.detach)) {
                const detachRevision = mark.detach.revision ?? revision;
                const effect = getMoveEffect(effects, CrossFieldTarget.Source, detachRevision, mark.detach.id, mark.count);
                if (effect.length < mark.count) {
                    const [firstMark, secondMark] = splitMark(mark, effect.length);
                    mark = firstMark;
                    inputQueue.unshift(secondMark);
                }
                const newMark = cloneMark(mark);
                applySourceEffects(newMark.detach, mark.count, detachRevision, effects, consumeEffect);
                const newChanges = getModifyAfter(effects, detachRevision, mark.detach.id, mark.count, consumeEffect);
                if (newChanges !== undefined) {
                    assert(composeChildren !== undefined, 0x814 /* Must provide a change composer if modifying moves */);
                    newMark.changes = composeChildren(mark.changes, newChanges);
                }
                outputQueue.push(newMark);
            }
            else {
                outputQueue.push(mark);
            }
        }
        else if (isMoveMark(mark)) {
            const type = mark.type;
            switch (type) {
                case "MoveOut": {
                    const effect = getMoveEffect(effects, CrossFieldTarget.Source, mark.revision ?? revision, mark.id, mark.count);
                    if (effect.length < mark.count) {
                        const [firstMark, secondMark] = splitMark(mark, effect.length);
                        mark = firstMark;
                        inputQueue.unshift(secondMark);
                    }
                    outputQueue.push(applyMoveEffectsToSource(mark, { revision: mark.revision, localId: mark.id }, true, revision, effects, consumeEffect, composeChildren));
                    break;
                }
                case "MoveIn": {
                    const effect = getMoveEffect(effects, CrossFieldTarget.Destination, mark.revision ?? revision, mark.id, mark.count);
                    if (effect.length < mark.count) {
                        const [firstMark, secondMark] = splitMark(mark, effect.length);
                        mark = firstMark;
                        inputQueue.unshift(secondMark);
                    }
                    const newMark = cloneMark(mark);
                    applyMoveEffectsToDest(newMark, mark.count, revision, effects, consumeEffect);
                    outputQueue.push(newMark);
                    break;
                }
                default:
                    unreachableCase(type);
            }
        }
        else {
            outputQueue.push(mark);
        }
        mark = inputQueue.shift();
    }
    return outputQueue;
}
// 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
export function getModifyAfter(moveEffects, revision, id, count, consumeEffect = true) {
    const target = CrossFieldTarget.Source;
    const effect = getMoveEffect(moveEffects, target, revision, id, count);
    if (effect.value?.modifyAfter !== undefined) {
        assert(effect.length === count, 0x6ee /* Expected effect to cover entire mark */);
        if (consumeEffect) {
            const newEffect = { ...effect.value };
            delete newEffect.modifyAfter;
            setMoveEffect(moveEffects, target, revision, id, count, newEffect, false);
        }
        return effect.value.modifyAfter;
    }
    return undefined;
}
function getEndpoint(moveEffects, target, revision, id, count, consumeEffect = true) {
    const effect = getMoveEffect(moveEffects, target, revision, id, count);
    assert(effect.length === count, 0x815 /* Expected effect to cover entire mark */);
    if (effect.value?.endpoint === undefined) {
        return undefined;
    }
    if (consumeEffect) {
        const newEffect = { ...effect.value };
        delete newEffect.endpoint;
        setMoveEffect(moveEffects, target, revision, id, count, newEffect, false);
    }
    return effect.value.endpoint;
}
//# sourceMappingURL=moveEffectTable.js.map