"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.compose = void 0;
const util_1 = require("../../util");
const changeset_1 = require("./changeset");
/**
 * 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) {
    if (changes.length === 1) {
        return changes[0].change;
    }
    let composedFieldMarks = {};
    for (const change of changes) {
        composedFieldMarks = composeFieldMarks(composedFieldMarks, change.change.marks);
    }
    return {
        marks: composedFieldMarks,
    };
}
exports.compose = compose;
function composeFieldMarks(baseFieldMarks, newFieldMarks) {
    var _a;
    const composed = {};
    for (const key of Object.keys(newFieldMarks)) {
        const composedMarkList = composeMarkLists((_a = baseFieldMarks[key]) !== null && _a !== void 0 ? _a : [], newFieldMarks[key]);
        if (composedMarkList.length > 0) {
            composed[key] = composedMarkList;
        }
    }
    for (const key of Object.keys(baseFieldMarks)) {
        if (!(key in newFieldMarks)) {
            composed[key] = baseFieldMarks[key];
        }
    }
    return composed;
}
function composeMarkLists(baseMarkList, newMarkList) {
    const factory = new changeset_1.MarkListFactory();
    const baseIter = new util_1.StackyIterator(baseMarkList);
    const newIter = new util_1.StackyIterator(newMarkList);
    for (let newMark of newIter) {
        let baseMark = baseIter.pop();
        if (baseMark === undefined) {
            // We have reached a region of the field that the base change does not affect.
            // We therefore adopt the new mark as is.
            factory.push((0, util_1.clone)(newMark));
        }
        else if ((0, changeset_1.isAttach)(newMark)) {
            // Content that is being attached by the new changeset cannot interact with base changes.
            // Note that attach marks from different changesets can only target the same gap if they are concurrent.
            // This method assumes that `newMarkList` is based on `baseMarkList`, so they are not concurrent.
            factory.pushContent((0, util_1.clone)(newMark));
            baseIter.push(baseMark);
        }
        else if ((0, changeset_1.isReattach)(newMark)) {
            // Content that is being re-attached by the new changeset can interact with base changes.
            // This can happen in two cases:
            // - The base change contains the detach that the re-attach is the inverse of.
            // - The base change contains a tombstone for the detach that the re-attach is the inverse of.
            // We're ignoring these cases for now. The impact of ignoring them is that the relative order of
            // reattached content and concurrently attached content is not preserved.
            // TODO: properly compose reattach marks with their matching base marks if any.
            factory.pushContent((0, util_1.clone)(newMark));
            baseIter.push(baseMark);
        }
        else if ((0, changeset_1.isDetachMark)(baseMark)) {
            // Content that is being detached by the base changeset can interact with the new changes.
            // This can happen in two cases:
            // - The new change contains reattach marks for this detach. (see above)
            // - The new change contains tombs for this detach.
            // We're ignoring these cases for now. The impact of ignoring them is that the relative order of
            // reattached content and concurrently attached content is not preserved.
            // TODO: properly compose detach marks with their matching new marks if any.
            factory.pushContent(baseMark);
            newIter.push(newMark);
        }
        else if ((0, changeset_1.isTomb)(baseMark) ||
            (0, changeset_1.isGapEffectMark)(baseMark) ||
            (0, changeset_1.isTomb)(newMark) ||
            (0, changeset_1.isGapEffectMark)(newMark)) {
            // We don't currently support Tomb and Gap marks (and don't offer ways to generate them).
            (0, util_1.fail)("TODO: support Tomb and Gap marks");
        }
        else {
            // If we've reached this branch then `baseMark` and `newMark` start at the same location
            // in the document field at the revision after the base changes and before the new changes.
            // Despite that, it's not necessarily true that they affect the same range in that document
            // field because they may be of different lengths.
            // We perform any necessary splitting in order to end up with a pair of marks that do have the same length.
            const newMarkLength = (0, changeset_1.getInputLength)(newMark);
            const baseMarkLength = (0, changeset_1.getOutputLength)(baseMark);
            if (newMarkLength < baseMarkLength) {
                let nextBaseMark;
                [baseMark, nextBaseMark] = (0, changeset_1.splitMarkOnOutput)(baseMark, newMarkLength);
                baseIter.push(nextBaseMark);
            }
            else if (newMarkLength > baseMarkLength) {
                let nextNewMark;
                [newMark, nextNewMark] = (0, changeset_1.splitMarkOnInput)(newMark, baseMarkLength);
                newIter.push(nextNewMark);
            }
            // Past this point, we are guaranteed that `newMark` 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, newMark);
            factory.push(composedMark);
        }
    }
    // Push the remaining base marks if any
    for (const baseMark of baseIter) {
        factory.push(baseMark);
    }
    return factory.list;
}
/**
 * Composes two marks where `newMark` is based on the state produced by `baseMark`.
 * @param newMark - The mark to compose with `baseMark`.
 * Its input range should be the same as `baseMark`'s output range.
 * @param baseMark - The mark to compose with `newMark`.
 * Its output range should be the same as `newMark`'s input range.
 * @returns A mark that is equivalent to applying both `baseMark` and `newMark` successively.
 */
