/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { assert, unreachableCase, DoublyLinkedList, } from "@fluidframework/core-utils/internal";
import { UsageError } from "@fluidframework/telemetry-utils/internal";
import { EndOfTreeSegment } from "./endOfTreeSegment.js";
import { LocalReferenceCollection } from "./localReference.js";
import { findRootMergeBlock } from "./mergeTree.js";
import { depthFirstNodeWalk } from "./mergeTreeNodeWalk.js";
import { assertSegmentLeaf, isSegmentLeaf, toSegmentLeaf, } from "./mergeTreeNodes.js";
import { UnorderedTrackingGroup, } from "./mergeTreeTracking.js";
import { MergeTreeDeltaType, ReferenceType } from "./ops.js";
import { matchProperties } from "./properties.js";
import { DetachedReferencePosition } from "./referencePositions.js";
import { toRemovalInfo } from "./segmentInfos.js";
/**
 * Tests whether x is a MergeTreeDeltaRevertible
 * @internal
 */
export function isMergeTreeDeltaRevertible(x) {
    return !!x && typeof x === "object" && "operation" in x && "trackingGroup" in x;
}
function findMergeTreeWithRevert(trackable) {
    const segmentOrNode = trackable.isLeaf() ? trackable : trackable.getSegment();
    const maybeRoot = findRootMergeBlock(toSegmentLeaf(segmentOrNode));
    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(maybeRoot.mergeTree);
        const refCallbacks = {
            afterSlide: (r) => {
                if (mergeTree.referencePositionToLocalPosition(r) === DetachedReferencePosition) {
                    const refs = LocalReferenceCollection.setOrGet(detachedReferences);
                    refs.addAfterTombstones([r]);
                }
            },
        };
        mergeTree.__mergeTreeRevertible = {
            refCallbacks,
            detachedReferences,
        };
    }
    return mergeTree;
}
function appendLocalInsertToRevertibles(deltaArgs, revertibles) {
    if (revertibles[revertibles.length - 1]?.operation !== MergeTreeDeltaType.INSERT) {
        revertibles.push({
            operation: MergeTreeDeltaType.INSERT,
            trackingGroup: new 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 !== MergeTreeDeltaType.REMOVE) {
        revertibles.push({
            operation: MergeTreeDeltaType.REMOVE,
            trackingGroup: new 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, 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 === MergeTreeDeltaType.ANNOTATE &&
                matchProperties(last?.propertyDeltas, propertyDeltas)) {
                last.trackingGroup.link(ds.segment);
            }
            else {
                last = {
                    operation: MergeTreeDeltaType.ANNOTATE,
                    propertyDeltas,
                    trackingGroup: new UnorderedTrackingGroup(),
                };
                last.trackingGroup.link(ds.segment);
                revertibles.push(last);
            }
        }
    }
    return revertibles;
}
/**
 * Appends a merge tree delta to the list of revertibles.
 *
 * @legacy @beta
 */
export function appendToMergeTreeDeltaRevertibles(deltaArgs, revertibles) {
    if (deltaArgs.deltaSegments.length === 0) {
        return;
    }
    switch (deltaArgs.operation) {
        case MergeTreeDeltaType.INSERT: {
            appendLocalInsertToRevertibles(deltaArgs, revertibles);
            break;
        }
        case MergeTreeDeltaType.REMOVE: {
            appendLocalRemoveToRevertibles(deltaArgs, revertibles);
            break;
        }
        case MergeTreeDeltaType.ANNOTATE: {
            appendLocalAnnotateToRevertibles(deltaArgs, revertibles);
            break;
        }
        default: {
            throw new UsageError("Unsupported event delta type", {
                operation: deltaArgs.operation,
            });
        }
    }
}
/**
 * Removes all revertibles from the list of revertibles.
 *
 * @legacy @beta
 */
export 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);
            }
        }
    }
}
function revertLocalInsert(driver, mergeTreeWithRevert, revertible) {
    while (revertible.trackingGroup.size > 0) {
        const tracked = revertible.trackingGroup.tracked[0];
        assert(tracked.trackingCollection.unlink(revertible.trackingGroup), 0x3f1 /* tracking group removed */);
        assert(isSegmentLeaf(tracked), 0x3f2 /* inserts must track segments */);
        if (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];
        assert(tracked.trackingCollection.unlink(revertible.trackingGroup), 0x3f3 /* tracking group removed */);
        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 === DetachedReferencePosition || !isSegmentLeaf(refSeg)) {
            throw new UsageError("Cannot insert at detached references position");
        }
        if (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;
        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 DoublyLinkedList());
                    before.push(lref);
                }
                else {
                    const after = (insertRef.after ??= new DoublyLinkedList());
                    after.unshift(lref);
                }
            }
        };
        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)) {
            assert(forward, 0x3f6 /* forward should always be true when detached */);
            mergeTreeWithRevert?.__mergeTreeRevertible.detachedReferences.localRefs.walkReferences(refHandler);
        }
        if (insertRef !== undefined) {
            const localRefs = 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);
        assert(unlinked && isSegmentLeaf(tracked), 0x3f7 /* annotates must track segments */);
        if (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 @beta
 */
export 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 MergeTreeDeltaType.INSERT: {
                    revertLocalInsert(driver, mergeTreeWithRevert, r);
                    break;
                }
                case MergeTreeDeltaType.REMOVE: {
                    revertLocalRemove(driver, mergeTreeWithRevert, r);
                    break;
                }
                case MergeTreeDeltaType.ANNOTATE: {
                    revertLocalAnnotate(driver, mergeTreeWithRevert, r);
                    break;
                }
                default: {
                    unreachableCase(operation);
                }
            }
        }
    }
}
//# sourceMappingURL=revertibles.js.map