"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.MergeTree = exports.getSlideToSegoff = exports.findRootMergeBlock = void 0;
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable no-bitwise */
const core_utils_1 = require("@fluidframework/core-utils");
const telemetry_utils_1 = require("@fluidframework/telemetry-utils");
const collections_1 = require("./collections/index.cjs");
const constants_1 = require("./constants.cjs");
const localReference_1 = require("./localReference.cjs");
const mergeTreeNodes_1 = require("./mergeTreeNodes.cjs");
const mergeTreeDeltaCallback_1 = require("./mergeTreeDeltaCallback.cjs");
const opBuilder_1 = require("./opBuilder.cjs");
const ops_1 = require("./ops.cjs");
const partialLengths_1 = require("./partialLengths.cjs");
// eslint-disable-next-line import/no-deprecated
const properties_1 = require("./properties.cjs");
const referencePositions_1 = require("./referencePositions.cjs");
const segmentPropertiesManager_1 = require("./segmentPropertiesManager.cjs");
const mergeTreeNodeWalk_1 = require("./mergeTreeNodeWalk.cjs");
const zamboni_1 = require("./zamboni.cjs");
const endOfTreeSegment_1 = require("./endOfTreeSegment.cjs");
function wasRemovedAfter(seg, seq) {
    return (seg.removedSeq !== constants_1.UnassignedSequenceNumber &&
        (seg.removedSeq === undefined || seg.removedSeq > seq));
}
function markSegmentMoved(seg, moveInfo) {
    seg.moveDst = moveInfo.moveDst;
    seg.movedClientIds = moveInfo.movedClientIds.slice();
    seg.movedSeqs = [moveInfo.movedSeq];
    seg.movedSeq = moveInfo.movedSeq;
    seg.localMovedSeq = moveInfo.localMovedSeq;
    seg.wasMovedOnInsert = moveInfo.wasMovedOnInsert;
}
function isMoved(segment) {
    return (0, mergeTreeNodes_1.toMoveInfo)(segment) !== undefined;
}
function isRemoved(segment) {
    return (0, mergeTreeNodes_1.toRemovalInfo)(segment) !== undefined;
}
function isRemovedAndAcked(segment) {
    const removalInfo = (0, mergeTreeNodes_1.toRemovalInfo)(segment);
    return removalInfo !== undefined && removalInfo.removedSeq !== constants_1.UnassignedSequenceNumber;
}
function isMovedAndAcked(segment) {
    const moveInfo = (0, mergeTreeNodes_1.toMoveInfo)(segment);
    return moveInfo !== undefined && moveInfo.movedSeq !== constants_1.UnassignedSequenceNumber;
}
function isRemovedAndAckedOrMovedAndAcked(segment) {
    return isRemovedAndAcked(segment) || isMovedAndAcked(segment);
}
function nodeTotalLength(mergeTree, node) {
    if (!node.isLeaf()) {
        return node.cachedLength;
    }
    return mergeTree.localNetLength(node);
}
const LRUSegmentComparer = {
    min: { maxSeq: -2 },
    compare: (a, b) => a.maxSeq - b.maxSeq,
};
function recordTileStart(segment, segpos, refSeq, clientId, start, end, searchInfo) {
    if (mergeTreeNodes_1.Marker.is(segment)) {
        if ((0, referencePositions_1.refHasTileLabel)(segment, searchInfo.tileLabel)) {
            searchInfo.tile = segment;
        }
    }
    return false;
}
function tileShift(node, segpos, refSeq, clientId, offset, end, searchInfo) {
    if (node.isLeaf()) {
        const seg = node;
        if ((searchInfo.mergeTree.localNetLength(seg) ?? 0) > 0 && mergeTreeNodes_1.Marker.is(seg)) {
            if ((0, referencePositions_1.refHasTileLabel)(seg, searchInfo.tileLabel)) {
                searchInfo.tile = seg;
            }
        }
    }
    else {
        const block = node;
        const marker = searchInfo.tilePrecedesPos
            ? block.rightmostTiles[searchInfo.tileLabel]
            : block.leftmostTiles[searchInfo.tileLabel];
        if (marker !== undefined) {
            (0, core_utils_1.assert)(marker.isLeaf() && mergeTreeNodes_1.Marker.is(marker), 0x868 /* Object returned is not a valid marker */);
            searchInfo.tile = marker;
        }
    }
    return true;
}
function addTile(tile, tiles) {
    const tileLabels = (0, referencePositions_1.refGetTileLabels)(tile);
    if (tileLabels) {
        for (const tileLabel of tileLabels) {
            tiles[tileLabel] = tile;
        }
    }
}
function addTileIfNotPresent(tile, tiles) {
    const tileLabels = (0, referencePositions_1.refGetTileLabels)(tile);
    if (tileLabels) {
        for (const tileLabel of tileLabels) {
            if (tiles[tileLabel] === undefined) {
                tiles[tileLabel] = tile;
            }
        }
    }
}
class HierMergeBlock extends mergeTreeNodes_1.MergeBlock {
    constructor(childCount) {
        super(childCount);
        // eslint-disable-next-line import/no-deprecated
        this.rightmostTiles = (0, properties_1.createMap)();
        // eslint-disable-next-line import/no-deprecated
        this.leftmostTiles = (0, properties_1.createMap)();
    }
    hierBlock() {
        return this;
    }
}
function findRootMergeBlock(segmentOrNode) {
    if (segmentOrNode === undefined) {
        return undefined;
    }
    let maybeRoot = segmentOrNode.isLeaf()
        ? segmentOrNode.parent
        : segmentOrNode;
    while (maybeRoot?.parent !== undefined) {
        maybeRoot = maybeRoot.parent;
    }
    return maybeRoot?.mergeTree !== undefined ? maybeRoot : undefined;
}
exports.findRootMergeBlock = findRootMergeBlock;
/**
 * @param segment - The segment to slide from.
 * @param cache - Optional cache mapping segments to their sliding destinations.
 * Excursions will be avoided for segments in the cache, and the cache will be populated with
 * entries for all segments visited during excursion.
 * This can reduce the number of times the tree needs to be scanned if a range containing many
 * SlideOnRemove references is removed.
 * @returns The segment a SlideOnRemove reference should slide to, or undefined if there is no
 * valid segment (i.e. the tree is empty).
 * @internal
 */
function getSlideToSegment(segment, slidingPreference = localReference_1.SlidingPreference.FORWARD, cache, useNewSlidingBehavior = false) {
    if (!segment ||
        !isRemovedAndAckedOrMovedAndAcked(segment) ||
        segment.endpointType !== undefined) {
        return [segment, undefined];
    }
    const cachedSegment = cache?.get(segment);
    if (cachedSegment !== undefined) {
        return [cachedSegment.seg, undefined];
    }
    const result = {};
    cache?.set(segment, result);
    const goFurtherToFindSlideToSegment = (seg) => {
        if (seg.seq !== constants_1.UnassignedSequenceNumber && !isRemovedAndAckedOrMovedAndAcked(seg)) {
            result.seg = seg;
            return false;
        }
        if (cache !== undefined &&
            (seg.removedSeq === segment.removedSeq || seg.movedSeq === segment.movedSeq)) {
            cache.set(seg, result);
        }
        return true;
    };
    if (slidingPreference === localReference_1.SlidingPreference.BACKWARD) {
        (0, mergeTreeNodeWalk_1.backwardExcursion)(segment, goFurtherToFindSlideToSegment);
    }
    else {
        (0, mergeTreeNodeWalk_1.forwardExcursion)(segment, goFurtherToFindSlideToSegment);
    }
    if (result.seg !== undefined) {
        return [result.seg, undefined];
    }
    // in the new sliding behavior, we don't look in the opposite direction
    // if we fail to find a segment to slide to in the right direction.
    //
    // in other words, rather than going `forward ?? backward ?? detached` (or
    // `backward ?? forward ?? detached`), we would slide `forward ?? detached`
    // or `backward ?? detached`
    //
    // in both of these cases detached may be substituted for one of the special
    // endpoint segments, if such behavior is enabled
    if (!useNewSlidingBehavior) {
        if (slidingPreference === localReference_1.SlidingPreference.BACKWARD) {
            (0, mergeTreeNodeWalk_1.forwardExcursion)(segment, goFurtherToFindSlideToSegment);
        }
        else {
            (0, mergeTreeNodeWalk_1.backwardExcursion)(segment, goFurtherToFindSlideToSegment);
        }
    }
    let maybeEndpoint;
    if (slidingPreference === localReference_1.SlidingPreference.BACKWARD) {
        maybeEndpoint = "start";
    }
    else if (slidingPreference === localReference_1.SlidingPreference.FORWARD) {
        maybeEndpoint = "end";
    }
    return [result.seg, maybeEndpoint];
}
/**
 * Returns the position to slide a reference to if a slide is required.
 * @param segoff - The segment and offset to slide from
 * @returns segment and offset to slide the reference to
 * @internal
 */
function getSlideToSegoff(segoff, slidingPreference = localReference_1.SlidingPreference.FORWARD, useNewSlidingBehavior = false) {
    if (segoff.segment === undefined) {
        return segoff;
    }
    const [segment, _] = getSlideToSegment(segoff.segment, slidingPreference, undefined, useNewSlidingBehavior);
    if (segment === segoff.segment) {
        return segoff;
    }
    const offset = segment && segment.ordinal < segoff.segment.ordinal ? segment.cachedLength - 1 : 0;
    return {
        segment,
        offset,
    };
}
exports.getSlideToSegoff = getSlideToSegoff;
/**
 * @internal
 */