function composeMarks(baseMark, newMark) {
    if ((0, changeset_1.isSkipMark)(baseMark)) {
        return (0, util_1.clone)(newMark);
    }
    if ((0, changeset_1.isSkipMark)(newMark)) {
        return baseMark;
    }
    const baseType = baseMark.type;
    const newType = newMark.type;
    if (newType === "MDelete" || baseType === "MDelete") {
        // This should not occur yet because we discard all modifications to deleted subtrees
        // In the long run we want to preserve them.
        (0, util_1.fail)("TODO: support modifications to deleted subtree");
    }
    switch (baseType) {
        case "Insert":
            switch (newType) {
                case "Modify": {
                    return Object.assign(Object.assign({}, newMark), { type: "MInsert", id: baseMark.id, content: baseMark.content[0] });
                }
                case "Delete": {
                    // The insertion made by the base change is subsequently deleted.
                    // TODO: preserve the insertions as muted
                    return 0;
                }
                default:
                    (0, util_1.fail)("Not implemented");
            }
        case "MInsert": {
            switch (newType) {
                case "Modify": {
                    updateModifyLike(newMark, baseMark);
                    return baseMark;
                }
                case "Delete": {
                    // The insertion made by the base change is subsequently deleted.
                    // TODO: preserve the insertions as muted
                    return 0;
                }
                default:
                    (0, util_1.fail)("Not implemented");
            }
        }
        case "Modify": {
            switch (newType) {
                case "Modify": {
                    updateModifyLike(newMark, baseMark);
                    if (baseMark.fields === undefined && baseMark.value === undefined) {
                        return 1;
                    }
                    return baseMark;
                }
                case "Delete": {
                    // For now the deletion obliterates all other modifications.
                    // In the long run we want to preserve them.
                    return (0, util_1.clone)(newMark);
                }
                default:
                    (0, util_1.fail)("Not implemented");
            }
        }
        case "Revive": {
            switch (newType) {
                case "Modify": {
                    const modRevive = {
                        type: "MRevive",
                        id: baseMark.id,
                        tomb: baseMark.tomb,
                    };
                    updateModifyLike(newMark, modRevive);
                    return modRevive;
                }
                case "Delete": {
                    // The deletion undoes the revival
                    return 0;
                }
                default:
                    (0, util_1.fail)("Not implemented");
            }
        }
        default:
            (0, util_1.fail)("Not implemented");
    }
}
function updateModifyLike(curr, base) {
    var _a;
    if (curr.fields !== undefined) {
        base.fields = composeFieldMarks((_a = base.fields) !== null && _a !== void 0 ? _a : {}, curr.fields);
        if (Object.keys(base.fields).length === 0) {
            delete base.fields;
        }
    }
    if (curr.value !== undefined) {
        // Later values override earlier ones
        base.value = (0, util_1.clone)(curr.value);
    }
}
//# sourceMappingURL=compose.js.map