"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.revertMergeTreeDeltaRevertibles = exports.discardMergeTreeDeltaRevertible = exports.appendToMergeTreeDeltaRevertibles = exports.isMergeTreeDeltaRevertible = void 0;
const internal_1 = require("@fluidframework/core-utils/internal");
const internal_2 = require("@fluidframework/telemetry-utils/internal");
const endOfTreeSegment_js_1 = require("./endOfTreeSegment.js");
const localReference_js_1 = require("./localReference.js");
const mergeTree_js_1 = require("./mergeTree.js");
const mergeTreeNodeWalk_js_1 = require("./mergeTreeNodeWalk.js");
const mergeTreeNodes_js_1 = require("./mergeTreeNodes.js");
const mergeTreeTracking_js_1 = require("./mergeTreeTracking.js");
const ops_js_1 = require("./ops.js");
const properties_js_1 = require("./properties.js");
const referencePositions_js_1 = require("./referencePositions.js");
const segmentInfos_js_1 = require("./segmentInfos.js");
/**
 * Tests whether x is a MergeTreeDeltaRevertible
 * @internal
 */
function isMergeTreeDeltaRevertible(x) {
    return !!x && typeof x === "object" && "operation" in x && "trackingGroup" in x;
}
exports.isMergeTreeDeltaRevertible = isMergeTreeDeltaRevertible;
function findMergeTreeWithRevert(trackable) {
    const segmentOrNode = trackable.isLeaf() ? trackable : trackable.getSegment();
    const maybeRoot = (0, mergeTree_js_1.findRootMergeBlock)((0, mergeTreeNodes_js_1.toSegmentLeaf)(segmentOrNode));
    (0, internal_1.assert)(maybeRoot?.mergeTree !== undefined, 0x5c2 /* trackable is invalid as it is not in a rooted merge tree. */);
    const mergeTree = maybeRoot.mergeTree;
    if (mergeTree.__mergeTreeRevertible === undefined) {
        const detachedReferences = new endOfTreeSegment_js_1.EndOfTreeSegment(maybeRoot.mergeTree);
        const refCallbacks = {
            afterSlide: (r) => {
                if (mergeTree.referencePositionToLocalPosition(r) === referencePositions_js_1.DetachedReferencePosition) {
                    const refs = localReference_js_1.LocalReferenceCollection.setOrGet(detachedReferences);
                    refs.addAfterTombstones([r]);
                }
            },
        };
        mergeTree.__mergeTreeRevertible = {
            refCallbacks,
            detachedReferences,
        };
    }
    return mergeTree;
}
function appendLocalInsertToRevertibles(deltaArgs, revertibles) {
    if (revertibles[revertibles.length - 1]?.operation !== ops_js_1.MergeTreeDeltaType.INSERT) {
        revertibles.push({
            operation: ops_js_1.MergeTreeDeltaType.INSERT,
            trackingGroup: new mergeTreeTracking_js_1.UnorderedTrackingGroup(),
        });
    }
    const last = revertibles[revertibles.length - 1];
    for (const t of deltaArgs.deltaSegments)
        last.trackingGroup.link(t.segment);
    return revertibles;
}
function appendLocalRemoveToRevertibles(deltaArgs, revertibles) {
    if (revertibles[revertibles.length - 1]?.operation !== ops_js_1.MergeTreeDeltaType.REMOVE) {
        revertibles.push({
            operation: ops_js_1.MergeTreeDeltaType.REMOVE,
            trackingGroup: new mergeTreeTracking_js_1.UnorderedTrackingGroup(),
        });
    }
    const last = revertibles[revertibles.length - 1];
    const mergeTreeWithRevert = findMergeTreeWithRevert(deltaArgs.deltaSegments[0].segment);
    for (const t of deltaArgs.deltaSegments) {
        const props = {
            segSpec: t.segment.toJSONObject(),
            referenceSpace: "mergeTreeDeltaRevertible",
        };
        const ref = mergeTreeWithRevert.createLocalReferencePosition(t.segment, 0, ops_js_1.ReferenceType.SlideOnRemove, props);
        ref.callbacks = mergeTreeWithRevert.__mergeTreeRevertible.refCallbacks;
        for (const tg of t.segment.trackingCollection.trackingGroups) {
            tg.link(ref);
            tg.unlink(t.segment);
        }
        last.trackingGroup.link(ref);
    }
    return revertibles;
}
function appendLocalAnnotateToRevertibles(deltaArgs, revertibles) {
    let last = revertibles[revertibles.length - 1];
    for (const ds of deltaArgs.deltaSegments) {
        const propertyDeltas = ds.propertyDeltas;
        if (propertyDeltas) {
            if (last?.operation === ops_js_1.MergeTreeDeltaType.ANNOTATE &&
                (0, properties_js_1.matchProperties)(last?.propertyDeltas, propertyDeltas)) {
                last.trackingGroup.link(ds.segment);
            }
            else {
                last = {
                    operation: ops_js_1.MergeTreeDeltaType.ANNOTATE,
                    propertyDeltas,
                    trackingGroup: new mergeTreeTracking_js_1.UnorderedTrackingGroup(),
                };
                last.trackingGroup.link(ds.segment);
                revertibles.push(last);
            }
        }
    }
    return revertibles;
}
/**
 * Appends a merge tree delta to the list of revertibles.
 *
 * @legacy
 * @alpha
 */
