/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { assert, unreachableCase } from "@fluidframework/core-utils";
import { UsageError } from "@fluidframework/telemetry-utils";
import { DoublyLinkedList } from "./collections/index.mjs";
import { EndOfTreeSegment } from "./endOfTreeSegment.mjs";
import { LocalReferenceCollection } from "./localReference.mjs";
import { toRemovalInfo } from "./mergeTreeNodes.mjs";
import { depthFirstNodeWalk } from "./mergeTreeNodeWalk.mjs";
import { UnorderedTrackingGroup } from "./mergeTreeTracking.mjs";
import { MergeTreeDeltaType, ReferenceType } from "./ops.mjs";
import { matchProperties } from "./properties.mjs";
import { DetachedReferencePosition } from "./referencePositions.mjs";
import { findRootMergeBlock } from "./mergeTree.mjs";
/**
 * 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(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 = (detachedReferences.localRefs ?? (detachedReferences.localRefs = new LocalReferenceCollection(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];
    deltaArgs.deltaSegments.forEach((t) => 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);
    deltaArgs.deltaSegments.forEach((t) => {
        const props = {
            segSpec: t.segment.toJSONObject(),
            referenceSpace: "mergeTreeDeltaRevertible",
        };
        const ref = mergeTreeWithRevert.createLocalReferencePosition(t.segment, 0, ReferenceType.SlideOnRemove, props);
        ref.callbacks = mergeTreeWithRevert.__mergeTreeRevertible.refCallbacks;
        t.segment.trackingCollection.trackingGroups.forEach((tg) => {
            tg.link(ref);
            tg.unlink(t.segment);
        });
        last.trackingGroup.link(ref);
    });
    return revertibles;
}
function appendLocalAnnotateToRevertibles(deltaArgs, revertibles) {
    let last = revertibles[revertibles.length - 1];
    deltaArgs.deltaSegments.forEach((ds) => {
        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;
}
/**
 * @internal
 */
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,
            });
    }
}
/**
 * @internal
 */
export function discardMergeTreeDeltaRevertible(revertibles) {
    revertibles.forEach((r) => {
        r.trackingGroup.tracked.forEach((t) => {
            t.trackingCollection.unlink(r.trackingGroup);
            // remove untracked local references
            if (t.trackingCollection.empty && !t.isLeaf()) {
                t.getSegment()?.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(tracked.isLeaf(), 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 || refSeg === undefined) {
            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.collabWindow.currentSeq, mergeTreeWithRevert.collabWindow.clientId).segment;
        assert(insertSegment !== undefined, 0x3f5 /* insert segment must exist at position */);
        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 ?? (insertRef.before = new DoublyLinkedList()));
                    before.push(lref);
                }
                else {
                    const after = (insertRef.after ?? (insertRef.after = new DoublyLinkedList()));
                    after.unshift(lref);
                }
            }
        };
        depthFirstNodeWalk(
        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
        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 = (insertSegment.localRefs ?? (insertSegment.localRefs = new LocalReferenceCollection(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));
            }
        }
        tracked.trackingCollection.trackingGroups.forEach((tg) => {
            tg.link(insertSegment);
            tg.unlink(tracked);
        });
        tracked.getSegment()?.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 && tracked.isLeaf(), 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.collabWindow.currentSeq, mergeTreeWithRevert.collabWindow.clientId);
}
/**
 * @internal
 */
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 ?? (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.mjs.map