"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.invert = void 0;
const core_utils_1 = require("@fluidframework/core-utils");
const util_1 = require("../../util");
const modular_schema_1 = require("../modular-schema");
const types_1 = require("./types");
const markListFactory_1 = require("./markListFactory");
const utils_1 = require("./utils");
/**
 * Inverts a given changeset.
 * @param change - The changeset to produce the inverse of.
 * @returns The inverse of the given `change` such that the inverse can be applied after `change`.
 *
 * WARNING! This implementation is incomplete:
 * - Support for slices is not implemented.
 */
function invert(change, invertChild, genId, crossFieldManager, revisionMetadata) {
    return invertMarkList(change.change, change.revision, invertChild, crossFieldManager, revisionMetadata);
}
exports.invert = invert;
function invertMarkList(markList, revision, invertChild, crossFieldManager, revisionMetadata) {
    const inverseMarkList = new markListFactory_1.MarkListFactory();
    for (const mark of markList) {
        const inverseMarks = invertMark(mark, revision, invertChild, crossFieldManager, revisionMetadata);
        inverseMarkList.push(...inverseMarks);
    }
    return inverseMarkList.list;
}
function invertMark(mark, revision, invertChild, crossFieldManager, revisionMetadata) {
    if (!(0, utils_1.isImpactful)(mark, revision, revisionMetadata)) {
        return [invertNodeChangeOrSkip(mark.count, mark.changes, invertChild, mark.cellId)];
    }
    const type = mark.type;
    switch (type) {
        case types_1.NoopMarkType: {
            const inverse = { ...mark };
            if (mark.changes !== undefined) {
                inverse.changes = invertChild(mark.changes);
            }
            return [inverse];
        }
        case "Delete": {
            (0, core_utils_1.assert)(revision !== undefined, 0x5a1 /* Unable to revert to undefined revision */);
            const outputId = (0, utils_1.getOutputCellId)(mark, revision, revisionMetadata);
            const inverse = mark.cellId === undefined
                ? {
                    type: "Insert",
                    id: mark.id,
                    cellId: outputId,
                    count: mark.count,
                }
                : {
                    type: "Delete",
                    id: mark.id,
                    cellId: outputId,
                    count: mark.count,
                    detachIdOverride: (0, utils_1.getInputCellId)(mark, revision, revisionMetadata),
                };
            return [(0, utils_1.withNodeChange)(inverse, invertNodeChange(mark.changes, invertChild))];
        }
        case "Insert": {
            (0, core_utils_1.assert)(mark.cellId !== undefined, 0x80c /* Active inserts should target empty cells */);
            const deleteMark = {
                type: "Delete",
                count: mark.count,
                id: mark.cellId.localId,
            };
            if ((0, utils_1.isReattach)(mark)) {
                deleteMark.detachIdOverride = mark.cellId;
            }
            const inverse = (0, utils_1.withNodeChange)(deleteMark, invertNodeChange(mark.changes, invertChild));
            return [inverse];
        }
        case "MoveOut": {
            if (mark.changes !== undefined) {
                (0, core_utils_1.assert)(mark.count === 1, 0x6ed /* Mark with changes can only target a single cell */);
                const endpoint = (0, utils_1.getEndpoint)(mark, revision);
                crossFieldManager.set(modular_schema_1.CrossFieldTarget.Destination, endpoint.revision, endpoint.localId, mark.count, invertChild(mark.changes), true);
            }
            const cellId = (0, utils_1.getDetachOutputId)(mark, mark.revision ?? revision ?? (0, util_1.fail)("Revision must be defined"), revisionMetadata) ?? {
                revision: mark.revision ?? revision ?? (0, util_1.fail)("Revision must be defined"),
                localId: mark.id,
            };
            const moveIn = {
                type: "MoveIn",
                id: mark.id,
            };
            if (mark.finalEndpoint !== undefined) {
                moveIn.finalEndpoint = { localId: mark.finalEndpoint.localId };
            }
            let effect = moveIn;
            if ((0, utils_1.areInputCellsEmpty)(mark)) {
                effect = {
                    type: "AttachAndDetach",
                    attach: moveIn,
                    detach: {
                        type: "Delete",
                        id: mark.id,
                        detachIdOverride: (0, utils_1.getInputCellId)(mark, revision, revisionMetadata),
                    },
                };
            }
            return [{ ...effect, count: mark.count, cellId }];
        }
        case "MoveIn": {
            const invertedMark = {
                type: "MoveOut",
                id: mark.id,
                count: mark.count,
            };
            if (mark.finalEndpoint) {
                invertedMark.finalEndpoint = { localId: mark.finalEndpoint.localId };
            }
            if ((0, utils_1.isReattach)(mark)) {
                invertedMark.detachIdOverride = mark.cellId;
            }
            return applyMovedChanges(invertedMark, revision, crossFieldManager);
        }
        case "AttachAndDetach": {
            // Which should get the child change? Don't want to invert twice
            const attach = {
                count: mark.count,
                cellId: mark.cellId,
                ...mark.attach,
            };
            const idAfterAttach = (0, utils_1.getOutputCellId)(attach, revision, undefined);
            // We put `mark.changes` on the detach so that if it is a move source
            // the changes can be sent to the endpoint.
            const detach = {
                count: mark.count,
                cellId: idAfterAttach,
                changes: mark.changes,
                ...mark.detach,
            };
            const attachInverses = invertMark(attach, revision, invertChild, crossFieldManager, revisionMetadata);
            const detachInverses = invertMark(detach, revision, invertChild, crossFieldManager, revisionMetadata);
            if (detachInverses.length === 0) {
                return attachInverses;
            }
            (0, core_utils_1.assert)(detachInverses.length === 1, 0x80d /* Only expected MoveIn marks to be split when inverting */);
            let detachInverse = detachInverses[0];
            (0, core_utils_1.assert)((0, utils_1.isAttach)(detachInverse), 0x80e /* Inverse of a detach should be an attach */);
            const inverses = [];
            for (const attachInverse of attachInverses) {
                let detachInverseCurr = detachInverse;
                if (attachInverse.count !== detachInverse.count) {
                    [detachInverseCurr, detachInverse] = (0, utils_1.splitMark)(detachInverse, attachInverse.count);
                }
                if (attachInverse.type === types_1.NoopMarkType) {
                    if (attachInverse.changes !== undefined) {
                        (0, core_utils_1.assert)(detachInverseCurr.changes === undefined, 0x80f /* Unexpected node changes */);
                        detachInverseCurr.changes = attachInverse.changes;
                    }
                    inverses.push(detachInverseCurr);
                    continue;
                }
                (0, core_utils_1.assert)((0, utils_1.isDetach)(attachInverse), 0x810 /* Inverse of an attach should be a detach */);
                const inverted = {
                    type: "AttachAndDetach",
                    count: attachInverse.count,
                    attach: (0, utils_1.extractMarkEffect)(detachInverseCurr),
                    detach: (0, utils_1.extractMarkEffect)(attachInverse),
                };
                if (detachInverseCurr.cellId !== undefined) {
                    inverted.cellId = detachInverseCurr.cellId;
                }
                if (detachInverseCurr.changes !== undefined) {
                    inverted.changes = detachInverseCurr.changes;
                }
                if (attachInverse.changes !== undefined) {
                    (0, core_utils_1.assert)(inverted.changes === undefined, 0x811 /* Unexpected node changes */);
                    inverted.changes = attachInverse.changes;
                }
                inverses.push((0, utils_1.normalizeCellRename)(inverted));
            }
            return inverses;
        }
        case "Placeholder":
            (0, util_1.fail)("Should not invert placeholder marks");
        default:
            (0, core_utils_1.unreachableCase)(type);
    }
}
function applyMovedChanges(mark, revision, manager) {
    // Although this is a source mark, we query the destination because this was a destination mark during the original invert pass.
    const entry = manager.get(modular_schema_1.CrossFieldTarget.Destination, mark.revision ?? revision, mark.id, mark.count, true);
    if (entry.length < mark.count) {
        const [mark1, mark2] = (0, utils_1.splitMark)(mark, entry.length);
        const mark1WithChanges = entry.value !== undefined
            ? (0, utils_1.withNodeChange)(mark1, entry.value)
            : mark1;
        return [mark1WithChanges, ...applyMovedChanges(mark2, revision, manager)];
    }
    if (entry.value !== undefined) {
        return [
            (0, utils_1.withNodeChange)(mark, entry.value),
        ];
    }
    return [mark];
}
function invertNodeChangeOrSkip(length, changes, inverter, detachEvent) {
    if (changes !== undefined) {
        (0, core_utils_1.assert)(length === 1, 0x66c /* A modify mark must have length equal to one */);
        const noop = {
            count: 1,
            changes: inverter(changes),
        };
        if (detachEvent !== undefined) {
            noop.cellId = detachEvent;
        }
        return noop;
    }
    return { count: detachEvent === undefined ? length : 0 };
}
function invertNodeChange(change, inverter) {
    return change === undefined ? undefined : inverter(change);
}
//# sourceMappingURL=invert.js.map