"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.amendInvert = 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 format_1 = require("./format");
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, reviver, genId, crossFieldManager) {
    return invertMarkList(change.change, change.revision, reviver, invertChild, crossFieldManager);
}
exports.invert = invert;
function amendInvert(invertedChange, originalRevision, reviver, genId, crossFieldManager) {
    return amendMarkList(invertedChange, originalRevision, crossFieldManager);
}
exports.amendInvert = amendInvert;
function invertMarkList(markList, revision, reviver, invertChild, crossFieldManager) {
    const inverseMarkList = new markListFactory_1.MarkListFactory();
    let inputIndex = 0;
    for (const mark of markList) {
        const inverseMarks = invertMark(mark, inputIndex, revision, reviver, invertChild, crossFieldManager);
        inverseMarkList.push(...inverseMarks);
        inputIndex += (0, utils_1.getInputLength)(mark);
    }
    return inverseMarkList.list;
}
function invertMark(mark, inputIndex, revision, reviver, invertChild, crossFieldManager) {
    switch (mark.type) {
        case format_1.NoopMarkType: {
            const inverse = { ...mark };
            if (mark.changes !== undefined) {
                if (mark.cellId === undefined) {
                    inverse.changes = invertChild(mark.changes, inputIndex);
                }
                else {
                    // TODO: preserve modifications to the removed nodes.
                    delete inverse.changes;
                }
            }
            return [inverse];
        }
        case "Insert": {
            (0, core_utils_1.assert)(mark.cellId !== undefined, 0x72c /* Insert marks must have a cellId */);
            if (mark.transientDetach !== undefined) {
                (0, core_utils_1.assert)(revision !== undefined, 0x720 /* Unable to revert to undefined revision */);
                return [
                    (0, utils_1.withNodeChange)({
                        type: "Revive",
                        cellId: {
                            revision: mark.transientDetach.revision ?? revision,
                            localId: mark.transientDetach.localId,
                        },
                        content: reviver(revision, inputIndex, mark.content.length),
                        count: mark.content.length,
                        inverseOf: mark.revision ?? revision,
                        transientDetach: {
                            revision: mark.cellId.revision ?? revision,
                            localId: mark.cellId.localId,
                        },
                    }, invertNodeChange(mark.changes, inputIndex, invertChild)),
                ];
            }
            else {
                const inverse = (0, utils_1.withNodeChange)({ type: "Delete", count: mark.content.length, id: mark.cellId.localId }, invertNodeChange(mark.changes, inputIndex, invertChild));
                return [inverse];
            }
        }
        case "Delete": {
            (0, core_utils_1.assert)(revision !== undefined, 0x5a1 /* Unable to revert to undefined revision */);
            const markRevision = mark.revision ?? revision;
            const inverseRevision = mark.detachIdOverride?.revision ?? markRevision;
            if (mark.cellId === undefined) {
                const inverse = (0, utils_1.withNodeChange)({
                    type: "Revive",
                    cellId: mark.detachIdOverride ?? {
                        revision: markRevision,
                        localId: mark.id,
                    },
                    content: reviver(inverseRevision, inputIndex, mark.count),
                    count: mark.count,
                    inverseOf: inverseRevision,
                }, invertNodeChange(mark.changes, inputIndex, invertChild));
                return [inverse];
            }
            // TODO: preserve modifications to the removed nodes.
            return [];
        }
        case "Revive": {
            if (!(0, utils_1.isReattachConflicted)(mark)) {
                (0, core_utils_1.assert)(mark.cellId !== undefined, 0x707 /* Active reattach should have a detach event */);
                if (mark.transientDetach !== undefined) {
                    (0, core_utils_1.assert)(revision !== undefined, 0x721 /* Unable to revert to undefined revision */);
                    return [
                        (0, utils_1.withNodeChange)({
                            type: "Revive",
                            cellId: {
                                revision: mark.transientDetach.revision ?? revision,
                                localId: mark.transientDetach.localId,
                            },
                            content: reviver(revision, inputIndex, mark.count),
                            count: mark.count,
                            inverseOf: mark.revision ?? revision,
                            transientDetach: {
                                revision: mark.revision ?? revision,
                                localId: mark.cellId.localId,
                            },
                        }, invertNodeChange(mark.changes, inputIndex, invertChild)),
                    ];
                }
                const deleteMark = {
                    type: "Delete",
                    count: mark.count,
                    id: mark.cellId.localId,
                    detachIdOverride: mark.cellId,
                };
                const inverse = (0, utils_1.withNodeChange)(deleteMark, invertNodeChange(mark.changes, inputIndex, invertChild));
                return [inverse];
            }
            return mark.transientDetach !== undefined
                ? invertMark({
                    type: "Delete",
                    count: mark.count,
                    revision: mark.transientDetach.revision ?? revision,
                    changes: mark.changes,
                    id: mark.transientDetach.localId,
                }, inputIndex, revision, reviver, invertChild, crossFieldManager)
                : [
                    invertNodeChangeOrSkip(mark.count, mark.changes, inputIndex, invertChild, mark.cellId),
                ];
        }
        case "MoveOut":
        case "ReturnFrom": {
            if ((0, utils_1.areInputCellsEmpty)(mark)) {
                // TODO: preserve modifications to the removed nodes.
                return [];
            }
            if (mark.type === "ReturnFrom" && mark.isDstConflicted) {
                // The nodes were present but the destination was conflicted, the mark had no effect on the nodes.
                return [invertNodeChangeOrSkip(mark.count, mark.changes, inputIndex, invertChild)];
            }
            if (mark.changes !== undefined) {
                (0, core_utils_1.assert)(mark.count === 1, 0x6ed /* Mark with changes can only target a single cell */);
                crossFieldManager.set(modular_schema_1.CrossFieldTarget.Destination, mark.revision ?? revision, mark.id, mark.count, invertChild(mark.changes, inputIndex), true);
            }
            const cellId = (0, utils_1.getDetachCellId)(mark, mark.revision ?? revision ?? (0, util_1.fail)("Revision must be defined")) ?? {
                revision: mark.revision ?? revision ?? (0, util_1.fail)("Revision must be defined"),
                localId: mark.id,
            };
            return [
                {
                    type: "ReturnTo",
                    id: mark.id,
                    count: mark.count,
                    cellId,
                },
            ];
        }
        case "MoveIn":
        case "ReturnTo": {
            if (mark.isSrcConflicted) {
                return mark.type === "ReturnTo" && mark.cellId === undefined
                    ? [{ count: mark.count }]
                    : [];
            }
            if (mark.type === "ReturnTo") {
                if (mark.cellId === undefined) {
                    // The nodes were already attached, so the mark did not affect them.
                    return [{ count: mark.count }];
                }
                else if ((0, utils_1.isConflictedReattach)(mark)) {
                    // The nodes were not attached and could not be attached.
                    return [];
                }
            }
            const invertedMark = {
                type: "ReturnFrom",
                id: mark.id,
                count: mark.count,
            };
            if (mark.type === "ReturnTo" && mark.cellId !== undefined) {
                invertedMark.detachIdOverride = mark.cellId;
            }
            return applyMovedChanges(invertedMark, revision, crossFieldManager);
        }
        default:
            (0, util_1.fail)("Not implemented");
    }
}
function amendMarkList(marks, revision, crossFieldManager) {
    const factory = new markListFactory_1.MarkListFactory();
    for (const mark of marks) {
        if (mark.type === "MoveOut" || mark.type === "ReturnFrom") {
            factory.push(...applyMovedChanges(mark, revision, crossFieldManager));
        }
        else {
            factory.push(mark);
        }
    }
    return factory.list;
}
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 === undefined) {
        return [mark];
    }
    if (entry.start > mark.id) {
        // The entry does not apply to the first cell in the mark.
        const [mark1, mark2] = (0, utils_1.splitMark)(mark, entry.start - mark.id);
        return [mark1, ...applyMovedChanges(mark2, revision, manager)];
    }
    else if (entry.start + entry.length < mark.id + mark.count) {
        // The entry applies to the first cell in the mark, but not the mark's entire range.
        const [mark1, mark2] = (0, utils_1.splitMark)(mark, entry.start + entry.length - mark.id);
        return [(0, utils_1.withNodeChange)(mark1, entry.value), ...applyMovedChanges(mark2, revision, manager)];
    }
    else {
        // The entry applies to all cells in the mark.
        return [(0, utils_1.withNodeChange)(mark, entry.value)];
    }
}
function invertNodeChangeOrSkip(length, changes, index, 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, index),
        };
        if (detachEvent !== undefined) {
            noop.cellId = detachEvent;
        }
        return noop;
    }
    return { count: detachEvent === undefined ? length : 0 };
}
function invertNodeChange(change, index, inverter) {
    return change === undefined ? undefined : inverter(change, index);
}
//# sourceMappingURL=invert.js.map