class MergeTree {
    constructor(options) {
        this.options = options;
        // eslint-disable-next-line import/no-deprecated
        this.collabWindow = new mergeTreeNodes_1.CollaborationWindow();
        // eslint-disable-next-line import/no-deprecated
        this.pendingSegments = new collections_1.DoublyLinkedList();
        this.segmentsToScour = new core_utils_1.Heap(LRUSegmentComparer);
        /**
         * Whether or not all blocks in the mergeTree currently have information about local partial lengths computed.
         * This information is only necessary on reconnect, and otherwise costly to bookkeep.
         * This field enables tracking whether partials need to be recomputed using localSeq information.
         */
        this.localPartialsComputed = false;
        // for now assume only markers have ids and so point directly at the Segment
        // if we need to have pointers to non-markers, we can change to point at local refs
        this.idToMarker = new Map();
        /**
         * Array containing the sequence number of all move operations within the
         * collab window
         *
         * When a segment is inserted, we must traverse to the left and right of it
         * to determine whether the segment was inserted into an obliterated range.
         * By keeping track of all move seqs, we can significantly reduce the search
         * space we must traverse.
         *
         * Sequence numbers in `moveSeqs` are sorted to accelerate bookkeeping.
         *
         * See https://github.com/microsoft/FluidFramework/blob/main/packages/dds/merge-tree/docs/Obliterate.md#remote-perspective
         * for additional context
         */
        this.moveSeqs = [];
        /**
         * Similar to moveSeqs, but tracks local moves. These are not the move
         * operations within the collab window, but rather local moves that have
         * not been acked.
         */
        this.localMoveSeqs = new Set();
        /**
         * Groups of segments moved by local moves/obliterates
         *
         * When a local obliterate is acked, we must also ack segments that were
         * concurrently obliterated on insert. We check this segment group to find
         * such segments
         */
        // eslint-disable-next-line import/no-deprecated
        this.locallyMovedSegments = new Map();
        this.splitLeafSegment = (segment, pos) => {
            if (!(pos > 0 && segment)) {
                return {};
            }
            const next = segment.splitAt(pos);
            this.mergeTreeMaintenanceCallback?.({
                operation: mergeTreeDeltaCallback_1.MergeTreeMaintenanceType.SPLIT,
                deltaSegments: [{ segment }, { segment: next }],
            }, undefined);
            return { next };
        };
        this.startOfTree = new endOfTreeSegment_1.StartOfTreeSegment(this);
        this.endOfTree = new endOfTreeSegment_1.EndOfTreeSegment(this);
        this._root = this.makeBlock(0);
        this._root.mergeTree = this;
        this.attributionPolicy = options?.attribution?.policyFactory?.();
    }
    get root() {
        return this._root;
    }
    set root(value) {
        this._root = value;
        value.mergeTree = this;
    }
    makeBlock(childCount) {
        const block = new HierMergeBlock(childCount);
        block.ordinal = "";
        return block;
    }
    /**
     * Compute the net length of this segment from a local perspective.
     * @param segment - Segment whose length to find
     * @param localSeq - localSeq at which to find the length of this segment. If not provided,
     * default is to consider the local client's current perspective. Only local sequence
     * numbers corresponding to un-acked operations give valid results.
     */
    localNetLength(segment, refSeq, localSeq) {
        const removalInfo = (0, mergeTreeNodes_1.toRemovalInfo)(segment);
        const moveInfo = (0, mergeTreeNodes_1.toMoveInfo)(segment);
        if (localSeq === undefined) {
            if (removalInfo !== undefined || moveInfo !== undefined) {
                if ((!!removalInfo && !(0, mergeTreeNodes_1.seqLTE)(removalInfo.removedSeq, this.collabWindow.minSeq)) ||
                    (!!moveInfo && !(0, mergeTreeNodes_1.seqLTE)(moveInfo.movedSeq, this.collabWindow.minSeq))) {
                    return 0;
                }
                // this segment removed and outside the collab window which means it is zamboni eligible
                // this also means the segment could not exist, so we should not consider it
                // when making decisions about conflict resolutions
                return undefined;
            }
            else {
                return segment.cachedLength;
            }
        }
        (0, core_utils_1.assert)(refSeq !== undefined, 0x398 /* localSeq provided for local length without refSeq */);
        (0, core_utils_1.assert)(segment.seq !== undefined, 0x399 /* segment with no seq in mergeTree */);
        const { seq, removedSeq, localRemovedSeq, movedSeq, localMovedSeq } = segment;
        if (seq !== constants_1.UnassignedSequenceNumber) {
            // inserted remotely
            if (seq > refSeq ||
                (removedSeq !== undefined &&
                    removedSeq !== constants_1.UnassignedSequenceNumber &&
                    removedSeq <= refSeq) ||
                (movedSeq !== undefined &&
                    movedSeq !== constants_1.UnassignedSequenceNumber &&
                    movedSeq <= refSeq) ||
                (localRemovedSeq !== undefined && localRemovedSeq <= localSeq) ||
                (localMovedSeq !== undefined && localMovedSeq <= localSeq)) {
                return 0;
            }
            return segment.cachedLength;
        }
        else {
            (0, core_utils_1.assert)(segment.localSeq !== undefined, 0x39a /* unacked segment with undefined localSeq */);
            // inserted locally, still un-acked
            if (segment.localSeq > localSeq ||
                (localRemovedSeq !== undefined && localRemovedSeq <= localSeq) ||
                (localMovedSeq !== undefined && localMovedSeq <= localSeq)) {
                return 0;
            }
            return segment.cachedLength;
        }
    }
    unlinkMarker(marker) {
        const id = marker.getId();
        if (id) {
            this.idToMarker.delete(id);
        }
    }
    addNode(block, node) {
        const index = block.childCount++;
        block.assignChild(node, index, false);
        return index;
    }
    reloadFromSegments(segments) {
        // This code assumes that a later call to `startCollaboration()` will initialize partial lengths.
        (0, core_utils_1.assert)(!this.collabWindow.collaborating, 0x049 /* "Trying to reload from segments while collaborating!" */);
        const maxChildren = mergeTreeNodes_1.MaxNodesInBlock - 1;
        // Starting with the leaf segments, recursively builds the B-Tree layer by layer from the bottom up.
        const buildMergeBlock = (nodes) => {
            const blockCount = Math.ceil(nodes.length / maxChildren); // Compute # blocks require for this level of B-Tree
            const blocks = new Array(blockCount); // Pre-alloc array to collect nodes
            // For each block in this level of the B-Tree...
            for (let nodeIndex = 0, blockIndex = 0; // Start with the first block and first node
             blockIndex < blockCount; // If we have more blocks, we also have more nodes to insert
             blockIndex++ // Advance to next block in this layer.
            ) {
                const block = (blocks[blockIndex] = this.makeBlock(0));
                // For each child of the current block, insert a node (while we have nodes left)
                // and update the block's info.
                for (let childIndex = 0; childIndex < maxChildren && nodeIndex < nodes.length; // While we still have children & nodes left
                 childIndex++, nodeIndex++ // Advance to next child & node
                ) {
                    // Insert the next node into the current block
                    this.addNode(block, nodes[nodeIndex]);
                }
                // Calculate this block's info.  Previously this was inlined into the above loop as a micro-optimization,
                // but it turns out to be negligible in practice since `reloadFromSegments()` is only invoked for the
                // snapshot header.  The bulk of the segments in long documents are inserted via `insertSegments()`.
                this.blockUpdate(block);
            }
            return blocks.length === 1 // If there is only one block at this layer...
                ? blocks[0] // ...then we're done.  Return the root.
                : buildMergeBlock(blocks); // ...otherwise recursively build the next layer above blocks.
        };
        if (segments.length > 0) {
            this.root = buildMergeBlock(segments);
            this.nodeUpdateOrdinals(this.root);
        }
        else {
            this.root = this.makeBlock(0);
        }
    }
    // For now assume min starts at zero
    startCollaboration(localClientId, minSeq, currentSeq) {
        this.collabWindow.clientId = localClientId;
        this.collabWindow.minSeq = minSeq;
        this.collabWindow.collaborating = true;
        this.collabWindow.currentSeq = currentSeq;
        this.nodeUpdateLengthNewStructure(this.root, true);
    }
    addToLRUSet(leaf, seq) {
        // If the parent node has not yet been marked for scour (i.e., needsScour is not false or undefined),
        // add the segment and mark the mark the node now.
        // TODO: 'seq' may be less than the current sequence number when inserting pre-ACKed
        //       segments from a snapshot.  We currently skip these for now.
        if (leaf.parent.needsScour !== true && seq > this.collabWindow.currentSeq) {
            leaf.parent.needsScour = true;
            this.segmentsToScour.add({ segment: leaf, maxSeq: seq });
        }
    }
    getLength(refSeq, clientId) {
        return this.blockLength(this.root, refSeq, clientId);
    }
    /**
     * Returns the current length of the MergeTree for the local client.
     */
    get length() {
        return this.root.cachedLength;
    }
    getPosition(node, refSeq, clientId, localSeq) {
        if (node.isLeaf() && node.endpointType === "start") {
            return 0;
        }
        let totalOffset = 0;
        let parent = node.parent;
        let prevParent;
        while (parent) {
            const children = parent.children;
            for (let childIndex = 0; childIndex < parent.childCount; childIndex++) {
                const child = children[childIndex];
                if ((!!prevParent && child === prevParent) || child === node) {
                    break;
                }
                totalOffset += this.nodeLength(child, refSeq, clientId, localSeq) ?? 0;
            }
            prevParent = parent;
            parent = parent.parent;
        }
        return totalOffset;
    }
    getContainingSegment(pos, refSeq, clientId, localSeq) {
        (0, core_utils_1.assert)(localSeq === undefined || clientId === this.collabWindow.clientId, 0x39b /* localSeq provided for non-local client */);
        let segment;
        let offset;
        const leaf = (leafSeg, segpos, _refSeq, _clientId, start) => {
            segment = leafSeg;
            offset = start;
            return false;
        };
        this.nodeMap(refSeq, clientId, leaf, undefined, undefined, pos, pos + 1, localSeq);
        return { segment, offset };
    }
    /**
     * Slides or removes references from the provided list of segments.
     *
     * The order of the references is preserved for references of the same sliding
     * preference. Relative order between references that slide backward and those
     * that slide forward is not preserved, even in the case when they slide to
     * the same segment.
     *
     * @remarks
     *
     * 1. Preserving the order of the references is a useful property for reference-based undo/redo
     * (see revertibles.ts).
     *
     * 2. For use cases which necessitate eventual consistency across clients,
     * this method should only be called with segments for which the current client sequence number is
     * max(remove segment sequence number, add reference sequence number).
     * See `packages\dds\merge-tree\REFERENCEPOSITIONS.md`
     *
     * @param segments - An array of (not necessarily contiguous) segments with increasing ordinals.
     */
    slideAckedRemovedSegmentReferences(segments) {
        // References are slid in groups to preserve their order.
        let currentForwardSlideGroup = [];
        let currentBackwardSlideGroup = [];
        let currentForwardMaybeEndpoint;
        let currentForwardSlideDestination;
        let currentForwardSlideIsForward;
        const forwardPred = (ref) => ref.slidingPreference !== localReference_1.SlidingPreference.BACKWARD;
        let currentBackwardMaybeEndpoint;
        let currentBackwardSlideDestination;
        let currentBackwardSlideIsForward;
        const backwardPred = (ref) => ref.slidingPreference === localReference_1.SlidingPreference.BACKWARD;
        const slideGroup = (currentSlideDestination, currentSlideIsForward, currentSlideGroup, pred, maybeEndpoint) => {
            if (currentSlideIsForward === undefined) {
                return;
            }
            const nonEndpointRefsToAdd = currentSlideGroup.map((collection) => (0, localReference_1.filterLocalReferencePositions)(collection, (ref) => pred(ref) && (maybeEndpoint ? !ref.canSlideToEndpoint : true)));
            const endpointRefsToAdd = currentSlideGroup.map((collection) => (0, localReference_1.filterLocalReferencePositions)(collection, (ref) => pred(ref) && !!ref.canSlideToEndpoint));
            if (maybeEndpoint) {
                const endpoint = maybeEndpoint === "start" ? this.startOfTree : this.endOfTree;
                const localRefs = (endpoint.localRefs ?? (endpoint.localRefs = new localReference_1.LocalReferenceCollection(endpoint)));
                if (currentSlideIsForward) {
                    localRefs.addBeforeTombstones(...endpointRefsToAdd);
                }
                else {
                    localRefs.addAfterTombstones(...endpointRefsToAdd);
                }
            }
            if (currentSlideDestination !== undefined) {
                const localRefs = (currentSlideDestination.localRefs ?? (currentSlideDestination.localRefs = new localReference_1.LocalReferenceCollection(currentSlideDestination)));
                if (currentSlideIsForward) {
                    localRefs.addBeforeTombstones(...nonEndpointRefsToAdd);
                }
                else {
                    localRefs.addAfterTombstones(...nonEndpointRefsToAdd);
                }
            }
            else {
                for (const collection of currentSlideGroup) {
                    for (const ref of collection) {
                        if (pred(ref) && !(0, referencePositions_1.refTypeIncludesFlag)(ref, ops_1.ReferenceType.StayOnRemove)) {
                            ref.callbacks?.beforeSlide?.(ref);
                            collection.removeLocalRef(ref);
                            ref.callbacks?.afterSlide?.(ref);
                        }
                    }
                }
            }
        };
        const trySlideSegment = (segment, currentSlideDestination, currentSlideIsForward, currentSlideGroup, pred, slidingPreference, currentMaybeEndpoint, reassign) => {
            // avoid sliding logic if this segment doesn't have any references
            // with the given sliding preference
            if (!segment.localRefs || !(0, localReference_1.anyLocalReferencePosition)(segment.localRefs, pred)) {
                return;
            }
            const [slideToSegment, maybeEndpoint] = getSlideToSegment(segment, slidingPreference, slidingPreference === localReference_1.SlidingPreference.FORWARD
                ? forwardSegmentCache
                : backwardSegmentCache, this.options?.mergeTreeReferencesCanSlideToEndpoint);
            const slideIsForward = slideToSegment === undefined ? false : slideToSegment.ordinal > segment.ordinal;
            if (slideToSegment !== currentSlideDestination ||
                slideIsForward !== currentSlideIsForward ||
                maybeEndpoint !== currentMaybeEndpoint) {
                slideGroup(currentSlideDestination, currentSlideIsForward, currentSlideGroup, pred, this.options?.mergeTreeReferencesCanSlideToEndpoint ? maybeEndpoint : undefined);
                reassign(segment.localRefs, slideToSegment, slideIsForward, this.options?.mergeTreeReferencesCanSlideToEndpoint ? maybeEndpoint : undefined);
            }
            else {
                currentSlideGroup.push(segment.localRefs);
            }
        };
        const forwardSegmentCache = new Map();
        const backwardSegmentCache = new Map();
        for (const segment of segments) {
            (0, core_utils_1.assert)(isRemovedAndAckedOrMovedAndAcked(segment), 0x2f1 /* slideReferences from a segment which has not been removed and acked */);
            if (segment.localRefs === undefined || segment.localRefs.empty) {
                continue;
            }
            trySlideSegment(segment, currentForwardSlideDestination, currentForwardSlideIsForward, currentForwardSlideGroup, forwardPred, localReference_1.SlidingPreference.FORWARD, currentForwardMaybeEndpoint, (localRefs, slideToSegment, slideIsForward, maybeEndpoint) => {
                currentForwardSlideGroup = [localRefs];
                currentForwardSlideDestination = slideToSegment;
                currentForwardSlideIsForward = slideIsForward;
                currentForwardMaybeEndpoint = maybeEndpoint;
            });
            trySlideSegment(segment, currentBackwardSlideDestination, currentBackwardSlideIsForward, currentBackwardSlideGroup, backwardPred, localReference_1.SlidingPreference.BACKWARD, currentBackwardMaybeEndpoint, (localRefs, slideToSegment, slideIsForward, maybeEndpoint) => {
                currentBackwardSlideGroup = [localRefs];
                currentBackwardSlideDestination = slideToSegment;
                currentBackwardSlideIsForward = slideIsForward;
                currentBackwardMaybeEndpoint = maybeEndpoint;
            });
        }
        slideGroup(currentForwardSlideDestination, currentForwardSlideIsForward, currentForwardSlideGroup, forwardPred, currentForwardMaybeEndpoint);
        slideGroup(currentBackwardSlideDestination, currentBackwardSlideIsForward, currentBackwardSlideGroup, backwardPred, currentBackwardMaybeEndpoint);
    }
    blockLength(node, refSeq, clientId) {
        return this.collabWindow.collaborating && clientId !== this.collabWindow.clientId
            ? node.partialLengths.getPartialLength(refSeq, clientId)
            : node.cachedLength ?? 0;
    }
    /**
     * Compute local partial length information
     *
     * Public only for use by internal tests
     */
    computeLocalPartials(refSeq) {
        if (this.localPartialsComputed) {
            return;
        }
        // eslint-disable-next-line import/no-deprecated
        const rebaseCollabWindow = new mergeTreeNodes_1.CollaborationWindow();
        rebaseCollabWindow.loadFrom(this.collabWindow);
        if (refSeq < this.collabWindow.minSeq) {
            rebaseCollabWindow.minSeq = refSeq;
        }
        this.root.partialLengths = partialLengths_1.PartialSequenceLengths.combine(this.root, rebaseCollabWindow, true, true);
        this.localPartialsComputed = true;
    }
    nodeLength(node, refSeq, clientId, localSeq) {
        if (!this.collabWindow.collaborating || this.collabWindow.clientId === clientId) {
            if (node.isLeaf()) {
                return this.localNetLength(node, refSeq, localSeq);
            }
            else if (localSeq === undefined) {
                // Local client sees all segments, even when collaborating
                return node.cachedLength;
            }
            else {
                this.computeLocalPartials(refSeq);
                // Local client should see all segments except those after localSeq.
                const partialLen = node.partialLengths.getPartialLength(refSeq, clientId, localSeq);
                partialLengths_1.PartialSequenceLengths.options.verifyExpected?.(this, node, refSeq, clientId, localSeq);
                return partialLen;
            }
        }
        else {
            // Sequence number within window
            if (!node.isLeaf()) {
                const partialLen = node.partialLengths.getPartialLength(refSeq, clientId);
                partialLengths_1.PartialSequenceLengths.options.verifyExpected?.(this, node, refSeq, clientId);
                return partialLen;
            }
            else {
                const segment = node;
                const removalInfo = (0, mergeTreeNodes_1.toRemovalInfo)(segment);
                const moveInfo = (0, mergeTreeNodes_1.toMoveInfo)(segment);
                if (removalInfo !== undefined) {
                    if ((0, mergeTreeNodes_1.seqLTE)(removalInfo.removedSeq, this.collabWindow.minSeq)) {
                        return undefined;
                    }
                    if ((0, mergeTreeNodes_1.seqLTE)(removalInfo.removedSeq, refSeq) ||
                        removalInfo.removedClientIds.includes(clientId)) {
                        return 0;
                    }
                }
                if (moveInfo !== undefined) {
                    if ((0, mergeTreeNodes_1.seqLTE)(moveInfo.movedSeq, this.collabWindow.minSeq)) {
                        return undefined;
                    }
                    if ((0, mergeTreeNodes_1.seqLTE)(moveInfo.movedSeq, refSeq) ||
                        moveInfo.movedClientIds.includes(clientId)) {
                        return 0;
                    }
                }
                return (0, mergeTreeNodes_1.seqLTE)(node.seq ?? 0, refSeq) || segment.clientId === clientId
                    ? segment.cachedLength
                    : 0;
            }
        }
    }
    setMinSeq(minSeq) {
        (0, core_utils_1.assert)(minSeq <= this.collabWindow.currentSeq, 0x04e /* "Trying to set minSeq above currentSeq of collab window!" */);
        // Only move forward
        (0, core_utils_1.assert)(this.collabWindow.minSeq <= minSeq, 0x04f /* "minSeq of collab window > target minSeq!" */);
        if (minSeq > this.collabWindow.minSeq) {
            this.collabWindow.minSeq = minSeq;
            const firstMoveSeqIdx = this.moveSeqs.findIndex((seq) => seq >= minSeq);
            this.moveSeqs = firstMoveSeqIdx === -1 ? [] : this.moveSeqs.slice(firstMoveSeqIdx);
            if (MergeTree.options.zamboniSegments) {
                (0, zamboni_1.zamboniSegments)(this);
            }
        }
    }
    referencePositionToLocalPosition(refPos, refSeq = this.collabWindow.currentSeq, clientId = this.collabWindow.clientId) {
        const seg = refPos.getSegment();
        if (seg?.parent === undefined) {
            return referencePositions_1.DetachedReferencePosition;
        }
        if (refPos.isLeaf()) {
            return this.getPosition(refPos, refSeq, clientId);
        }
        if ((0, referencePositions_1.refTypeIncludesFlag)(refPos, ops_1.ReferenceType.Transient) || seg.localRefs?.has(refPos)) {
            const offset = isRemoved(seg) || isMoved(seg) ? 0 : refPos.getOffset();
            return offset + this.getPosition(seg, refSeq, clientId);
        }
        return referencePositions_1.DetachedReferencePosition;
    }
    // TODO: filter function
    /**
     * Finds the nearest reference with ReferenceType.Tile to `startPos` in the direction dictated by `tilePrecedesPos`.
     * @deprecated Use searchForMarker instead.
     *
     * @param startPos - Position at which to start the search
     * @param clientId - clientId dictating the perspective to search from
     * @param tileLabel - Label of the tile to search for
     * @param tilePrecedesPos - Whether the desired tile comes before (true) or after (false) `startPos`
     */
    findTile(startPos, clientId, tileLabel, tilePrecedesPos = true) {
        const searchInfo = {
            mergeTree: this,
            tilePrecedesPos,
            tileLabel,
        };
        if (tilePrecedesPos) {
            this.search(startPos, constants_1.UniversalSequenceNumber, clientId, { leaf: recordTileStart, shift: tileShift }, searchInfo);
        }
        else {
            this.backwardSearch(startPos, constants_1.UniversalSequenceNumber, clientId, { leaf: recordTileStart, shift: tileShift }, searchInfo);
        }
        if (searchInfo.tile) {
            let pos;
            if (searchInfo.tile.isLeaf()) {
                const marker = searchInfo.tile;
                pos = this.getPosition(marker, constants_1.UniversalSequenceNumber, clientId);
            }
            else {
                const localRef = searchInfo.tile;
                pos = this.referencePositionToLocalPosition(localRef, constants_1.UniversalSequenceNumber, clientId);
            }
            return { tile: searchInfo.tile, pos };
        }
        return undefined;
    }
    /**
     * Finds the nearest reference with ReferenceType.Tile to `startPos` in the direction dictated by `forwards`.
     * Uses depthFirstNodeWalk in addition to block-accelerated functionality. The search position will be included in
     * the nodes to walk, so searching on all positions, including the endpoints, can be considered inclusive.
     * Any out of bound search positions will return undefined, so in order to search the whole string, a forward
     * search can begin at 0, or a backward search can begin at length-1.
     *
     * @param startPos - Position at which to start the search
     * @param clientId - clientId dictating the perspective to search from
     * @param markerLabel - Label of the marker to search for
     * @param forwards - Whether the string should be searched in the forward or backward direction
     */
    searchForMarker(startPos, clientId, markerLabel, forwards = true) {
        let foundMarker;
        const { segment } = this.getContainingSegment(startPos, constants_1.UniversalSequenceNumber, clientId);
        const segWithParent = segment;
        if (segWithParent?.parent === undefined) {
            return undefined;
        }
        (0, mergeTreeNodeWalk_1.depthFirstNodeWalk)(segWithParent.parent, segWithParent, (seg) => {
            if (seg.isLeaf()) {
                if (mergeTreeNodes_1.Marker.is(seg) && (0, referencePositions_1.refHasTileLabel)(seg, markerLabel)) {
                    foundMarker = seg;
                }
            }
            else {
                const block = seg;
                const marker = forwards
                    ? block.leftmostTiles[markerLabel]
                    : block.rightmostTiles[markerLabel];
                if (marker !== undefined) {
                    (0, core_utils_1.assert)(marker.isLeaf() && mergeTreeNodes_1.Marker.is(marker), 0x751 /* Object returned is not a valid marker */);
                    foundMarker = marker;
                }
            }
            return foundMarker !== undefined ? mergeTreeNodeWalk_1.NodeAction.Exit : mergeTreeNodeWalk_1.NodeAction.Skip;
        }, undefined, undefined, forwards);
        return foundMarker;
    }
    search(pos, refSeq, clientId, actions, clientData) {
        return this.searchBlock(this.root, pos, 0, refSeq, clientId, actions, clientData);
    }
    searchBlock(block, pos, segpos, refSeq, clientId, actions, clientData, localSeq) {
        const { pre, post, contains, leaf, shift } = actions ?? {};
        let _pos = pos;
        let _segpos = segpos;
        const children = block.children;
        pre?.(block, _segpos, refSeq, clientId, undefined, undefined, clientData);
        for (let childIndex = 0; childIndex < block.childCount; childIndex++) {
            const child = children[childIndex];
            const len = this.nodeLength(child, refSeq, clientId, localSeq) ?? 0;
            if ((!contains && _pos < len) ||
                contains?.(child, _pos, refSeq, clientId, undefined, undefined, clientData)) {
                // Found entry containing pos
                if (!child.isLeaf()) {
                    return this.searchBlock(child, _pos, _segpos, refSeq, clientId, actions, clientData, localSeq);
                }
                else {
                    leaf?.(child, _segpos, refSeq, clientId, _pos, -1, clientData);
                    return child;
                }
            }
            else {
                shift?.(child, _segpos, refSeq, clientId, _pos, undefined, clientData);
                _pos -= len;
                _segpos += len;
            }
        }
        post?.(block, _segpos, refSeq, clientId, undefined, undefined, clientData);
    }
    backwardSearch(pos, refSeq, clientId, actions, clientData) {
        const len = this.getLength(refSeq, clientId);
        if (pos > len) {
            return undefined;
        }
        return this.backwardSearchBlock(this.root, pos, len, refSeq, clientId, actions, clientData);
    }
    backwardSearchBlock(block, pos, segEnd, refSeq, clientId, actions, clientData) {
        const { pre, post, contains, leaf, shift } = actions ?? {};
        let _segEnd = segEnd;
        const children = block.children;
        pre?.(block, _segEnd, refSeq, clientId, undefined, undefined, clientData);
        for (let childIndex = block.childCount - 1; childIndex >= 0; childIndex--) {
            const child = children[childIndex];
            const len = this.nodeLength(child, refSeq, clientId) ?? 0;
            const segpos = _segEnd - len;
            if ((!contains && pos >= segpos) ||
                contains?.(child, pos, refSeq, clientId, undefined, undefined, clientData)) {
                // Found entry containing pos
                if (!child.isLeaf()) {
                    return this.backwardSearchBlock(child, pos, _segEnd, refSeq, clientId, actions, clientData);
                }
                else {
                    leaf?.(child, segpos, refSeq, clientId, pos, -1, clientData);
                    return child;
                }
            }
            else {
                shift?.(child, segpos, refSeq, clientId, pos, undefined, clientData);
                _segEnd = segpos;
            }
        }
        post?.(block, _segEnd, refSeq, clientId, undefined, undefined, clientData);
    }
    updateRoot(splitNode) {
        if (splitNode !== undefined) {
            const newRoot = this.makeBlock(2);
            newRoot.assignChild(this.root, 0, false);
            newRoot.assignChild(splitNode, 1, false);
            this.root = newRoot;
            this.nodeUpdateOrdinals(this.root);
            this.nodeUpdateLengthNewStructure(this.root);
        }
    }
    /**
     * Assign sequence number to existing segment; update partial lengths to reflect the change
     * @param seq - sequence number given by server to pending segment
     */
    ackPendingSegment(opArgs) {
        const seq = opArgs.sequencedMessage.sequenceNumber;
        const pendingSegmentGroup = this.pendingSegments.shift()?.data;
        const nodesToUpdate = [];
        let overwrite = false;
        if (pendingSegmentGroup !== undefined) {
            const deltaSegments = [];
            const overlappingRemoves = [];
            pendingSegmentGroup.segments.map((pendingSegment) => {
                const localMovedSeq = pendingSegment.localMovedSeq;
                const overlappingRemove = !pendingSegment.ack(pendingSegmentGroup, opArgs);
                if (opArgs.op.type === ops_1.MergeTreeDeltaType.OBLITERATE &&
                    localMovedSeq !== undefined) {
                    const locallyMovedSegments = this.locallyMovedSegments.get(localMovedSeq);
                    if (locallyMovedSegments) {
                        for (const segment of locallyMovedSegments.segments) {
                            segment.localMovedSeq = undefined;
                            if (!nodesToUpdate.includes(segment.parent)) {
                                nodesToUpdate.push(segment.parent);
                            }
                            if (segment.movedSeq === constants_1.UnassignedSequenceNumber) {
                                segment.movedSeq = seq;
                            }
                        }
                        this.locallyMovedSegments.delete(localMovedSeq);
                    }
                }
                overwrite = overlappingRemove || overwrite;
                if (opArgs.op.type === ops_1.MergeTreeDeltaType.OBLITERATE) {
                    if (seq !== this.moveSeqs[this.moveSeqs.length - 1]) {
                        this.moveSeqs.push(seq);
                    }
                    if (localMovedSeq !== undefined) {
                        this.localMoveSeqs.delete(localMovedSeq);
                    }
                }
                overlappingRemoves.push(overlappingRemove);
                if (MergeTree.options.zamboniSegments) {
                    this.addToLRUSet(pendingSegment, seq);
                }
                if (!nodesToUpdate.includes(pendingSegment.parent)) {
                    nodesToUpdate.push(pendingSegment.parent);
                }
                deltaSegments.push({
                    segment: pendingSegment,
                });
            });
            // Perform slides after all segments have been acked, so that
            // positions after slide are final
            if (opArgs.op.type === ops_1.MergeTreeDeltaType.REMOVE ||
                opArgs.op.type === ops_1.MergeTreeDeltaType.OBLITERATE) {
                this.slideAckedRemovedSegmentReferences(pendingSegmentGroup.segments);
            }
            this.mergeTreeMaintenanceCallback?.({
                deltaSegments,
                operation: mergeTreeDeltaCallback_1.MergeTreeMaintenanceType.ACKNOWLEDGED,
            }, opArgs);
            const clientId = this.collabWindow.clientId;
            for (const node of nodesToUpdate) {
                this.blockUpdatePathLengths(node, seq, clientId, overwrite);
            }
        }
        if (MergeTree.options.zamboniSegments) {
            (0, zamboni_1.zamboniSegments)(this);
        }
    }
    addToPendingList(segment, 
    // eslint-disable-next-line import/no-deprecated
    segmentGroup, localSeq, previousProps) {
        let _segmentGroup = segmentGroup;
        if (_segmentGroup === undefined) {
            _segmentGroup = {
                segments: [],
                localSeq,
                refSeq: this.collabWindow.currentSeq,
            };
            if (previousProps) {
                _segmentGroup.previousProps = [];
            }
            this.pendingSegments.push(_segmentGroup);
        }
        if ((!_segmentGroup.previousProps && !!previousProps) ||
            (!!_segmentGroup.previousProps && !previousProps)) {
            throw new Error("All segments in group should have previousProps or none");
        }
        if (previousProps) {
            _segmentGroup.previousProps.push(previousProps);
        }
        segment.segmentGroups.enqueue(_segmentGroup);
        return _segmentGroup;
    }
    // TODO: error checking
    getMarkerFromId(id) {
        return this.idToMarker.get(id);
    }
    /**
     * Given a position specified relative to a marker id, lookup the marker
     * and convert the position to a character position.
     * @param relativePos - Id of marker (may be indirect) and whether position is before or after marker.
     * @param refseq - The reference sequence number at which to compute the position.
     * @param clientId - The client id with which to compute the position.
     */
    posFromRelativePos(relativePos, refseq = this.collabWindow.currentSeq, clientId = this.collabWindow.clientId) {
        let pos = -1;
        let marker;
        if (relativePos.id) {
            marker = this.getMarkerFromId(relativePos.id);
        }
        if (marker) {
            pos = this.getPosition(marker, refseq, clientId);
            if (!relativePos.before) {
                pos += marker.cachedLength;
                if (relativePos.offset !== undefined) {
                    pos += relativePos.offset;
                }
            }
            else {
                if (relativePos.offset !== undefined) {
                    pos -= relativePos.offset;
                }
            }
        }
        return pos;
    }
    insertSegments(pos, segments, refSeq, clientId, seq, opArgs) {
        this.ensureIntervalBoundary(pos, refSeq, clientId);
        const localSeq = seq === constants_1.UnassignedSequenceNumber ? ++this.collabWindow.localSeq : undefined;
        this.blockInsert(pos, refSeq, clientId, seq, localSeq, segments);
        // opArgs == undefined => loading snapshot or test code
        if (opArgs !== undefined) {
            this.mergeTreeDeltaCallback?.(opArgs, {
                operation: ops_1.MergeTreeDeltaType.INSERT,
                deltaSegments: segments.map((segment) => ({ segment })),
            });
        }
        if (this.collabWindow.collaborating &&
            MergeTree.options.zamboniSegments &&
            seq !== constants_1.UnassignedSequenceNumber) {
            (0, zamboni_1.zamboniSegments)(this);
        }
    }
    /**
     * Resolves a remote client's position against the local sequence
     * and returns the remote client's position relative to the local
     * sequence. The client ref seq must be above the minimum sequence number
     * or the return value will be undefined.
     * Generally this method is used in conjunction with signals which provide
     * point in time values for the below parameters, and is useful for things
     * like displaying user position. It should not be used with persisted values
     * as persisted values will quickly become invalid as the remoteClientRefSeq
     * moves below the minimum sequence number
     * @param remoteClientPosition - The remote client's position to resolve
     * @param remoteClientRefSeq - The reference sequence number of the remote client
     * @param remoteClientId - The client id of the remote client
     */
    resolveRemoteClientPosition(remoteClientPosition, remoteClientRefSeq, remoteClientId) {
        if (remoteClientRefSeq < this.collabWindow.minSeq) {
            return undefined;
        }
        const segmentInfo = this.getContainingSegment(remoteClientPosition, remoteClientRefSeq, remoteClientId);
        const { currentSeq, clientId } = this.collabWindow;
        if (segmentInfo?.segment) {
            const segmentPosition = this.getPosition(segmentInfo.segment, currentSeq, clientId);
            return segmentPosition + segmentInfo.offset;
        }
        else {
            if (remoteClientPosition === this.getLength(remoteClientRefSeq, remoteClientId)) {
                return this.getLength(currentSeq, clientId);
            }
        }
    }
    blockInsert(pos, refSeq, clientId, seq, localSeq, newSegments) {
        const continueFrom = (node) => {
            let siblingExists = false;
            (0, mergeTreeNodeWalk_1.forwardExcursion)(node, () => {
                siblingExists = true;
                return false;
            });
            return siblingExists;
        };
        // eslint-disable-next-line import/no-deprecated
        let segmentGroup;
        const saveIfLocal = (locSegment) => {
            // Save segment so we can assign sequence number when acked by server
            if (this.collabWindow.collaborating) {
                if (locSegment.seq === constants_1.UnassignedSequenceNumber &&
                    clientId === this.collabWindow.clientId) {
                    segmentGroup = this.addToPendingList(locSegment, segmentGroup, localSeq);
                }
                // LocSegment.seq === 0 when coming from SharedSegmentSequence.loadBody()
                // In all other cases this has to be true (checked by addToLRUSet):
                // locSegment.seq > this.collabWindow.currentSeq
                else if (locSegment.seq > this.collabWindow.minSeq &&
                    MergeTree.options.zamboniSegments) {
                    this.addToLRUSet(locSegment, locSegment.seq);
                }
            }
        };
        const onLeaf = (segment, _pos, context) => {
            const segmentChanges = {};
            if (segment) {
                // Insert before segment
                segmentChanges.replaceCurrent = context.candidateSegment;
                segmentChanges.next = segment;
            }
            else {
                segmentChanges.next = context.candidateSegment;
            }
            return segmentChanges;
        };
        // TODO: build tree from segs and insert all at once
        let insertPos = pos;
        for (const newSegment of newSegments) {
            if (newSegment.cachedLength > 0) {
                newSegment.seq = seq;
                newSegment.localSeq = localSeq;
                newSegment.clientId = clientId;
                if (mergeTreeNodes_1.Marker.is(newSegment)) {
                    const markerId = newSegment.getId();
                    if (markerId) {
                        this.idToMarker.set(markerId, newSegment);
                    }
                }
                const splitNode = this.insertingWalk(this.root, insertPos, refSeq, clientId, seq, {
                    leaf: onLeaf,
                    candidateSegment: newSegment,
                    continuePredicate: continueFrom,
                });
                if (newSegment.parent === undefined) {
                    // Indicates an attempt to insert past the end of the merge-tree's content.
                    const errorConstructor = localSeq !== undefined ? telemetry_utils_1.UsageError : telemetry_utils_1.DataProcessingError;
                    throw new errorConstructor("MergeTree insert failed", {
                        currentSeq: this.collabWindow.currentSeq,
                        minSeq: this.collabWindow.minSeq,
                        segSeq: newSegment.seq,
                    });
                }
                this.updateRoot(splitNode);
                saveIfLocal(newSegment);
                insertPos += newSegment.cachedLength;
                if (!this.options?.mergeTreeEnableObliterate) {
                    continue;
                }
                let moveUpperBound = Number.POSITIVE_INFINITY;
                const smallestSeqMoveOp = this.getSmallestSeqMoveOp();
                if (smallestSeqMoveOp === undefined) {
                    continue;
                }
                const leftAckedSegments = {};
                const leftLocalSegments = {};
                let _localMovedSeq;
                let _movedSeq;
                let movedClientIds;
                const findLeftMovedSegment = (seg) => {
                    const movedSeqs = seg.movedSeqs?.filter((movedSeq) => movedSeq >= refSeq) ?? [];
                    const localMovedSeqs = seg.localMovedSeq ? [seg.localMovedSeq] : [];
                    for (const movedSeq of movedSeqs) {
                        leftAckedSegments[movedSeq] = seg;
                    }
                    for (const localMovedSeq of localMovedSeqs) {
                        leftLocalSegments[localMovedSeq] = seg;
                    }
                    if ((seg.movedSeqs?.length ?? 0) > 0 || localMovedSeqs.length > 0) {
                        return true;
                    }
                    if (!isRemoved(seg) || wasRemovedAfter(seg, moveUpperBound)) {
                        moveUpperBound = Math.min(moveUpperBound, seg.seq ?? Number.POSITIVE_INFINITY);
                    }
                    // If we've reached a segment that existed before any of our in-collab-window move ops
                    // happened, no need to continue.
                    return moveUpperBound >= smallestSeqMoveOp;
                };
                const findRightMovedSegment = (seg) => {
                    const movedSeqs = seg.movedSeqs?.filter((movedSeq) => movedSeq >= refSeq) ?? [];
                    const localMovedSeqs = seg.localMovedSeq ? [seg.localMovedSeq] : [];
                    for (const movedSeq of movedSeqs) {
                        const left = leftAckedSegments[movedSeq];
                        if (left) {
                            _movedSeq = movedSeq;
                            const clientIdIdx = left.movedSeqs?.indexOf(movedSeq) ?? -1;
                            const movedClientId = left.movedClientIds?.[clientIdIdx];
                            (0, core_utils_1.assert)(movedClientId !== undefined, 0x869 /* expected client id to exist */);
                            movedClientIds = [movedClientId];
                            return false;
                        }
                    }
                    for (const localMovedSeq of localMovedSeqs) {
                        const left = leftLocalSegments[localMovedSeq];
                        if (left) {
                            _localMovedSeq = localMovedSeq;
                            const clientIdIdx = left.movedSeqs?.indexOf(constants_1.UnassignedSequenceNumber) ?? -1;
                            const movedClientId = left.movedClientIds?.[clientIdIdx];
                            (0, core_utils_1.assert)(movedClientId !== undefined, 0x86a /* expected client id to exist */);
                            movedClientIds = [movedClientId];
                            return false;
                        }
                    }
                    if ((seg.movedSeqs?.length ?? 0) || localMovedSeqs.length > 0) {
                        return true;
                    }
                    if (!isRemoved(seg) || wasRemovedAfter(seg, moveUpperBound)) {
                        moveUpperBound = Math.min(moveUpperBound, seg.seq ?? Number.POSITIVE_INFINITY);
                    }
                    // If we've reached a segment that existed before any of our in-collab-window move ops
                    // happened, no need to continue.
                    return moveUpperBound >= smallestSeqMoveOp;
                };
                (0, mergeTreeNodeWalk_1.backwardExcursion)(newSegment, findLeftMovedSegment);
                moveUpperBound = Number.POSITIVE_INFINITY;
                (0, mergeTreeNodeWalk_1.forwardExcursion)(newSegment, findRightMovedSegment);
                if (_localMovedSeq !== undefined || _movedSeq !== undefined) {
                    (0, core_utils_1.assert)(movedClientIds !== undefined, 0x86b /* movedClientIds should be set if local/moved seq is set */);
                    const moveInfo = {
                        movedClientIds,
                        movedSeq: _movedSeq ?? constants_1.UnassignedSequenceNumber,
                        movedSeqs: _movedSeq === undefined ? [constants_1.UnassignedSequenceNumber] : [_movedSeq],
                        localMovedSeq: _localMovedSeq,
                        wasMovedOnInsert: (_movedSeq ?? -1) !== constants_1.UnassignedSequenceNumber,
                    };
                    markSegmentMoved(newSegment, moveInfo);
                    if (moveInfo.localMovedSeq !== undefined) {
                        const movedSegmentGroup = this.locallyMovedSegments.get(moveInfo.localMovedSeq);
                        (0, core_utils_1.assert)(movedSegmentGroup !== undefined, 0x86c /* expected segment group to exist */);
                        this.addToPendingList(newSegment, movedSegmentGroup, localSeq);
                    }
                    if (newSegment.parent) {
                        this.blockUpdatePathLengths(newSegment.parent, seq, clientId);
                    }
                }
            }
        }
    }
    ensureIntervalBoundary(pos, refSeq, clientId) {
        const splitNode = this.insertingWalk(this.root, pos, refSeq, clientId, constants_1.TreeMaintenanceSequenceNumber, { leaf: this.splitLeafSegment });
        this.updateRoot(splitNode);
    }
    // Assume called only when pos == len
    breakTie(pos, node, seq) {
        if (node.isLeaf()) {
            if (pos !== 0) {
                return false;
            }
            // normalize the seq numbers
            // if the new seg is local (UnassignedSequenceNumber) give it the highest possible
            // seq for comparison, as it will get a seq higher than any other seq once sequences
            // if the current seg is local (UnassignedSequenceNumber) give it the second highest
            // possible seq, as the highest is reserved for the previous.
            const newSeq = seq === constants_1.UnassignedSequenceNumber ? Number.MAX_SAFE_INTEGER : seq;
            const segSeq = node.seq === constants_1.UnassignedSequenceNumber ? Number.MAX_SAFE_INTEGER - 1 : node.seq ?? 0;
            return (newSeq > segSeq ||
                (node.movedSeq !== undefined &&
                    node.movedSeq !== constants_1.UnassignedSequenceNumber &&
                    node.movedSeq > seq) ||
                (node.removedSeq !== undefined &&
                    node.removedSeq !== constants_1.UnassignedSequenceNumber &&
                    node.removedSeq > seq));
        }
        else {
            return true;
        }
    }
    getSmallestSeqMoveOp() {
        return this.moveSeqs[0] ?? (this.localMoveSeqs.size > 0 ? -1 : undefined);
    }
    insertingWalk(block, pos, refSeq, clientId, seq, context, isLastChildBlock = true) {
        let _pos = pos;
        const children = block.children;
        let childIndex;
        let child;
        let newNode;
        let fromSplit;
        for (childIndex = 0; childIndex < block.childCount; childIndex++) {
            child = children[childIndex];
            // ensure we walk down the far edge of the tree, even if all sub-tree is eligible for zamboni
            const isLastNonLeafBlock = isLastChildBlock && !child.isLeaf() && childIndex === block.childCount - 1;
            const len = this.nodeLength(child, refSeq, clientId) ?? (isLastChildBlock ? 0 : undefined);
            if (len === undefined) {
                // if the seg len is undefined, the segment
                // will be removed, so should just be skipped for now
                continue;
            }
            (0, core_utils_1.assert)(len >= 0, 0x4bc /* Length should not be negative */);
            if (_pos < len || (_pos === len && this.breakTie(_pos, child, seq))) {
                // Found entry containing pos
                if (!child.isLeaf()) {
                    const childBlock = child;
                    // Internal node
                    const splitNode = this.insertingWalk(childBlock, _pos, refSeq, clientId, seq, context, isLastNonLeafBlock);
                    if (splitNode === undefined) {
                        this.blockUpdateLength(block, seq, clientId);
                        return undefined;
                    }
                    else if (splitNode === MergeTree.theUnfinishedNode) {
                        _pos -= len; // Act as if shifted segment
                        continue;
                    }
                    else {
                        newNode = splitNode;
                        fromSplit = splitNode;
                        childIndex++; // Insert after
                    }
                }
                else {
                    const segment = child;
                    const segmentChanges = context.leaf(segment, _pos, context);
                    if (segmentChanges.replaceCurrent) {
                        block.assignChild(segmentChanges.replaceCurrent, childIndex, false);
                        segmentChanges.replaceCurrent.ordinal = child.ordinal;
                    }
                    if (segmentChanges.next) {
                        newNode = segmentChanges.next;
                        childIndex++; // Insert after
                    }
                    else {
                        // No change
                        return undefined;
                    }
                }
                break;
            }
            else {
                _pos -= len;
            }
        }
        if (!newNode) {
            if (_pos === 0) {
                if (context.continuePredicate?.(block)) {
                    return MergeTree.theUnfinishedNode;
                }
                else {
                    const segmentChanges = context.leaf(undefined, _pos, context);
                    newNode = segmentChanges.next;
                    // Assert segmentChanges.replaceCurrent === undefined
                }
            }
        }
        if (newNode) {
            for (let i = block.childCount; i > childIndex; i--) {
                block.children[i] = block.children[i - 1];
                block.children[i].index = i;
            }
            block.assignChild(newNode, childIndex, false);
            block.childCount++;
            block.setOrdinal(newNode, childIndex);
            if (block.childCount < mergeTreeNodes_1.MaxNodesInBlock) {
                if (fromSplit) {
                    this.nodeUpdateOrdinals(fromSplit);
                }
                this.blockUpdateLength(block, seq, clientId);
                return undefined;
            }
            else {
                // Don't update ordinals because higher block will do it
                const newNodeFromSplit = this.split(block);
                partialLengths_1.PartialSequenceLengths.options.verifyExpected?.(this, block, refSeq, clientId);
                partialLengths_1.PartialSequenceLengths.options.verifyExpected?.(this, newNodeFromSplit, refSeq, clientId);
                return newNodeFromSplit;
            }
        }
        else {
            return undefined;
        }
    }
    split(node) {
        const halfCount = mergeTreeNodes_1.MaxNodesInBlock / 2;
        const newNode = this.makeBlock(halfCount);
        node.childCount = halfCount;
        // Update ordinals to reflect lowered child count
        this.nodeUpdateOrdinals(node);
        for (let i = 0; i < halfCount; i++) {
            newNode.assignChild(node.children[halfCount + i], i, false);
            node.children[halfCount + i] = undefined;
        }
        this.nodeUpdateLengthNewStructure(node);
        this.nodeUpdateLengthNewStructure(newNode);
        return newNode;
    }
    nodeUpdateOrdinals(block) {
        for (let i = 0; i < block.childCount; i++) {
            const child = block.children[i];
            block.setOrdinal(child, i);
            if (!child.isLeaf()) {
                this.nodeUpdateOrdinals(child);
            }
        }
    }
    /**
     * Annotate a range with properties
     * @param start - The inclusive start position of the range to annotate
     * @param end - The exclusive end position of the range to annotate
     * @param props - The properties to annotate the range with
     * @param refSeq - The reference sequence number to use to apply the annotate
     * @param clientId - The id of the client making the annotate
     * @param seq - The sequence number of the annotate operation
     * @param opArgs - The op args for the annotate op. this is passed to the merge tree callback if there is one
     * @param rollback - Whether this is for a local rollback and what kind
     */
    annotateRange(start, end, props, refSeq, clientId, seq, opArgs, rollback = segmentPropertiesManager_1.PropertiesRollback.None) {
        this.ensureIntervalBoundary(start, refSeq, clientId);
        this.ensureIntervalBoundary(end, refSeq, clientId);
        const deltaSegments = [];
        const localSeq = seq === constants_1.UnassignedSequenceNumber ? ++this.collabWindow.localSeq : undefined;
        // eslint-disable-next-line import/no-deprecated
        let segmentGroup;
        const annotateSegment = (segment) => {
            (0, core_utils_1.assert)(!mergeTreeNodes_1.Marker.is(segment) ||
                !(mergeTreeNodes_1.reservedMarkerIdKey in props) ||
                props.markerId === segment.properties?.markerId, 0x5ad /* Cannot change the markerId of an existing marker */);
            const propertyDeltas = segment.addProperties(props, seq, this.collabWindow.collaborating, rollback);
            deltaSegments.push({ segment, propertyDeltas });
            if (this.collabWindow.collaborating) {
                if (seq === constants_1.UnassignedSequenceNumber) {
                    segmentGroup = this.addToPendingList(segment, segmentGroup, localSeq, propertyDeltas);
                }
                else {
                    if (MergeTree.options.zamboniSegments) {
                        this.addToLRUSet(segment, seq);
                    }
                }
            }
            return true;
        };
        this.nodeMap(refSeq, clientId, annotateSegment, undefined, undefined, start, end);
        // OpArgs == undefined => test code
        if (deltaSegments.length > 0) {
            this.mergeTreeDeltaCallback?.(opArgs, {
                operation: ops_1.MergeTreeDeltaType.ANNOTATE,
                deltaSegments,
            });
        }
        if (this.collabWindow.collaborating && seq !== constants_1.UnassignedSequenceNumber) {
            if (MergeTree.options.zamboniSegments) {
                (0, zamboni_1.zamboniSegments)(this);
            }
        }
    }
    /**
     * @alpha
     */
    obliterateRange(start, end, refSeq, clientId, seq, overwrite = false, opArgs) {
        if (!this.options?.mergeTreeEnableObliterate) {
            throw new telemetry_utils_1.UsageError("Attempted to send obliterate op without enabling feature flag.");
        }
        this.ensureIntervalBoundary(start, refSeq, clientId);
        this.ensureIntervalBoundary(end, refSeq, clientId);
        let _overwrite = overwrite;
        const localOverlapWithRefs = [];
        const movedSegments = [];
        const localSeq = seq === constants_1.UnassignedSequenceNumber ? ++this.collabWindow.localSeq : undefined;
        if (seq !== constants_1.UnassignedSequenceNumber && seq !== this.moveSeqs[this.moveSeqs.length - 1]) {
            this.moveSeqs.push(seq);
        }
        else if (seq === constants_1.UnassignedSequenceNumber && localSeq !== undefined) {
            this.localMoveSeqs.add(localSeq);
        }
        // eslint-disable-next-line import/no-deprecated
        let segmentGroup;
        const markMoved = (segment, pos, _start, _end) => {
            const existingMoveInfo = (0, mergeTreeNodes_1.toMoveInfo)(segment);
            if (clientId !== segment.clientId &&
                segment.seq !== undefined &&
                seq !== constants_1.UnassignedSequenceNumber &&
                (refSeq < segment.seq || segment.seq === constants_1.UnassignedSequenceNumber)) {
                segment.wasMovedOnInsert = true;
            }
            if (existingMoveInfo !== undefined) {
                _overwrite = true;
                if (existingMoveInfo.movedSeq === constants_1.UnassignedSequenceNumber) {
                    // we moved this locally, but someone else moved it first
                    // so put them at the head of the list
                    // The list isn't ordered, but we keep the first move at the head
                    // for partialLengths bookkeeping purposes
                    existingMoveInfo.movedClientIds.unshift(clientId);
                    existingMoveInfo.movedSeq = seq;
                    existingMoveInfo.movedSeqs.unshift(seq);
                    if (segment.localRefs?.empty === false) {
                        localOverlapWithRefs.push(segment);
                    }
                }
                else {
                    // Do not replace earlier sequence number for move
                    existingMoveInfo.movedClientIds.push(clientId);
                    existingMoveInfo.movedSeqs.push(seq);
                }
            }
            else {
                segment.movedClientIds = [clientId];
                segment.movedSeq = seq;
                segment.localMovedSeq = localSeq;
                segment.movedSeqs = [seq];
                movedSegments.push({ segment });
            }
            // Save segment so can assign moved sequence number when acked by server
            if (this.collabWindow.collaborating) {
                if (segment.movedSeq === constants_1.UnassignedSequenceNumber &&
                    clientId === this.collabWindow.clientId) {
                    segmentGroup = this.addToPendingList(segment, segmentGroup, localSeq);
                }
                else {
                    if (MergeTree.options.zamboniSegments) {
                        this.addToLRUSet(segment, seq);
                    }
                }
            }
            return true;
        };
        const afterMarkMoved = (node, pos, _start, _end) => {
            if (_overwrite) {
                this.nodeUpdateLengthNewStructure(node);
            }
            else {
                this.blockUpdateLength(node, seq, clientId);
            }
            return true;
        };
        this.nodeMap(refSeq, clientId, markMoved, undefined, afterMarkMoved, start, end, undefined, seq !== constants_1.UnassignedSequenceNumber ? seq : undefined);
        this.slideAckedRemovedSegmentReferences(localOverlapWithRefs);
        // opArgs == undefined => test code
        if (movedSegments.length > 0) {
            this.mergeTreeDeltaCallback?.(opArgs, {
                operation: ops_1.MergeTreeDeltaType.OBLITERATE,
                deltaSegments: movedSegments,
            });
        }
        if (segmentGroup && localSeq !== undefined) {
            this.locallyMovedSegments.set(localSeq, segmentGroup);
        }
        // these events are newly removed
        // so we slide after eventing in case the consumer wants to make reference
        // changes at remove time, like add a ref to track undo redo.
        if (!this.collabWindow.collaborating || clientId !== this.collabWindow.clientId) {
            this.slideAckedRemovedSegmentReferences(movedSegments.map(({ segment }) => segment));
        }
        if (this.collabWindow.collaborating && seq !== constants_1.UnassignedSequenceNumber) {
            if (MergeTree.options.zamboniSegments) {
                (0, zamboni_1.zamboniSegments)(this);
            }
        }
    }
    markRangeRemoved(start, end, refSeq, clientId, seq, overwrite = false, opArgs) {
        let _overwrite = overwrite;
        this.ensureIntervalBoundary(start, refSeq, clientId);
        this.ensureIntervalBoundary(end, refSeq, clientId);
        // eslint-disable-next-line import/no-deprecated
        let segmentGroup;
        const removedSegments = [];
        const localOverlapWithRefs = [];
        const localSeq = seq === constants_1.UnassignedSequenceNumber ? ++this.collabWindow.localSeq : undefined;
        const markRemoved = (segment, pos, _start, _end) => {
            const existingRemovalInfo = (0, mergeTreeNodes_1.toRemovalInfo)(segment);
            if (existingRemovalInfo !== undefined) {
                _overwrite = true;
                if (existingRemovalInfo.removedSeq === constants_1.UnassignedSequenceNumber) {
                    // we removed this locally, but someone else removed it first
                    // so put them at the head of the list
                    // The list isn't ordered, but we keep the first removal at the head
                    // for partialLengths bookkeeping purposes
                    existingRemovalInfo.removedClientIds.unshift(clientId);
                    existingRemovalInfo.removedSeq = seq;
                    if (segment.localRefs?.empty === false) {
                        localOverlapWithRefs.push(segment);
                    }
                }
                else {
                    // Do not replace earlier sequence number for remove
                    existingRemovalInfo.removedClientIds.push(clientId);
                }
            }
            else {
                segment.removedClientIds = [clientId];
                segment.removedSeq = seq;
                segment.localRemovedSeq = localSeq;
                removedSegments.push({ segment });
            }
            // Save segment so we can assign removed sequence number when acked by server
            if (this.collabWindow.collaborating) {
                if (segment.removedSeq === constants_1.UnassignedSequenceNumber &&
                    clientId === this.collabWindow.clientId) {
                    segmentGroup = this.addToPendingList(segment, segmentGroup, localSeq);
                }
                else {
                    if (MergeTree.options.zamboniSegments) {
                        this.addToLRUSet(segment, seq);
                    }
                }
            }
            return true;
        };
        const afterMarkRemoved = (node, pos, _start, _end) => {
            if (_overwrite) {
                this.nodeUpdateLengthNewStructure(node);
            }
            else {
                this.blockUpdateLength(node, seq, clientId);
            }
            return true;
        };
        this.nodeMap(refSeq, clientId, markRemoved, undefined, afterMarkRemoved, start, end);
        // these segments are already viewed as being removed locally and are not event-ed
        // so can slide non-StayOnRemove refs immediately
        this.slideAckedRemovedSegmentReferences(localOverlapWithRefs);
        // opArgs == undefined => test code
        if (removedSegments.length > 0) {
            this.mergeTreeDeltaCallback?.(opArgs, {
                operation: ops_1.MergeTreeDeltaType.REMOVE,
                deltaSegments: removedSegments,
            });
        }
        // these events are newly removed
        // so we slide after eventing in case the consumer wants to make reference
        // changes at remove time, like add a ref to track undo redo.
        if (!this.collabWindow.collaborating || clientId !== this.collabWindow.clientId) {
            this.slideAckedRemovedSegmentReferences(removedSegments.map(({ segment }) => segment));
        }
        if (this.collabWindow.collaborating && seq !== constants_1.UnassignedSequenceNumber) {
            if (MergeTree.options.zamboniSegments) {
                (0, zamboni_1.zamboniSegments)(this);
            }
        }
    }
    /**
     * Revert an unacked local op
     */
    // eslint-disable-next-line import/no-deprecated
    rollback(op, localOpMetadata) {
        if (op.type === ops_1.MergeTreeDeltaType.REMOVE) {
            const pendingSegmentGroup = this.pendingSegments.pop?.()?.data;
            if (pendingSegmentGroup === undefined || pendingSegmentGroup !== localOpMetadata) {
                throw new Error("Rollback op doesn't match last edit");
            }
            pendingSegmentGroup.segments.forEach((segment) => {
                const segmentSegmentGroup = segment.segmentGroups?.pop?.();
                (0, core_utils_1.assert)(segmentSegmentGroup === pendingSegmentGroup, 0x3ee /* Unexpected segmentGroup in segment */);
                (0, core_utils_1.assert)(segment.removedClientIds !== undefined &&
                    segment.removedClientIds[0] === this.collabWindow.clientId, 0x39d /* Rollback segment removedClientId does not match local client */);
                segment.removedClientIds = undefined;
                segment.removedSeq = undefined;
                segment.localRemovedSeq = undefined;
                // Note: optional chaining short-circuits:
                // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining#short-circuiting
                this.mergeTreeDeltaCallback?.({ op: (0, opBuilder_1.createInsertSegmentOp)(this.findRollbackPosition(segment), segment) }, {
                    operation: ops_1.MergeTreeDeltaType.INSERT,
                    deltaSegments: [{ segment }],
                });
                for (let updateNode = segment.parent; updateNode !== undefined; updateNode = updateNode.parent) {
                    this.blockUpdateLength(updateNode, constants_1.UnassignedSequenceNumber, this.collabWindow.clientId);
                }
            });
        }
        else if (op.type === ops_1.MergeTreeDeltaType.INSERT ||
            op.type === ops_1.MergeTreeDeltaType.ANNOTATE) {
            const pendingSegmentGroup = this.pendingSegments.pop?.()?.data;
            if (pendingSegmentGroup === undefined ||
                pendingSegmentGroup !== localOpMetadata ||
                (op.type === ops_1.MergeTreeDeltaType.ANNOTATE && !pendingSegmentGroup.previousProps)) {
                throw new Error("Rollback op doesn't match last edit");
            }
            let i = 0;
            for (const segment of pendingSegmentGroup.segments) {
                const segmentSegmentGroup = segment.segmentGroups.pop?.();
                (0, core_utils_1.assert)(segmentSegmentGroup === pendingSegmentGroup, 0x3ef /* Unexpected segmentGroup in segment */);
                const start = this.findRollbackPosition(segment);
                if (op.type === ops_1.MergeTreeDeltaType.INSERT) {
                    segment.seq = constants_1.UniversalSequenceNumber;
                    segment.localSeq = undefined;
                    const removeOp = (0, opBuilder_1.createRemoveRangeOp)(start, start + segment.cachedLength);
                    this.markRangeRemoved(start, start + segment.cachedLength, constants_1.UniversalSequenceNumber, this.collabWindow.clientId, constants_1.UniversalSequenceNumber, false, { op: removeOp });
                } /* op.type === MergeTreeDeltaType.ANNOTATE */
                else {
                    const props = pendingSegmentGroup.previousProps[i];
                    const annotateOp = (0, opBuilder_1.createAnnotateRangeOp)(start, start + segment.cachedLength, props);
                    this.annotateRange(start, start + segment.cachedLength, props, constants_1.UniversalSequenceNumber, this.collabWindow.clientId, constants_1.UniversalSequenceNumber, { op: annotateOp }, segmentPropertiesManager_1.PropertiesRollback.Rollback);
                    i++;
                }
            }
        }
        else {
            throw new Error("Unsupported op type for rollback");
        }
    }
    /**
     * Walk the segments up to the current segment and calculate its position
     */
    findRollbackPosition(segment) {
        let segmentPosition = 0;
        (0, mergeTreeNodeWalk_1.walkAllChildSegments)(this.root, (seg) => {
            // If we've found the desired segment, terminate the walk and return 'segmentPosition'.
            if (seg === segment) {
                return false;
            }
            // If not removed, increase position
            if (seg.removedSeq === undefined) {
                segmentPosition += seg.cachedLength;
            }
            return true;
        });
        return segmentPosition;
    }
    nodeUpdateLengthNewStructure(node, recur = false) {
        this.blockUpdate(node);
        if (this.collabWindow.collaborating) {
            this.localPartialsComputed = false;
            node.partialLengths = partialLengths_1.PartialSequenceLengths.combine(node, this.collabWindow, recur);
        }
    }
    removeLocalReferencePosition(lref) {
        const segment = lref.getSegment();
        return segment?.localRefs?.removeLocalRef(lref);
    }
    createLocalReferencePosition(_segment, offset, refType, properties, slidingPreference, canSlideToEndpoint) {
        if (_segment !== "start" &&
            _segment !== "end" &&
            isRemovedAndAckedOrMovedAndAcked(_segment) &&
            !(0, referencePositions_1.refTypeIncludesFlag)(refType, ops_1.ReferenceType.SlideOnRemove | ops_1.ReferenceType.Transient) &&
            _segment.endpointType === undefined) {
            throw new telemetry_utils_1.UsageError("Can only create SlideOnRemove or Transient local reference position on a removed or obliterated segment");
        }
        let segment;
        if (_segment === "start") {
            segment = this.startOfTree;
        }
        else if (_segment === "end") {
            segment = this.endOfTree;
        }
        else {
            segment = _segment;
        }
        const localRefs = segment.localRefs ?? new localReference_1.LocalReferenceCollection(segment);
        segment.localRefs = localRefs;
        const segRef = localRefs.createLocalRef(offset, refType, properties, slidingPreference, canSlideToEndpoint);
        return segRef;
    }
    // Segments should either be removed remotely, removed locally, or inserted locally
    normalizeAdjacentSegments(affectedSegments) {
        // Eagerly demand this since we're about to shift elements in the list around
        const currentOrder = Array.from(affectedSegments, ({ data: seg }) => ({
            parent: seg.parent,
            index: seg.index,
            ordinal: seg.ordinal,
        }));
        // Last segment which was not affected locally.
        let lastLocalSegment = affectedSegments.last;
        while (lastLocalSegment !== undefined && isRemovedAndAcked(lastLocalSegment.data)) {
            lastLocalSegment = lastLocalSegment.prev;
        }
        if (!lastLocalSegment) {
            return;
        }
        for (let segmentToSlide = lastLocalSegment, nearerSegment = lastLocalSegment?.prev; segmentToSlide !== undefined; segmentToSlide = nearerSegment, nearerSegment = nearerSegment?.prev) {
            // Slide iterCur forward as far as possible
            if (isRemovedAndAcked(segmentToSlide.data)) {
                // Slide past all segments that are not also remotely removed
                affectedSegments.remove(segmentToSlide);
                affectedSegments.insertAfter(lastLocalSegment, segmentToSlide.data);
            }
            else if (isRemoved(segmentToSlide.data)) {
                (0, core_utils_1.assert)(segmentToSlide.data.localRemovedSeq !== undefined, 0x54d /* Removed segment that hasnt had its removal acked should be locally removed */);
                // Slide each locally removed item past all segments that have localSeq > lremoveItem.localSeq
                // but not past remotely removed segments;
                let cur = segmentToSlide;
                let scan = cur.next;
                while (scan !== undefined &&
                    !isRemovedAndAcked(scan.data) &&
                    scan.data.localSeq !== undefined &&
                    scan.data.localSeq > segmentToSlide.data.localRemovedSeq) {
                    cur = scan;
                    scan = scan.next;
                }
                if (cur !== segmentToSlide) {
                    affectedSegments.remove(segmentToSlide);
                    affectedSegments.insertAfter(cur, segmentToSlide.data);
                }
            }
        }
        const newOrder = Array.from(affectedSegments.map(({ data }) => data));
        newOrder.forEach((seg) => seg.localRefs?.walkReferences((lref) => lref.callbacks?.beforeSlide?.(lref)));
        const perSegmentTrackingGroups = new Map();
        for (const segment of newOrder) {
            const { trackingCollection } = segment;
            const trackingGroups = Array.from(trackingCollection.trackingGroups);
            perSegmentTrackingGroups.set(segment, trackingGroups);
            for (const group of trackingCollection.trackingGroups) {
                trackingCollection.unlink(group);
            }
        }
        for (let i = 0; i < newOrder.length; i++) {
            const seg = newOrder[i];
            const { parent, index, ordinal } = currentOrder[i];
            parent?.assignChild(seg, index, false);
            seg.ordinal = ordinal;
        }
        for (const [segment, groups] of perSegmentTrackingGroups.entries()) {
            for (const group of groups) {
                segment.trackingCollection.link(group);
            }
        }
        // Finally, update internal node bookkeeping on ancestors of the swapped nodes.
        // Toposort would improve this by a log factor, but probably not worth the added code size
        const depths = new Map();
        const computeDepth = (block) => {
            if (!depths.has(block)) {
                depths.set(block, block.parent === undefined ? 0 : 1 + computeDepth(block.parent));
            }
            return depths.get(block);
        };
        newOrder.forEach(computeDepth);
        for (const [node] of Array.from(depths.entries()).sort((a, b) => b[1] - a[1])) {
            if (!node.isLeaf()) {
                this.nodeUpdateLengthNewStructure(node);
            }
        }
        newOrder.forEach((seg) => seg.localRefs?.walkReferences((lref) => lref.callbacks?.afterSlide?.(lref)));
    }
    /**
     * Normalizes the segments nearby `segmentGroup` to be ordered as they would if the op submitting `segmentGroup`
     * is rebased to the current sequence number.
     * This primarily affects the ordering of adjacent segments that were removed between the original submission of
     * the local ops and now.
     * Consider the following sequence of events:
     * Initial state: "hi my friend" (seq: 0)
     * - Client 1 inserts "good " to make "hi my good friend" (op1, refSeq: 0)
     * - Client 2 deletes "my " to make "hi friend" (op2, refSeq: 0)
     * - op2 is sequenced giving seq 1
     * - Client 1 disconnects and reconnects at seq: 1.
     *
     * At this point in time, client 1 will have segments ["hi ", Removed"my ", Local"good ", "friend"].
     * However, the rebased op that it submits will cause client 2 to have segments
     * ["hi ", Local"good ", Removed"my ", "friend"].
     *
     * The difference in ordering can be problematic for tie-breaking concurrently inserted segments in some scenarios.
     * Rather than incur extra work tie-breaking these scenarios for all clients, when client 1 rebases its operation,
     * it can fix up its local state to align with what would be expected of the op it resubmits.
     */
    normalizeSegmentsOnRebase() {
        let currentRangeToNormalize = new collections_1.DoublyLinkedList();
        let rangeContainsLocalSegs = false;
        let rangeContainsRemoteRemovedSegs = false;
        const normalize = () => {
            if (rangeContainsLocalSegs &&
                rangeContainsRemoteRemovedSegs &&
                currentRangeToNormalize.length > 1) {
                this.normalizeAdjacentSegments(currentRangeToNormalize);
            }
        };
        (0, mergeTreeNodeWalk_1.walkAllChildSegments)(this.root, (seg) => {
            if (isRemoved(seg) || seg.seq === constants_1.UnassignedSequenceNumber) {
                if (isRemovedAndAcked(seg)) {
                    rangeContainsRemoteRemovedSegs = true;
                }
                if (seg.seq === constants_1.UnassignedSequenceNumber) {
                    rangeContainsLocalSegs = true;
                }
                currentRangeToNormalize.push(seg);
            }
            else {
                normalize();
                currentRangeToNormalize = new collections_1.DoublyLinkedList();
                rangeContainsLocalSegs = false;
                rangeContainsRemoteRemovedSegs = false;
            }
            return true;
        });
        normalize();
    }
    addNodeReferences(node, rightmostTiles, leftmostTiles) {
        if (node.isLeaf()) {
            const segment = node;
            if ((this.localNetLength(segment) ?? 0) > 0 && mergeTreeNodes_1.Marker.is(segment)) {
                const markerId = segment.getId();
                // Also in insertMarker but need for reload segs case
                // can add option for this only from reload segs
                if (markerId) {
                    this.idToMarker.set(markerId, segment);
                }
                if ((0, referencePositions_1.refTypeIncludesFlag)(segment, ops_1.ReferenceType.Tile)) {
                    addTile(segment, rightmostTiles);
                    addTileIfNotPresent(segment, leftmostTiles);
                }
            }
        }
        else {
            const block = node;
            // eslint-disable-next-line import/no-deprecated
            (0, properties_1.extend)(rightmostTiles, block.rightmostTiles);
            // eslint-disable-next-line import/no-deprecated
            (0, properties_1.extendIfUndefined)(leftmostTiles, block.leftmostTiles);
        }
    }
    blockUpdate(block) {
        let len;
        const hierBlock = block.hierBlock();
        if (hierBlock) {
            // eslint-disable-next-line import/no-deprecated
            hierBlock.rightmostTiles = (0, properties_1.createMap)();
            // eslint-disable-next-line import/no-deprecated
            hierBlock.leftmostTiles = (0, properties_1.createMap)();
        }
        for (let i = 0; i < block.childCount; i++) {
            const child = block.children[i];
            const nodeLength = nodeTotalLength(this, child);
            if (nodeLength !== undefined) {
                len ?? (len = 0);
                len += nodeLength;
            }
            if (hierBlock) {
                this.addNodeReferences(child, hierBlock.rightmostTiles, hierBlock.leftmostTiles);
            }
        }
        block.cachedLength = len;
    }
    blockUpdatePathLengths(startBlock, seq, clientId, newStructure = false) {
        let block = startBlock;
        while (block !== undefined) {
            if (newStructure) {
                this.nodeUpdateLengthNewStructure(block);
            }
            else {
                this.blockUpdateLength(block, seq, clientId);
            }
            block = block.parent;
        }
    }
    blockUpdateLength(node, seq, clientId) {
        this.blockUpdate(node);
        this.localPartialsComputed = false;
        if (this.collabWindow.collaborating &&
            seq !== constants_1.UnassignedSequenceNumber &&
            seq !== constants_1.TreeMaintenanceSequenceNumber) {
            if (node.partialLengths !== undefined &&
                MergeTree.options.incrementalUpdate &&
                clientId !== constants_1.NonCollabClient) {
                node.partialLengths.update(node, seq, clientId, this.collabWindow);
            }
            else {
                node.partialLengths = partialLengths_1.PartialSequenceLengths.combine(node, this.collabWindow);
            }
            partialLengths_1.PartialSequenceLengths.options.verifyExpected?.(this, node, seq, clientId);
        }
    }
    /**
     * Map over all visible segments in a given range
     *
     * A segment is visible if its length is greater than 0
     *
     * See `this.nodeMap` for additional documentation
     */
    mapRange(handler, refSeq, clientId, accum, start, end, splitRange = false, visibilitySeq = refSeq) {
        if (splitRange) {
            if (start) {
                this.ensureIntervalBoundary(start, refSeq, clientId);
            }
            if (end) {
                this.ensureIntervalBoundary(end, refSeq, clientId);
            }
        }
        this.nodeMap(refSeq, clientId, handler, accum, undefined, start, end, undefined, visibilitySeq);
    }
    /**
     * Map over all visible segments in a given range
     *
     * A segment is visible if its length is greater than 0
     *
     * @param refSeq - The sequence number used to determine the range (start
     * and end positions) of segments to iterate over.
     *
     * @param visibilitySeq - An additional sequence number to further configure
     * segment visibility during traversal. This is the same as refSeq, except
     * in the case of obliterate.
     *
     * In the case where `refSeq == visibilitySeq`, mapping is done on all
     * visible segments from `start` to `end`.
     *
     * If a segment is invisible at both `visibilitySeq` and `refSeq`, then it
     * will not be traversed and mapped. Otherwise, if the segment is visible at
     * either seq, it will be mapped.
     *
     * If a segment is only visible at `visibilitySeq`, it will still be mapped,
     * but it will not count as a segment within the range. That is, it will be
     * ignored for the purposes of tracking when traversal should end.
     */
    nodeMap(refSeq, clientId, leaf, accum, post, start = 0, end, localSeq, visibilitySeq = refSeq) {
        const endPos = end ?? this.nodeLength(this.root, refSeq, clientId, localSeq) ?? 0;
        if (endPos === start) {
            return;
        }
        let pos = 0;
        (0, mergeTreeNodeWalk_1.depthFirstNodeWalk)(this.root, this.root.children[0], (node) => {
            if (endPos <= pos) {
                return mergeTreeNodeWalk_1.NodeAction.Exit;
            }
            const len = this.nodeLength(node, visibilitySeq, clientId, localSeq);
            const lenAtRefSeq = (visibilitySeq === refSeq
                ? len
                : this.nodeLength(node, refSeq, clientId, localSeq)) ?? 0;
            const isUnackedAndInObliterate = visibilitySeq !== refSeq &&
                (!node.isLeaf() || node.seq === constants_1.UnassignedSequenceNumber);
            if ((len === undefined && lenAtRefSeq === 0) ||
                (len === 0 && !isUnackedAndInObliterate && lenAtRefSeq === 0)) {
                return mergeTreeNodeWalk_1.NodeAction.Skip;
            }
            const nextPos = pos + lenAtRefSeq;
            // start is beyond the current node, so we can skip it
            if (start >= nextPos) {
                pos = nextPos;
                return mergeTreeNodeWalk_1.NodeAction.Skip;
            }
            if (node.isLeaf()) {
                if (leaf(node, pos, refSeq, clientId, start - pos, endPos - pos, accum) ===
                    false) {
                    return mergeTreeNodeWalk_1.NodeAction.Exit;
                }
                pos = nextPos;
            }
        }, undefined, post === undefined
            ? undefined
            : (block) => post(block, pos, refSeq, clientId, start - pos, endPos - pos, accum));
    }
}
exports.MergeTree = MergeTree;
MergeTree.options = {
    incrementalUpdate: true,
    insertAfterRemovedSegs: true,
    zamboniSegments: true,
};
MergeTree.theUnfinishedNode = { childCount: -1 };
//# sourceMappingURL=mergeTree.cjs.map