function appendToMergeTreeDeltaRevertibles(deltaArgs, revertibles) {
    if (deltaArgs.deltaSegments.length === 0) {
        return;
    }
    switch (deltaArgs.operation) {
        case ops_js_1.MergeTreeDeltaType.INSERT: {
            appendLocalInsertToRevertibles(deltaArgs, revertibles);
            break;
        }
        case ops_js_1.MergeTreeDeltaType.REMOVE: {
            appendLocalRemoveToRevertibles(deltaArgs, revertibles);
            break;
        }
        case ops_js_1.MergeTreeDeltaType.ANNOTATE: {
            appendLocalAnnotateToRevertibles(deltaArgs, revertibles);
            break;
        }
        default: {
            throw new internal_2.UsageError("Unsupported event delta type", {
                operation: deltaArgs.operation,
            });
        }
    }
}
exports.appendToMergeTreeDeltaRevertibles = appendToMergeTreeDeltaRevertibles;
/**
 * Removes all revertibles from the list of revertibles.
 *
 * @legacy
 * @alpha
 */
function discardMergeTreeDeltaRevertible(revertibles) {
    for (const r of revertibles) {
        for (const t of r.trackingGroup.tracked) {
            t.trackingCollection.unlink(r.trackingGroup);
            // remove untracked local references
            if (t.trackingCollection.empty && !t.isLeaf()) {
                const segment = t.getSegment();
                segment?.localRefs?.removeLocalRef(t);
            }
        }
    }
}
exports.discardMergeTreeDeltaRevertible = discardMergeTreeDeltaRevertible;
function revertLocalInsert(driver, mergeTreeWithRevert, revertible) {
    while (revertible.trackingGroup.size > 0) {
        const tracked = revertible.trackingGroup.tracked[0];
        (0, internal_1.assert)(tracked.trackingCollection.unlink(revertible.trackingGroup), 0x3f1 /* tracking group removed */);
        (0, internal_1.assert)((0, mergeTreeNodes_js_1.isSegmentLeaf)(tracked), 0x3f2 /* inserts must track segments */);
        if ((0, segmentInfos_js_1.toRemovalInfo)(tracked) === undefined) {
            const start = getPosition(mergeTreeWithRevert, tracked);
            driver.removeRange(start, start + tracked.cachedLength);
        }
    }
}
function revertLocalRemove(driver, mergeTreeWithRevert, revertible) {
    while (revertible.trackingGroup.size > 0) {
        const tracked = revertible.trackingGroup.tracked[0];
        (0, internal_1.assert)(tracked.trackingCollection.unlink(revertible.trackingGroup), 0x3f3 /* tracking group removed */);
        (0, internal_1.assert)(!tracked.isLeaf(), 0x3f4 /* removes must track local refs */);
        const refSeg = tracked.getSegment();
        let realPos = mergeTreeWithRevert.referencePositionToLocalPosition(tracked);
        // References which are on EndOfStringSegment don't return detached for pos,
        // they will return the length of the merge-tree. this case just catches
        // random references, likely not created in the revertible flow,
        // that are tying to be reverted for some reason.
        if (realPos === referencePositions_js_1.DetachedReferencePosition || !(0, mergeTreeNodes_js_1.isSegmentLeaf)(refSeg)) {
            throw new internal_2.UsageError("Cannot insert at detached references position");
        }
        if ((0, segmentInfos_js_1.toRemovalInfo)(refSeg) === undefined && refSeg.localRefs?.isAfterTombstone(tracked)) {
            realPos++;
        }
        const props = tracked.properties;
        driver.insertFromSpec(realPos, props.segSpec);
        const insertSegment = mergeTreeWithRevert.getContainingSegment(realPos, mergeTreeWithRevert.localPerspective)?.segment;
        (0, mergeTreeNodes_js_1.assertSegmentLeaf)(insertSegment);
        const localSlideFilter = (lref) => lref.properties?.referenceSpace ===
            "mergeTreeDeltaRevertible";
        const insertRef = {};
        const forward = insertSegment.ordinal < refSeg.ordinal;
        const refHandler = (lref) => {
            // once we reach it keep the original reference where it is
            // we'll move tracking groups, and remove it as a last step.
            if (tracked === lref) {
                return false;
            }
            if (localSlideFilter(lref)) {
                if (forward) {
                    const before = (insertRef.before ??= new internal_1.DoublyLinkedList());
                    before.push(lref);
                }
                else {
                    const after = (insertRef.after ??= new internal_1.DoublyLinkedList());
                    after.unshift(lref);
                }
            }
        };
        (0, mergeTreeNodeWalk_js_1.depthFirstNodeWalk)(insertSegment.parent, insertSegment, undefined, (seg) => {
            if (seg.localRefs?.empty === false) {
                return seg.localRefs.walkReferences(refHandler, undefined, forward);
            }
            return true;
        }, undefined, forward);
        if (mergeTreeWithRevert?.__mergeTreeRevertible?.detachedReferences?.localRefs?.has(tracked)) {
            (0, internal_1.assert)(forward, 0x3f6 /* forward should always be true when detached */);
            mergeTreeWithRevert?.__mergeTreeRevertible.detachedReferences.localRefs.walkReferences(refHandler);
        }
        if (insertRef !== undefined) {
            const localRefs = localReference_js_1.LocalReferenceCollection.setOrGet(insertSegment);
            if (insertRef.before?.empty === false) {
                localRefs.addBeforeTombstones(insertRef.before.map((n) => n.data));
            }
            if (insertRef.after?.empty === false) {
                localRefs.addAfterTombstones(insertRef.after.map((n) => n.data));
            }
        }
        for (const tg of tracked.trackingCollection.trackingGroups) {
            tg.link(insertSegment);
            tg.unlink(tracked);
        }
        const segment = tracked.getSegment();
        segment?.localRefs?.removeLocalRef(tracked);
    }
}
function revertLocalAnnotate(driver, mergeTreeWithRevert, revertible) {
    while (revertible.trackingGroup.size > 0) {
        const tracked = revertible.trackingGroup.tracked[0];
        const unlinked = tracked.trackingCollection.unlink(revertible.trackingGroup);
        (0, internal_1.assert)(unlinked && (0, mergeTreeNodes_js_1.isSegmentLeaf)(tracked), 0x3f7 /* annotates must track segments */);
        if ((0, segmentInfos_js_1.toRemovalInfo)(tracked) === undefined) {
            const start = getPosition(mergeTreeWithRevert, tracked);
            driver.annotateRange(start, start + tracked.cachedLength, revertible.propertyDeltas);
        }
    }
}
function getPosition(mergeTreeWithRevert, segment) {
    return mergeTreeWithRevert.getPosition(segment, mergeTreeWithRevert.localPerspective);
}
/**
 * Reverts all operations in the list of revertibles.
 *
 * @legacy
 * @alpha
 */
function revertMergeTreeDeltaRevertibles(driver, revertibles) {
    let mergeTreeWithRevert;
    while (revertibles.length > 0) {
        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
        const r = revertibles.pop();
        const operation = r.operation;
        if (r.trackingGroup.size > 0) {
            mergeTreeWithRevert ??= findMergeTreeWithRevert(r.trackingGroup.tracked[0]);
            switch (operation) {
                case ops_js_1.MergeTreeDeltaType.INSERT: {
                    revertLocalInsert(driver, mergeTreeWithRevert, r);
                    break;
                }
                case ops_js_1.MergeTreeDeltaType.REMOVE: {
                    revertLocalRemove(driver, mergeTreeWithRevert, r);
                    break;
                }
                case ops_js_1.MergeTreeDeltaType.ANNOTATE: {
                    revertLocalAnnotate(driver, mergeTreeWithRevert, r);
                    break;
                }
                default: {
                    (0, internal_1.unreachableCase)(operation);
                }
            }
        }
    }
}
exports.revertMergeTreeDeltaRevertibles = revertMergeTreeDeltaRevertibles;
//# sourceMappingURL=revertibles.js.map