"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.clientSeqComparer = exports.internedSpaces = exports.compareStrings = exports.compareNumbers = exports.CollaborationWindow = exports.IncrementalMapState = exports.IncrementalExecOp = exports.Marker = exports.reservedMarkerSimpleTypeKey = exports.reservedMarkerIdKey = exports.BaseSegment = exports.MergeBlock = exports.MaxNodesInBlock = exports.ordinalToArray = exports.MergeNode = exports.toRemovalInfo = void 0;
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable @typescript-eslint/consistent-type-assertions */
/* eslint-disable @typescript-eslint/prefer-optional-chain, no-bitwise */
const common_utils_1 = require("@fluidframework/common-utils");
const container_utils_1 = require("@fluidframework/container-utils");
const collections_1 = require("./collections");
const constants_1 = require("./constants");
const localReference_1 = require("./localReference");
const mergeTreeDeltaCallback_1 = require("./mergeTreeDeltaCallback");
const mergeTreeTracking_1 = require("./mergeTreeTracking");
const ops_1 = require("./ops");
const partialLengths_1 = require("./partialLengths");
const properties_1 = require("./properties");
const referencePositions_1 = require("./referencePositions");
const segmentGroupCollection_1 = require("./segmentGroupCollection");
const segmentPropertiesManager_1 = require("./segmentPropertiesManager");
function toRemovalInfo(maybe) {
    if ((maybe === null || maybe === void 0 ? void 0 : maybe.removedClientIds) !== undefined && (maybe === null || maybe === void 0 ? void 0 : maybe.removedSeq) !== undefined) {
        return maybe;
    }
    (0, common_utils_1.assert)((maybe === null || maybe === void 0 ? void 0 : maybe.removedClientIds) === undefined && (maybe === null || maybe === void 0 ? void 0 : maybe.removedSeq) === undefined, 0x2bf /* "both removedClientIds and removedSeq should be set or not set" */);
}
exports.toRemovalInfo = toRemovalInfo;
function isRemoved(segment) {
    return toRemovalInfo(segment) !== undefined;
}
function isRemovedAndAcked(segment) {
    const removalInfo = toRemovalInfo(segment);
    return removalInfo !== undefined && removalInfo.removedSeq !== constants_1.UnassignedSequenceNumber;
}
class MergeNode {
    constructor() {
        this.index = 0;
        this.ordinal = "";
        this.cachedLength = 0;
    }
    isLeaf() {
        return false;
    }
}
exports.MergeNode = MergeNode;
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;
            }
        }
    }
}
function applyStackDelta(currentStackMap, deltaStackMap) {
    // eslint-disable-next-line guard-for-in, no-restricted-syntax
    for (const label in deltaStackMap) {
        const deltaStack = deltaStackMap[label];
        if (!deltaStack.empty()) {
            let currentStack = currentStackMap[label];
            if (currentStack === undefined) {
                currentStack = new collections_1.Stack();
                currentStackMap[label] = currentStack;
            }
            for (const delta of deltaStack.items) {
                applyRangeReference(currentStack, delta);
            }
        }
    }
}
function applyRangeReference(stack, delta) {
    if ((0, referencePositions_1.refTypeIncludesFlag)(delta, ops_1.ReferenceType.NestBegin)) {
        stack.push(delta);
        return true;
    }
    else {
        // Assume delta is end reference
        const top = stack.top();
        // TODO: match end with begin
        if (top && ((0, referencePositions_1.refTypeIncludesFlag)(top, ops_1.ReferenceType.NestBegin))) {
            stack.pop();
        }
        else {
            stack.push(delta);
        }
        return false;
    }
}
function addNodeReferences(mergeTree, node, rightmostTiles, leftmostTiles, rangeStacks) {
    var _a;
    function updateRangeInfo(label, refPos) {
        let stack = rangeStacks[label];
        if (stack === undefined) {
            stack = new collections_1.Stack();
            rangeStacks[label] = stack;
        }
        applyRangeReference(stack, refPos);
    }
    if (node.isLeaf()) {
        const segment = node;
        if (((_a = mergeTree.localNetLength(segment)) !== null && _a !== void 0 ? _a : 0) > 0) {
            if (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) {
                    mergeTree.mapIdToSegment(markerId, segment);
                }
                if ((0, referencePositions_1.refTypeIncludesFlag)(segment, ops_1.ReferenceType.Tile)) {
                    addTile(segment, rightmostTiles);
                    addTileIfNotPresent(segment, leftmostTiles);
                }
                if (segment.refType & (ops_1.ReferenceType.NestBegin | ops_1.ReferenceType.NestEnd)) {
                    const rangeLabels = (0, referencePositions_1.refGetRangeLabels)(segment);
                    if (rangeLabels) {
                        for (const label of rangeLabels) {
                            updateRangeInfo(label, segment);
                        }
                    }
                }
            }
            else {
                const baseSegment = node;
                if (baseSegment.localRefs && (baseSegment.localRefs.hierRefCount !== undefined) &&
                    (baseSegment.localRefs.hierRefCount > 0)) {
                    for (const lref of baseSegment.localRefs) {
                        if ((0, referencePositions_1.refTypeIncludesFlag)(lref, ops_1.ReferenceType.Tile)) {
                            addTile(lref, rightmostTiles);
                            addTileIfNotPresent(lref, leftmostTiles);
                        }
                        if (lref.refType & (ops_1.ReferenceType.NestBegin | ops_1.ReferenceType.NestEnd)) {
                            for (const label of (0, referencePositions_1.refGetRangeLabels)(lref)) {
                                updateRangeInfo(label, lref);
                            }
                        }
                    }
                }
            }
        }
    }
    else {
        const block = node;
        applyStackDelta(rangeStacks, block.rangeStacks);
        (0, properties_1.extend)(rightmostTiles, block.rightmostTiles);
        (0, properties_1.extendIfUndefined)(leftmostTiles, block.leftmostTiles);
    }
}
function ordinalToArray(ord) {
    const a = [];
    if (ord) {
        for (let i = 0, len = ord.length; i < len; i++) {
            a.push(ord.charCodeAt(i));
        }
    }
    return a;
}
exports.ordinalToArray = ordinalToArray;
// Note that the actual branching factor of the MergeTree is `MaxNodesInBlock - 1`.  This is because
// the MergeTree always inserts first, then checks for overflow and splits if the child count equals
// `MaxNodesInBlock`.  (i.e., `MaxNodesInBlock` contains 1 extra slot for temporary storage to
// facilitate splits.)
exports.MaxNodesInBlock = 8;
class MergeBlock extends MergeNode {
    constructor(childCount) {
        super();
        this.childCount = childCount;
        this.children = new Array(exports.MaxNodesInBlock);
    }
    hierBlock() {
        return undefined;
    }
    setOrdinal(child, index) {
        let childCount = this.childCount;
        if (childCount === 8) {
            childCount = 7;
        }
        (0, common_utils_1.assert)((childCount >= 1) && (childCount <= 7), 0x040 /* "Child count is not within [1,7] range!" */);
        let localOrdinal;
        const ordinalWidth = 1 << (exports.MaxNodesInBlock - (childCount + 1));
        if (index === 0) {
            localOrdinal = ordinalWidth - 1;
        }
        else {
            const prevOrd = this.children[index - 1].ordinal;
            const prevOrdCode = prevOrd.charCodeAt(prevOrd.length - 1);
            localOrdinal = prevOrdCode + ordinalWidth;
        }
        child.ordinal = this.ordinal + String.fromCharCode(localOrdinal);
        (0, common_utils_1.assert)(child.ordinal.length === (this.ordinal.length + 1), 0x041 /* "Unexpected child ordinal length!" */);
        if (index > 0) {
            (0, common_utils_1.assert)(child.ordinal > this.children[index - 1].ordinal, 0x042);
        }
    }
    assignChild(child, index, updateOrdinal = true) {
        child.parent = this;
        child.index = index;
        if (updateOrdinal) {
            this.setOrdinal(child, index);
        }
        this.children[index] = child;
    }
}
exports.MergeBlock = MergeBlock;
class HierMergeBlock extends MergeBlock {
    constructor(childCount) {
        super(childCount);
        this.rightmostTiles = (0, properties_1.createMap)();
        this.leftmostTiles = (0, properties_1.createMap)();
        this.rangeStacks = (0, properties_1.createMap)();
    }
    addNodeReferences(mergeTree, node) {
        addNodeReferences(mergeTree, node, this.rightmostTiles, this.leftmostTiles, this.rangeStacks);
    }
    hierBlock() {
        return this;
    }
    hierToString(indentCount) {
        let strbuf = "";
        // eslint-disable-next-line guard-for-in, no-restricted-syntax
        for (const key in this.rangeStacks) {
            const stack = this.rangeStacks[key];
            strbuf += internedSpaces(indentCount);
            strbuf += `${key}: `;
            for (const item of stack.items) {
                strbuf += `${item.toString()} `;
            }
            strbuf += "\n";
        }
        return strbuf;
    }
}
function nodeTotalLength(mergeTree, node) {
    if (!node.isLeaf()) {
        return node.cachedLength;
    }
    return mergeTree.localNetLength(node);
}
class BaseSegment extends MergeNode {
    constructor() {
        super(...arguments);
        this.clientId = constants_1.LocalClientId;
        this.seq = constants_1.UniversalSequenceNumber;
        this.segmentGroups = new segmentGroupCollection_1.SegmentGroupCollection(this);
        this.trackingCollection = new mergeTreeTracking_1.TrackingGroupCollection(this);
    }
    addProperties(newProps, op, seq, collabWindow) {
        if (!this.propertyManager) {
            this.propertyManager = new segmentPropertiesManager_1.PropertiesManager();
        }
        if (!this.properties) {
            this.properties = (0, properties_1.createMap)();
        }
        return this.propertyManager.addProperties(this.properties, newProps, op, seq, collabWindow && collabWindow.collaborating);
    }
    hasProperty(key) {
        return !!this.properties && (this.properties[key] !== undefined);
    }
    isLeaf() {
        return true;
    }
    cloneInto(b) {
        var _a;
        b.clientId = this.clientId;
        // TODO: deep clone properties
        b.properties = (0, properties_1.clone)(this.properties);
        b.removedClientIds = (_a = this.removedClientIds) === null || _a === void 0 ? void 0 : _a.slice();
        // TODO: copy removed client overlap and branch removal info
        b.removedSeq = this.removedSeq;
        b.seq = this.seq;
    }
    canAppend(segment) {
        return false;
    }
    addSerializedProps(jseg) {
        if (this.properties) {
            jseg.props = this.properties;
        }
    }
    ack(segmentGroup, opArgs, mergeTree) {
        const currentSegmentGroup = this.segmentGroups.dequeue();
        (0, common_utils_1.assert)(currentSegmentGroup === segmentGroup, 0x043 /* "On ack, unexpected segmentGroup!" */);
        switch (opArgs.op.type) {
            case ops_1.MergeTreeDeltaType.ANNOTATE:
                (0, common_utils_1.assert)(!!this.propertyManager, 0x044 /* "On annotate ack, missing segment property manager!" */);
                this.propertyManager.ackPendingProperties(opArgs.op);
                return true;
            case ops_1.MergeTreeDeltaType.INSERT:
                (0, common_utils_1.assert)(this.seq === constants_1.UnassignedSequenceNumber, 0x045 /* "On insert, seq number already assigned!" */);
                this.seq = opArgs.sequencedMessage.sequenceNumber;
                this.localSeq = undefined;
                return true;
            case ops_1.MergeTreeDeltaType.REMOVE:
                const removalInfo = toRemovalInfo(this);
                (0, common_utils_1.assert)(removalInfo !== undefined, 0x046 /* "On remove ack, missing removal info!" */);
                this.localRemovedSeq = undefined;
                if (removalInfo.removedSeq === constants_1.UnassignedSequenceNumber) {
                    removalInfo.removedSeq = opArgs.sequencedMessage.sequenceNumber;
                    return true;
                }
                return false;
            default:
                throw new Error(`${opArgs.op.type} is in unrecognized operation type`);
        }
    }
    splitAt(pos) {
        var _a;
        if (pos > 0) {
            const leafSegment = this.createSplitSegmentAt(pos);
            if (leafSegment) {
                this.copyPropertiesTo(leafSegment);
                leafSegment.parent = this.parent;
                // Give the leaf a temporary yet valid ordinal.
                // when this segment is put in the tree, it will get it's real ordinal,
                // but this ordinal meets all the necessary invariants for now.
                leafSegment.ordinal = this.ordinal + String.fromCharCode(0);
                leafSegment.removedClientIds = (_a = this.removedClientIds) === null || _a === void 0 ? void 0 : _a.slice();
                leafSegment.removedSeq = this.removedSeq;
                leafSegment.localRemovedSeq = this.localRemovedSeq;
                leafSegment.seq = this.seq;
                leafSegment.localSeq = this.localSeq;
                leafSegment.clientId = this.clientId;
                this.segmentGroups.copyTo(leafSegment);
                this.trackingCollection.copyTo(leafSegment);
                if (this.localRefs) {
                    this.localRefs.split(pos, leafSegment);
                }
            }
            return leafSegment;
        }
    }
    copyPropertiesTo(other) {
        if (this.propertyManager) {
            if (this.properties) {
                other.propertyManager = new segmentPropertiesManager_1.PropertiesManager();
                other.properties = this.propertyManager.copyTo(this.properties, other.properties, other.propertyManager);
            }
        }
    }
}
exports.BaseSegment = BaseSegment;
exports.reservedMarkerIdKey = "markerId";
exports.reservedMarkerSimpleTypeKey = "markerSimpleType";
class Marker extends BaseSegment {
    constructor(refType) {
        super();
        this.refType = refType;
        this.type = Marker.type;
        this.cachedLength = 1;
    }
    static is(segment) {
        return segment.type === Marker.type;
    }
    static make(refType, props) {
        const marker = new Marker(refType);
        if (props) {
            marker.addProperties(props);
        }
        return marker;
    }
    toJSONObject() {
        const obj = { marker: { refType: this.refType } };
        super.addSerializedProps(obj);
        return obj;
    }
    static fromJSONObject(spec) {
        if (spec && typeof spec === "object" && "marker" in spec) {
            return Marker.make(spec.marker.refType, spec.props);
        }
        return undefined;
    }
    clone() {
        const b = Marker.make(this.refType, this.properties);
        this.cloneInto(b);
        return b;
    }
    getSegment() {
        return this;
    }
    getOffset() {
        return 0;
    }
    hasSimpleType(simpleTypeName) {
        return !!this.properties &&
            this.properties[exports.reservedMarkerSimpleTypeKey] === simpleTypeName;
    }
    getProperties() {
        return this.properties;
    }
    getId() {
        if (this.properties && this.properties[exports.reservedMarkerIdKey]) {
            return this.properties[exports.reservedMarkerIdKey];
        }
    }
    /**
     * @deprecated - use refHasTileLabels
     */
    hasTileLabels() {
        return (0, referencePositions_1.refHasTileLabels)(this);
    }
    /**
     * @deprecated - use refHasRangeLabels
     */
    hasRangeLabels() {
        return (0, referencePositions_1.refHasRangeLabels)(this);
    }
    /**
     * @deprecated - use refHasTileLabel
     */
    hasTileLabel(label) {
        return (0, referencePositions_1.refHasTileLabel)(this, label);
    }
    /**
     * @deprecated - use refHasRangeLabel
     */
    hasRangeLabel(label) {
        return (0, referencePositions_1.refHasRangeLabel)(this, label);
    }
    /**
     * @deprecated - use refGetTileLabels
     */
    getTileLabels() {
        return (0, referencePositions_1.refGetTileLabels)(this);
    }
    /**
     * @deprecated - use refGetRangeLabels
     */
    getRangeLabels() {
        return (0, referencePositions_1.refGetRangeLabels)(this);
    }
    toString() {
        let bbuf = "";
        if ((0, referencePositions_1.refTypeIncludesFlag)(this, ops_1.ReferenceType.Tile)) {
            bbuf += "Tile";
        }
        if ((0, referencePositions_1.refTypeIncludesFlag)(this, ops_1.ReferenceType.NestBegin)) {
            if (bbuf.length > 0) {
                bbuf += "; ";
            }
            bbuf += "RangeBegin";
        }
        if ((0, referencePositions_1.refTypeIncludesFlag)(this, ops_1.ReferenceType.NestEnd)) {
            if (bbuf.length > 0) {
                bbuf += "; ";
            }
            bbuf += "RangeEnd";
        }
        let lbuf = "";
        const id = this.getId();
        if (id) {
            bbuf += ` (${id}) `;
        }
        const tileLabels = (0, referencePositions_1.refGetTileLabels)(this);
        if (tileLabels) {
            lbuf += "tile -- ";
            for (let i = 0, len = tileLabels.length; i < len; i++) {
                const tileLabel = tileLabels[i];
                if (i > 0) {
                    lbuf += "; ";
                }
                lbuf += tileLabel;
            }
        }
        const rangeLabels = (0, referencePositions_1.refGetRangeLabels)(this);
        if (rangeLabels) {
            let rangeKind = "begin";
            if ((0, referencePositions_1.refTypeIncludesFlag)(this, ops_1.ReferenceType.NestEnd)) {
                rangeKind = "end";
            }
            if (tileLabels) {
                lbuf += " ";
            }
            lbuf += `range ${rangeKind} -- `;
            const labels = rangeLabels;
            for (let i = 0, len = labels.length; i < len; i++) {
                const rangeLabel = labels[i];
                if (i > 0) {
                    lbuf += "; ";
                }
                lbuf += rangeLabel;
            }
        }
        let pbuf = "";
        if (this.properties) {
            pbuf += JSON.stringify(this.properties, (key, value) => {
                // Avoid circular reference when stringifying makers containing handles.
                // (Substitute a debug string instead.)
                const handle = !!value && value.IFluidHandle;
                // eslint-disable-next-line @typescript-eslint/no-unsafe-return
                return handle
                    ? `#Handle(${handle.routeContext.path}/${handle.path})`
                    : value;
            });
        }
        return `M ${bbuf}: ${lbuf} ${pbuf}`;
    }
    createSplitSegmentAt(pos) {
        return undefined;
    }
    canAppend(segment) {
        return false;
    }
    append() { throw new Error("Can not append to marker"); }
}
exports.Marker = Marker;
Marker.type = "Marker";
var IncrementalExecOp;
(function (IncrementalExecOp) {
    IncrementalExecOp[IncrementalExecOp["Go"] = 0] = "Go";
    IncrementalExecOp[IncrementalExecOp["Stop"] = 1] = "Stop";
    IncrementalExecOp[IncrementalExecOp["Yield"] = 2] = "Yield";
})(IncrementalExecOp = exports.IncrementalExecOp || (exports.IncrementalExecOp = {}));
class IncrementalMapState {
    constructor(block, actions, pos, refSeq, clientId, context, start, end, childIndex = 0) {
        this.block = block;
        this.actions = actions;
        this.pos = pos;
        this.refSeq = refSeq;
        this.clientId = clientId;
        this.context = context;
        this.start = start;
        this.end = end;
        this.childIndex = childIndex;
        this.op = IncrementalExecOp.Go;
    }
}
exports.IncrementalMapState = IncrementalMapState;
class CollaborationWindow {
    constructor() {
        this.clientId = constants_1.LocalClientId;
        this.collaborating = false;
        // Lowest-numbered segment in window; no client can reference a state before this one
        this.minSeq = 0;
        // Highest-numbered segment in window and current
        // reference segment for this client
        this.currentSeq = 0;
        this.localSeq = 0;
    }
    loadFrom(a) {
        this.clientId = a.clientId;
        this.collaborating = a.collaborating;
        this.minSeq = a.minSeq;
        this.currentSeq = a.currentSeq;
    }
}
exports.CollaborationWindow = CollaborationWindow;
const compareNumbers = (a, b) => a - b;
exports.compareNumbers = compareNumbers;
const compareStrings = (a, b) => a.localeCompare(b);
exports.compareStrings = compareStrings;
const indentStrings = ["", " ", "  "];
function internedSpaces(n) {
    if (indentStrings[n] === undefined) {
        indentStrings[n] = "";
        for (let i = 0; i < n; i++) {
            indentStrings[n] += " ";
        }
    }
    return indentStrings[n];
}
exports.internedSpaces = internedSpaces;
exports.clientSeqComparer = {
    min: { refSeq: -1, clientId: "" },
    compare: (a, b) => a.refSeq - b.refSeq,
};
const LRUSegmentComparer = {
    min: { maxSeq: -2 },
    compare: (a, b) => a.maxSeq - b.maxSeq,
};
function applyLeafRangeMarker(marker, searchInfo) {
    for (const rangeLabel of searchInfo.rangeLabels) {
        if ((0, referencePositions_1.refHasRangeLabel)(marker, rangeLabel)) {
            let currentStack = searchInfo.stacks[rangeLabel];
            if (currentStack === undefined) {
                currentStack = new collections_1.Stack();
                searchInfo.stacks[rangeLabel] = currentStack;
            }
            applyRangeReference(currentStack, marker);
        }
    }
}
function recordRangeLeaf(segment, segpos, refSeq, clientId, start, end, searchInfo) {
    if (Marker.is(segment)) {
        if (segment.refType &
            (ops_1.ReferenceType.NestBegin | ops_1.ReferenceType.NestEnd)) {
            applyLeafRangeMarker(segment, searchInfo);
        }
    }
    return false;
}
function rangeShift(node, segpos, refSeq, clientId, offset, end, searchInfo) {
    var _a;
    if (node.isLeaf()) {
        const seg = node;
        if ((((_a = searchInfo.mergeTree.localNetLength(seg)) !== null && _a !== void 0 ? _a : 0) > 0) && Marker.is(seg)) {
            if (seg.refType &
                (ops_1.ReferenceType.NestBegin | ops_1.ReferenceType.NestEnd)) {
                applyLeafRangeMarker(seg, searchInfo);
            }
        }
    }
    else {
        const block = node;
        applyStackDelta(searchInfo.stacks, block.rangeStacks);
    }
    return true;
}
function recordTileStart(segment, segpos, refSeq, clientId, start, end, searchInfo) {
    if (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) && Marker.is(seg)) {
            if ((0, referencePositions_1.refHasTileLabel)(seg, searchInfo.tileLabel)) {
                searchInfo.tile = seg;
            }
        }
    }
    else {
        const block = node;
        let marker;
        if (searchInfo.posPrecedesTile) {
            marker = block.rightmostTiles[searchInfo.tileLabel];
        }
        else {
            marker = block.leftmostTiles[searchInfo.tileLabel];
        }
        if (marker !== undefined) {
            searchInfo.tile = marker;
        }
    }
    return true;
}
const minListenerComparer = {
    min: { minRequired: Number.MIN_VALUE, onMinGE: () => { (0, common_utils_1.assert)(false, 0x048 /* "onMinGE()" */); } },
    compare: (a, b) => a.minRequired - b.minRequired,
};
// Represents a sequence of text segments
class MergeTree {
    // TODO: make and use interface describing options
    constructor(options) {
        this.options = options;
        this.blockUpdateActions = MergeTree.initBlockUpdateActions;
        this.collabWindow = new CollaborationWindow();
        // TODO: add remove on segment remove
        // 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.idToSegment = new Map();
        this.splitLeafSegment = (segment, pos) => {
            if (!(pos > 0 && segment)) {
                return {};
            }
            const next = segment.splitAt(pos);
            if (this.mergeTreeMaintenanceCallback) {
                this.mergeTreeMaintenanceCallback({
                    operation: mergeTreeDeltaCallback_1.MergeTreeMaintenanceType.SPLIT,
                    deltaSegments: [{ segment }, { segment: next }],
                }, undefined);
            }
            return { next };
        };
        this.root = this.makeBlock(0);
    }
    makeBlock(childCount) {
        const block = new HierMergeBlock(childCount);
        block.ordinal = "";
        return block;
    }
    clone() {
        const b = new MergeTree(this.options);
        // For now assume that b will not collaborate
        b.root = b.blockClone(this.root);
    }
    blockClone(block, segments) {
        const bBlock = this.makeBlock(block.childCount);
        for (let i = 0; i < block.childCount; i++) {
            const child = block.children[i];
            if (child.isLeaf()) {
                const segment = this.segmentClone(child);
                bBlock.assignChild(segment, i);
                if (segments) {
                    segments.push(segment);
                }
            }
            else {
                bBlock.assignChild(this.blockClone(child, segments), i);
            }
        }
        this.nodeUpdateLengthNewStructure(bBlock);
        this.nodeUpdateOrdinals(bBlock);
        return bBlock;
    }
    segmentClone(segment) {
        const b = segment.clone();
        return b;
    }
    localNetLength(segment) {
        const removalInfo = toRemovalInfo(segment);
        if (removalInfo !== undefined) {
            return 0;
        }
        else {
            return segment.cachedLength;
        }
    }
    // TODO: remove id when segment removed
    mapIdToSegment(id, segment) {
        this.idToSegment.set(id, segment);
    }
    addNode(block, node) {
        const index = block.childCount++;
        block.assignChild(node, index, false);
        return index;
    }
    /* eslint-disable max-len */
    reloadFromSegments(segments) {
        // This code assumes that a later call to `startCollaboration()` will initialize partial lengths.
        (0, common_utils_1.assert)(!this.collabWindow.collaborating, 0x049 /* "Trying to reload from segments while collaborating!" */);
        const maxChildren = exports.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);
            }
            // eslint-disable-next-line @typescript-eslint/no-unsafe-return
            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);
        }
    }
    /* eslint-enable max-len */
    // 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.segmentsToScour = new collections_1.Heap([], LRUSegmentComparer);
        this.pendingSegments = (0, collections_1.ListMakeHead)();
        this.nodeUpdateLengthNewStructure(this.root, true);
    }
    addToLRUSet(segment, 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 (segment.parent.needsScour !== true && seq > this.collabWindow.currentSeq) {
            segment.parent.needsScour = true;
            this.segmentsToScour.add({ segment, maxSeq: seq });
        }
    }
    underflow(node) {
        return node.childCount < (exports.MaxNodesInBlock / 2);
    }
    scourNode(node, holdNodes) {
        let prevSegment;
        for (let k = 0; k < node.childCount; k++) {
            const childNode = node.children[k];
            if (childNode.isLeaf()) {
                const segment = childNode;
                if (segment.segmentGroups.empty) {
                    if (segment.removedSeq !== undefined) {
                        if (segment.removedSeq > this.collabWindow.minSeq) {
                            holdNodes.push(segment);
                        }
                        else if (!segment.trackingCollection.empty) {
                            holdNodes.push(segment);
                        }
                        else {
                            // Notify maintenance event observers that the segment is being unlinked from the MergeTree
                            if (this.mergeTreeMaintenanceCallback) {
                                this.mergeTreeMaintenanceCallback({
                                    operation: mergeTreeDeltaCallback_1.MergeTreeMaintenanceType.UNLINK,
                                    deltaSegments: [{ segment }],
                                }, undefined);
                            }
                            segment.parent = undefined;
                        }
                        prevSegment = undefined;
                    }
                    else {
                        if (segment.seq <= this.collabWindow.minSeq) {
                            const canAppend = prevSegment
                                && prevSegment.canAppend(segment)
                                && (0, properties_1.matchProperties)(prevSegment.properties, segment.properties)
                                && prevSegment.trackingCollection.matches(segment.trackingCollection)
                                && this.localNetLength(segment) > 0;
                            if (canAppend) {
                                prevSegment.append(segment);
                                if (this.mergeTreeMaintenanceCallback) {
                                    this.mergeTreeMaintenanceCallback({
                                        operation: mergeTreeDeltaCallback_1.MergeTreeMaintenanceType.APPEND,
                                        deltaSegments: [{ segment: prevSegment }, { segment }],
                                    }, undefined);
                                }
                                segment.parent = undefined;
                                segment.trackingCollection.trackingGroups.forEach((tg) => tg.unlink(segment));
                            }
                            else {
                                holdNodes.push(segment);
                                if (this.localNetLength(segment) > 0) {
                                    prevSegment = segment;
                                }
                                else {
                                    prevSegment = undefined;
                                }
                            }
                        }
                        else {
                            holdNodes.push(segment);
                            prevSegment = undefined;
                        }
                    }
                }
                else {
                    holdNodes.push(segment);
                    prevSegment = undefined;
                }
            }
            else {
                holdNodes.push(childNode);
                prevSegment = undefined;
            }
        }
    }
    // Interior node with all node children
    packParent(parent) {
        const children = parent.children;
        let childIndex;
        let childBlock;
        const holdNodes = [];
        for (childIndex = 0; childIndex < parent.childCount; childIndex++) {
            // Debug assert not isLeaf()
            childBlock = children[childIndex];
            this.scourNode(childBlock, holdNodes);
            // Will replace this block with a packed block
            childBlock.parent = undefined;
        }
        const totalNodeCount = holdNodes.length;
        const halfCount = exports.MaxNodesInBlock / 2;
        let childCount = Math.min(exports.MaxNodesInBlock - 1, Math.floor(totalNodeCount / halfCount));
        if (childCount < 1) {
            childCount = 1;
        }
        const baseCount = Math.floor(totalNodeCount / childCount);
        let extraCount = totalNodeCount % childCount;
        const packedBlocks = new Array(exports.MaxNodesInBlock);
        let readCount = 0;
        for (let nodeIndex = 0; nodeIndex < childCount; nodeIndex++) {
            let nodeCount = baseCount;
            if (extraCount > 0) {
                nodeCount++;
                extraCount--;
            }
            const packedBlock = this.makeBlock(nodeCount);
            for (let packedNodeIndex = 0; packedNodeIndex < nodeCount; packedNodeIndex++) {
                const nodeToPack = holdNodes[readCount++];
                packedBlock.assignChild(nodeToPack, packedNodeIndex, false);
            }
            packedBlock.parent = parent;
            packedBlocks[nodeIndex] = packedBlock;
            this.nodeUpdateLengthNewStructure(packedBlock);
        }
        parent.children = packedBlocks;
        for (let j = 0; j < childCount; j++) {
            parent.assignChild(packedBlocks[j], j, false);
        }
        parent.childCount = childCount;
        if (this.underflow(parent) && (parent.parent)) {
            this.packParent(parent.parent);
        }
        else {
            this.nodeUpdateOrdinals(parent);
            this.blockUpdatePathLengths(parent, constants_1.UnassignedSequenceNumber, -1, true);
        }
    }
    zamboniSegments(zamboniSegmentsMaxCount = MergeTree.zamboniSegmentsMaxCount) {
        if (!this.collabWindow.collaborating) {
            return;
        }
        for (let i = 0; i < zamboniSegmentsMaxCount; i++) {
            let segmentToScour = this.segmentsToScour.peek();
            if (!segmentToScour || segmentToScour.maxSeq > this.collabWindow.minSeq) {
                break;
            }
            segmentToScour = this.segmentsToScour.get();
            // Only skip scouring if needs scour is explicitly false, not true or undefined
            if (segmentToScour.segment.parent && segmentToScour.segment.parent.needsScour !== false) {
                const block = segmentToScour.segment.parent;
                const childrenCopy = [];
                this.scourNode(block, childrenCopy);
                // This will avoid the cost of re-scouring nodes
                // that have recently been scoured
                block.needsScour = false;
                const newChildCount = childrenCopy.length;
                if (newChildCount < block.childCount) {
                    block.childCount = newChildCount;
                    block.children = childrenCopy;
                    for (let j = 0; j < newChildCount; j++) {
                        block.assignChild(childrenCopy[j], j, false);
                    }
                    if (this.underflow(block) && block.parent) {
                        this.packParent(block.parent);
                    }
                    else {
                        this.nodeUpdateOrdinals(block);
                        this.blockUpdatePathLengths(block, constants_1.UnassignedSequenceNumber, -1, true);
                    }
                }
            }
        }
    }
    getCollabWindow() {
        return this.collabWindow;
    }
    getStats() {
        const nodeGetStats = (block) => {
            const stats = {
                maxHeight: 0,
                nodeCount: 0,
                leafCount: 0,
                removedLeafCount: 0,
                liveCount: 0,
                histo: [],
            };
            for (let k = 0; k < exports.MaxNodesInBlock; k++) {
                stats.histo[k] = 0;
            }
            for (let i = 0; i < block.childCount; i++) {
                const child = block.children[i];
                let height = 1;
                if (!child.isLeaf()) {
                    const childStats = nodeGetStats(child);
                    height = 1 + childStats.maxHeight;
                    stats.nodeCount += childStats.nodeCount;
                    stats.leafCount += childStats.leafCount;
                    stats.removedLeafCount += childStats.removedLeafCount;
                    stats.liveCount += childStats.liveCount;
                    for (let j = 0; j < exports.MaxNodesInBlock; j++) {
                        stats.histo[j] += childStats.histo[j];
                    }
                }
                else {
                    stats.leafCount++;
                    const segment = child;
                    if (segment.removedSeq !== undefined) {
                        stats.removedLeafCount++;
                    }
                }
                if (height > stats.maxHeight) {
                    stats.maxHeight = height;
                }
            }
            stats.histo[block.childCount]++;
            stats.nodeCount++;
            stats.liveCount += block.childCount;
            return stats;
        };
        const rootStats = nodeGetStats(this.root);
        return rootStats;
    }
    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) {
        var _a;
        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 += (_a = this.nodeLength(child, refSeq, clientId)) !== null && _a !== void 0 ? _a : 0;
            }
            prevParent = parent;
            parent = parent.parent;
        }
        return totalOffset;
    }
    getContainingSegment(pos, refSeq, clientId) {
        let segment;
        let offset;
        const leaf = (leafSeg, segpos, _refSeq, _clientId, start) => {
            segment = leafSeg;
            offset = start;
            return false;
        };
        this.searchBlock(this.root, pos, 0, refSeq, clientId, { leaf }, undefined);
        return { segment, offset };
    }
    /**
     * @internal must only be used by client
     * @param segoff - The segment and offset to slide from
     * @returns The segment and offset to slide to
     */
    _getSlideToSegment(segoff) {
        if (!segoff.segment || !isRemovedAndAcked(segoff.segment)) {
            return segoff;
        }
        // Slide to the next farthest valid segment in the tree. If no such segment is found
        // slide to the last valid segment.
        // TODO this walks the whole tree to find the segment - could write a more efficient
        // walk that starts at the segment
        let foundStart = false;
        let foundSegmentPastStart = false;
        let slideToSegment;
        this.walkAllSegments(this.root, (seg) => {
            if (seg.seq !== constants_1.UnassignedSequenceNumber && !isRemovedAndAcked(seg)) {
                slideToSegment = seg;
                if (foundStart) {
                    foundSegmentPastStart = true;
                    return false;
                }
            }
            if (!foundStart && seg === segoff.segment) {
                foundStart = true;
            }
            return true;
        });
        let offset = 0;
        if (slideToSegment && !foundSegmentPastStart) {
            // If slid nearer then offset should be at the end of the segment
            offset = slideToSegment.cachedLength - 1;
        }
        return { segment: slideToSegment, offset };
    }
    /**
     * This method should only be called when the current client sequence number is
     * max(remove segment sequence number, add reference sequence number).
     * Otherwise eventual consistency is not guaranteed.
     * See `packages\dds\merge-tree\REFERENCEPOSITIONS.md`
     */
    slideReferences(segment, refsToSlide) {
        var _a;
        (0, common_utils_1.assert)(isRemovedAndAcked(segment), 0x2f1 /* slideReferences from a segment which has not been removed and acked */);
        (0, common_utils_1.assert)(!!segment.localRefs, 0x2f2 /* Ref not in the segment localRefs */);
        const newSegoff = this._getSlideToSegment({ segment, offset: 0 });
        const newSegment = newSegoff.segment;
        if (newSegment && !newSegment.localRefs) {
            newSegment.localRefs = new localReference_1.LocalReferenceCollection(newSegment);
        }
        for (const ref of refsToSlide) {
            const removedRef = segment.localRefs.removeLocalRef(ref);
            (0, common_utils_1.assert)(ref === removedRef, 0x2f3 /* Ref not in the segment localRefs */);
            if (!newSegment) {
                // No valid segments (all nodes removed or not yet created)
                ref.segment = undefined;
                ref.offset = 0;
            }
            else {
                ref.segment = newSegment;
                ref.offset = (_a = newSegoff.offset) !== null && _a !== void 0 ? _a : 0;
                (0, common_utils_1.assert)(!!newSegment.localRefs, 0x2f4 /* localRefs must be allocated */);
                newSegment.localRefs.addLocalRef(ref);
            }
        }
        // TODO is it required to update the path lengths?
        if (newSegment) {
            this.blockUpdatePathLengths(newSegment.parent, constants_1.TreeMaintenanceSequenceNumber, constants_1.LocalClientId);
        }
    }
    updateSegmentRefsAfterMarkRemoved(segment, pending) {
        if (!segment.localRefs || segment.localRefs.empty) {
            return;
        }
        const refsToSlide = [];
        const refsToStay = [];
        for (const lref of segment.localRefs) {
            if ((0, referencePositions_1.refTypeIncludesFlag)(lref, ops_1.ReferenceType.StayOnRemove)) {
                refsToStay.push(lref);
            }
            else if ((0, referencePositions_1.refTypeIncludesFlag)(lref, ops_1.ReferenceType.SlideOnRemove)) {
                if (pending) {
                    refsToStay.push(lref);
                }
                else {
                    refsToSlide.push(lref);
                }
            }
        }
        // Rethink implementation of keeping and sliding refs once other reference
        // changes are complete. This works but is fragile and possibly slow.
        if (!pending) {
            this.slideReferences(segment, refsToSlide);
        }
        segment.localRefs.clear();
        for (const lref of refsToStay) {
            lref.segment = segment;
            segment.localRefs.addLocalRef(lref);
        }
    }
    blockLength(node, refSeq, clientId) {
        if ((this.collabWindow.collaborating) && (clientId !== this.collabWindow.clientId)) {
            return node.partialLengths.getPartialLength(refSeq, clientId);
        }
        else {
            return node.cachedLength;
        }
    }
    nodeLength(node, refSeq, clientId) {
        if ((!this.collabWindow.collaborating) || (this.collabWindow.clientId === clientId)) {
            // Local client sees all segments, even when collaborating
            if (!node.isLeaf()) {
                return node.cachedLength;
            }
            else {
                return this.localNetLength(node);
            }
        }
        else {
            // Sequence number within window
            if (!node.isLeaf()) {
                return node.partialLengths.getPartialLength(refSeq, clientId);
            }
            else {
                const segment = node;
                const removalInfo = toRemovalInfo(segment);
                if (removalInfo !== undefined
                    && removalInfo.removedSeq !== constants_1.UnassignedSequenceNumber
                    && removalInfo.removedSeq <= refSeq) {
                    // this segment is a tombstone eligible for zamboni
                    // so should never be considered, as it may not exist
                    // on other clients
                    return undefined;
                }
                if (((segment.clientId === clientId) ||
                    ((segment.seq !== constants_1.UnassignedSequenceNumber) && (segment.seq <= refSeq)))) {
                    // Segment happened by reference sequence number or segment from requesting client
                    if (removalInfo !== undefined) {
                        if (removalInfo.removedClientIds.includes(clientId)) {
                            return 0;
                        }
                        else {
                            return segment.cachedLength;
                        }
                    }
                    else {
                        return segment.cachedLength;
                    }
                }
                else {
                    // the segment was inserted and removed before the
                    // this context, so it will never exist for this
                    // context
                    if (removalInfo !== undefined
                        && removalInfo.removedSeq !== constants_1.UnassignedSequenceNumber) {
                        return undefined;
                    }
                    // Segment invisible to client at reference sequence number/branch id/client id of op
                    return 0;
                }
            }
        }
    }
    addMinSeqListener(minRequired, onMinGE) {
        if (!this.minSeqListeners) {
            this.minSeqListeners = new collections_1.Heap([], minListenerComparer);
        }
        this.minSeqListeners.add({ minRequired, onMinGE });
    }
    notifyMinSeqListeners() {
        if (this.minSeqListeners) {
            while ((this.minSeqListeners.count() > 0) &&
                (this.minSeqListeners.peek().minRequired <= this.collabWindow.minSeq)) {
                const minListener = this.minSeqListeners.get();
                minListener.onMinGE(this.collabWindow.minSeq);
            }
        }
    }
    setMinSeq(minSeq) {
        (0, common_utils_1.assert)(minSeq <= this.collabWindow.currentSeq, 0x04e);
        // Only move forward
        (0, common_utils_1.assert)(this.collabWindow.minSeq <= minSeq, 0x04f /* "minSeq of collab window > target minSeq!" */);
        if (minSeq > this.collabWindow.minSeq) {
            this.collabWindow.minSeq = minSeq;
            if (MergeTree.options.zamboniSegments) {
                this.zamboniSegments();
            }
            this.notifyMinSeqListeners();
        }
    }
    referencePositionToLocalPosition(refPos, refSeq = this.collabWindow.currentSeq, clientId = this.collabWindow.clientId) {
        const seg = refPos.getSegment();
        if (seg && seg.parent) {
            const offset = !seg.removedSeq ? refPos.getOffset() : 0;
            return offset + this.getPosition(seg, refSeq, clientId);
        }
        return localReference_1.LocalReference.DetachedPosition;
    }
    getStackContext(startPos, clientId, rangeLabels) {
        const searchInfo = {
            mergeTree: this,
            stacks: (0, properties_1.createMap)(),
            rangeLabels,
        };
        this.search(startPos, constants_1.UniversalSequenceNumber, clientId, { leaf: recordRangeLeaf, shift: rangeShift }, searchInfo);
        return searchInfo.stacks;
    }
    // TODO: filter function
    findTile(startPos, clientId, tileLabel, posPrecedesTile = true) {
        const searchInfo = {
            mergeTree: this,
            posPrecedesTile,
            tileLabel,
        };
        if (posPrecedesTile) {
            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 = localRef.toPosition();
            }
            return { tile: searchInfo.tile, pos };
        }
    }
    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) {
        var _a;
        let _pos = pos;
        let _segpos = segpos;
        const children = block.children;
        if (actions && actions.pre) {
            actions.pre(block, _segpos, refSeq, clientId, undefined, undefined, clientData);
        }
        const contains = actions && actions.contains;
        for (let childIndex = 0; childIndex < block.childCount; childIndex++) {
            const child = children[childIndex];
            const len = (_a = this.nodeLength(child, refSeq, clientId)) !== null && _a !== void 0 ? _a : 0;
            if ((!contains && _pos < len)
                || (contains && 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);
                }
                else {
                    if (actions && actions.leaf) {
                        actions.leaf(child, _segpos, refSeq, clientId, _pos, -1, clientData);
                    }
                    return child;
                }
            }
            else {
                if (actions && actions.shift) {
                    actions.shift(child, _segpos, refSeq, clientId, _pos, undefined, clientData);
                }
                _pos -= len;
                _segpos += len;
            }
        }
        if (actions && actions.post) {
            actions.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) {
        var _a;
        let _segEnd = segEnd;
        const children = block.children;
        if (actions && actions.pre) {
            actions.pre(block, _segEnd, refSeq, clientId, undefined, undefined, clientData);
        }
        const contains = actions && actions.contains;
        for (let childIndex = block.childCount - 1; childIndex >= 0; childIndex--) {
            const child = children[childIndex];
            const len = (_a = this.nodeLength(child, refSeq, clientId)) !== null && _a !== void 0 ? _a : 0;
            const segpos = _segEnd - len;
            if (((!contains) && (pos >= segpos)) ||
                (contains && 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 {
                    if (actions && actions.leaf) {
                        actions.leaf(child, segpos, refSeq, clientId, pos, -1, clientData);
                    }
                    return child;
                }
            }
            else {
                if (actions && actions.shift) {
                    actions.shift(child, segpos, refSeq, clientId, pos, undefined, clientData);
                }
                _segEnd = segpos;
            }
        }
        if (actions && actions.post) {
            actions.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.dequeue();
        const nodesToUpdate = [];
        let overwrite = false;
        if (pendingSegmentGroup !== undefined) {
            const deltaSegments = [];
            pendingSegmentGroup.segments.map((pendingSegment) => {
                const modified = pendingSegment.ack(pendingSegmentGroup, opArgs, this);
                // This computation of overwrite appears incorrect. Leaving as is to avoid breaking something.
                overwrite = !modified || overwrite;
                if (modified && opArgs.op.type === ops_1.MergeTreeDeltaType.REMOVE) {
                    this.updateSegmentRefsAfterMarkRemoved(pendingSegment, false);
                }
                if (MergeTree.options.zamboniSegments) {
                    this.addToLRUSet(pendingSegment, seq);
                }
                if (!nodesToUpdate.includes(pendingSegment.parent)) {
                    nodesToUpdate.push(pendingSegment.parent);
                }
                deltaSegments.push({
                    segment: pendingSegment,
                });
            });
            if (this.mergeTreeMaintenanceCallback) {
                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);
                // NodeUpdatePathLengths(node, seq, clientId, true);
            }
        }
        if (MergeTree.options.zamboniSegments) {
            this.zamboniSegments();
        }
    }
    addToPendingList(segment, segmentGroup, localSeq) {
        let _segmentGroup = segmentGroup;
        if (_segmentGroup === undefined) {
            // TODO: review the cast
            _segmentGroup = { segments: [], localSeq };
            this.pendingSegments.enqueue(_segmentGroup);
        }
        segment.segmentGroups.enqueue(_segmentGroup);
        return _segmentGroup;
    }
    // TODO: error checking
    getMarkerFromId(id) {
        return this.idToSegment.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 (this.mergeTreeDeltaCallback && 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)) {
            this.zamboniSegments();
        }
    }
    insertAtReferencePosition(referencePosition, insertSegment, opArgs) {
        if (insertSegment.cachedLength === 0) {
            return;
        }
        if (insertSegment.parent
            || insertSegment.removedSeq
            || insertSegment.seq !== constants_1.UniversalSequenceNumber) {
            throw new Error("Cannot insert segment that has already been inserted.");
        }
        const rebalanceTree = (segment) => {
            // Blocks should never be left full
            // if the inserts makes the block full
            // then we need to walk up the chain of parents
            // and split the blocks until we find a block with
            // room
            let block = segment.parent;
            let ordinalUpdateNode = block;
            while (block !== undefined) {
                if (block.childCount >= exports.MaxNodesInBlock) {
                    const splitNode = this.split(block);
                    if (block === this.root) {
                        this.updateRoot(splitNode);
                        // Update root already updates all it's children ordinals
                        ordinalUpdateNode = undefined;
                    }
                    else {
                        this.insertChildNode(block.parent, splitNode, block.index + 1);
                        ordinalUpdateNode = splitNode.parent;
                        this.blockUpdateLength(block.parent, constants_1.UnassignedSequenceNumber, clientId);
                    }
                }
                else {
                    this.blockUpdateLength(block, constants_1.UnassignedSequenceNumber, clientId);
                }
                block = block.parent;
            }
            // Only update ordinals once, for all children,
            // on the path
            if (ordinalUpdateNode) {
                this.nodeUpdateOrdinals(ordinalUpdateNode);
            }
        };
        const clientId = this.collabWindow.clientId;
        const refSegment = referencePosition.getSegment();
        const refOffset = referencePosition.getOffset();
        const refSegLen = this.nodeLength(refSegment, this.collabWindow.currentSeq, clientId);
        let startSeg = refSegment;
        // if the change isn't at a boundary, we need to split the segment
        if (refOffset !== 0 && refSegLen !== undefined && refSegLen !== 0) {
            const splitSeg = this.splitLeafSegment(refSegment, refOffset);
            (0, common_utils_1.assert)(!!splitSeg.next, 0x050 /* "Next segment changes are undefined!" */);
            this.insertChildNode(refSegment.parent, splitSeg.next, refSegment.index + 1);
            rebalanceTree(splitSeg.next);
            startSeg = splitSeg.next;
        }
        // walk back from the segment, to see if there is a previous tie break seg
        this.leftExcursion(startSeg, (backSeg) => {
            if (!backSeg.isLeaf()) {
                return true;
            }
            const backLen = this.nodeLength(backSeg, this.collabWindow.currentSeq, clientId);
            // ignore removed segments
            if (backLen === undefined) {
                return true;
            }
            // Find the nearest 0 length seg we can insert over, as all other inserts
            // go near to far
            if (backLen === 0) {
                if (this.breakTie(0, backSeg, constants_1.UnassignedSequenceNumber)) {
                    startSeg = backSeg;
                }
                return true;
            }
            return false;
        });
        if (this.collabWindow.collaborating) {
            insertSegment.localSeq = ++this.collabWindow.localSeq;
            insertSegment.seq = constants_1.UnassignedSequenceNumber;
        }
        else {
            insertSegment.seq = constants_1.UniversalSequenceNumber;
        }
        insertSegment.clientId = clientId;
        if (Marker.is(insertSegment)) {
            const markerId = insertSegment.getId();
            if (markerId) {
                this.mapIdToSegment(markerId, insertSegment);
            }
        }
        this.insertChildNode(startSeg.parent, insertSegment, startSeg.index);
        rebalanceTree(insertSegment);
        if (this.mergeTreeDeltaCallback) {
            this.mergeTreeDeltaCallback(opArgs, {
                deltaSegments: [{ segment: insertSegment }],
                operation: ops_1.MergeTreeDeltaType.INSERT,
            });
        }
        if (this.collabWindow.collaborating) {
            this.addToPendingList(insertSegment, undefined, insertSegment.localSeq);
        }
    }
    /**
     * 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 segwindow = this.getCollabWindow();
        if (segmentInfo && segmentInfo.segment) {
            const segmentPosition = this.getPosition(segmentInfo.segment, segwindow.currentSeq, segwindow.clientId);
            return segmentPosition + segmentInfo.offset;
        }
        else {
            if (remoteClientPosition === this.getLength(remoteClientRefSeq, remoteClientId)) {
                return this.getLength(segwindow.currentSeq, segwindow.clientId);
            }
        }
    }
    insertChildNode(block, child, childIndex) {
        (0, common_utils_1.assert)(block.childCount < exports.MaxNodesInBlock, 0x051 /* "Too many children on merge block!" */);
        for (let i = block.childCount; i > childIndex; i--) {
            block.children[i] = block.children[i - 1];
            block.children[i].index = i;
        }
        block.childCount++;
        block.assignChild(child, childIndex, false);
    }
    blockInsert(pos, refSeq, clientId, seq, localSeq, newSegments) {
        let segIsLocal = false;
        const checkSegmentIsLocal = (segment) => {
            if (segment.seq === constants_1.UnassignedSequenceNumber) {
                segIsLocal = true;
            }
            // Only need to look at first segment that follows finished node
            return false;
        };
        const continueFrom = (node) => {
            segIsLocal = false;
            this.rightExcursion(node, checkSegmentIsLocal);
            return segIsLocal;
        };
        let segmentGroup;
        const saveIfLocal = (locSegment) => {
            // Save segment so 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);
                    // eslint-disable-next-line @typescript-eslint/brace-style
                }
                // 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) {
            segIsLocal = false;
            if (newSegment.cachedLength > 0) {
                newSegment.seq = seq;
                newSegment.localSeq = localSeq;
                newSegment.clientId = clientId;
                if (Marker.is(newSegment)) {
                    const markerId = newSegment.getId();
                    if (markerId) {
                        this.mapIdToSegment(markerId, newSegment);
                    }
                }
                const splitNode = this.insertingWalk(this.root, insertPos, refSeq, clientId, seq, { leaf: onLeaf, candidateSegment: newSegment, continuePredicate: continueFrom });
                if (newSegment.parent === undefined) {
                    throw new Error(`MergeTree insert failed: ${JSON.stringify({
                        currentSeq: this.collabWindow.currentSeq,
                        minSeq: this.collabWindow.minSeq,
                        segSeq: newSegment.seq,
                    })}`);
                }
                this.updateRoot(splitNode);
                saveIfLocal(newSegment);
                insertPos += newSegment.cachedLength;
            }
        }
    }
    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) {
        var _a;
        if (node.isLeaf()) {
            if (pos === 0) {
                // 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 : (_a = node.seq) !== null && _a !== void 0 ? _a : 0;
                return newSeq > segSeq;
            }
            return false;
        }
        else {
            return true;
        }
    }
    // Visit segments starting from node's left siblings, then up to node's parent
    leftExcursion(node, leafAction) {
        let go = true;
        let startNode = node;
        let parent = startNode.parent;
        while (parent) {
            const children = parent.children;
            let childIndex;
            let _node;
            let matchedStart = false;
            for (childIndex = parent.childCount - 1; childIndex >= 0; childIndex--) {
                _node = children[childIndex];
                if (matchedStart) {
                    if (!_node.isLeaf()) {
                        const childBlock = _node;
                        go = this.nodeMapReverse(childBlock, leafAction, 0, constants_1.UniversalSequenceNumber, this.collabWindow.clientId);
                    }
                    else {
                        go = leafAction(_node, 0, constants_1.UniversalSequenceNumber, this.collabWindow.clientId, 0, 0, undefined);
                    }
                    if (!go) {
                        return;
                    }
                }
                else {
                    matchedStart = (startNode === _node);
                }
            }
            startNode = parent;
            parent = parent.parent;
        }
    }
    /**
     * Visit segments starting from node's right siblings, then up to node's parent.
     * All segments past `node` are visited, regardless of their visibility.
     */
    rightExcursion(node, leafAction) {
        let go = true;
        let startNode = node;
        let parent = startNode.parent;
        while (parent) {
            const children = parent.children;
            let childIndex;
            let _node;
            let matchedStart = false;
            for (childIndex = 0; childIndex < parent.childCount; childIndex++) {
                _node = children[childIndex];
                if (matchedStart) {
                    if (!_node.isLeaf()) {
                        const childBlock = _node;
                        go = this.walkAllSegments(childBlock, leafAction);
                    }
                    else {
                        go = leafAction(_node);
                    }
                    if (!go) {
                        return;
                    }
                }
                else {
                    matchedStart = (startNode === _node);
                }
            }
            startNode = parent;
            parent = parent.parent;
        }
    }
    insertingWalk(block, pos, refSeq, clientId, seq, context) {
        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];
            const len = this.nodeLength(child, refSeq, clientId);
            if (len === undefined) {
                // if the seg len in undefined, the segment
                // will be removed, so should just be skipped for now
                continue;
            }
            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);
                    if (splitNode === undefined) {
                        if (context.structureChange) {
                            this.nodeUpdateLengthNewStructure(block);
                        }
                        else {
                            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
                        if (context.structureChange) {
                            this.nodeUpdateLengthNewStructure(block);
                        }
                        return undefined;
                    }
                }
                break;
            }
            else {
                _pos -= len;
            }
        }
        if (!newNode) {
            if (_pos === 0) {
                if ((seq !== constants_1.UnassignedSequenceNumber) && context.continuePredicate &&
                    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 < exports.MaxNodesInBlock) {
                if (fromSplit) {
                    this.nodeUpdateOrdinals(fromSplit);
                }
                if (context.structureChange) {
                    this.nodeUpdateLengthNewStructure(block);
                }
                else {
                    this.blockUpdateLength(block, seq, clientId);
                }
                return undefined;
            }
            else {
                // Don't update ordinals because higher block will do it
                return this.split(block);
            }
        }
        else {
            return undefined;
        }
    }
    split(node) {
        const halfCount = exports.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 combiningOp - Optional. Specifies how to combine values for the property, such as "incr" for increment.
     * @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
     */
    annotateRange(start, end, props, combiningOp, refSeq, clientId, seq, opArgs) {
        this.ensureIntervalBoundary(start, refSeq, clientId);
        this.ensureIntervalBoundary(end, refSeq, clientId);
        const deltaSegments = [];
        const localSeq = seq === constants_1.UnassignedSequenceNumber ? ++this.collabWindow.localSeq : undefined;
        let segmentGroup;
        const annotateSegment = (segment) => {
            const propertyDeltas = segment.addProperties(props, combiningOp, seq, this.collabWindow);
            deltaSegments.push({ segment, propertyDeltas });
            if (this.collabWindow.collaborating) {
                if (seq === constants_1.UnassignedSequenceNumber) {
                    segmentGroup = this.addToPendingList(segment, segmentGroup, localSeq);
                }
                else {
                    if (MergeTree.options.zamboniSegments) {
                        this.addToLRUSet(segment, seq);
                    }
                }
            }
            return true;
        };
        this.mapRange({ leaf: annotateSegment }, refSeq, clientId, undefined, start, end);
        // OpArgs == undefined => test code
        if (this.mergeTreeDeltaCallback && deltaSegments.length > 0) {
            this.mergeTreeDeltaCallback(opArgs, {
                operation: ops_1.MergeTreeDeltaType.ANNOTATE,
                deltaSegments,
            });
        }
        if (this.collabWindow.collaborating && (seq !== constants_1.UnassignedSequenceNumber)) {
            if (MergeTree.options.zamboniSegments) {
                this.zamboniSegments();
            }
        }
    }
    markRangeRemoved(start, end, refSeq, clientId, seq, overwrite = false, opArgs) {
        let _overwrite = overwrite;
        this.ensureIntervalBoundary(start, refSeq, clientId);
        this.ensureIntervalBoundary(end, refSeq, clientId);
        let segmentGroup;
        const removedSegments = [];
        const segmentsWithRefs = [];
        const localSeq = seq === constants_1.UnassignedSequenceNumber ? ++this.collabWindow.localSeq : undefined;
        const markRemoved = (segment, pos, _start, _end) => {
            const existingRemovalInfo = 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 first removal at the head.
                    existingRemovalInfo.removedClientIds.unshift(clientId);
                    existingRemovalInfo.removedSeq = seq;
                    segment.localRemovedSeq = undefined;
                }
                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 });
            }
            if (segment.localRefs && !segment.localRefs.empty) {
                segmentsWithRefs.push(segment);
            }
            // Save segment so 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.mapRange({ leaf: markRemoved, post: afterMarkRemoved }, refSeq, clientId, undefined, start, end);
        const pending = this.collabWindow.collaborating && clientId === this.collabWindow.clientId;
        for (const segment of segmentsWithRefs) {
            this.updateSegmentRefsAfterMarkRemoved(segment, pending);
        }
        // opArgs == undefined => test code
        if (this.mergeTreeDeltaCallback && removedSegments.length > 0) {
            this.mergeTreeDeltaCallback(opArgs, {
                operation: ops_1.MergeTreeDeltaType.REMOVE,
                deltaSegments: removedSegments,
            });
        }
        if (this.collabWindow.collaborating && (seq !== constants_1.UnassignedSequenceNumber)) {
            if (MergeTree.options.zamboniSegments) {
                this.zamboniSegments();
            }
        }
    }
    nodeUpdateLengthNewStructure(node, recur = false) {
        this.blockUpdate(node);
        if (this.collabWindow.collaborating) {
            node.partialLengths = partialLengths_1.PartialSequenceLengths.combine(this, node, this.collabWindow, recur);
        }
    }
    removeLocalReferencePosition(lref) {
        var _a;
        const segment = lref.getSegment();
        if (segment) {
            const removedRefs = (_a = segment === null || segment === void 0 ? void 0 : segment.localRefs) === null || _a === void 0 ? void 0 : _a.removeLocalRef(lref);
            if (removedRefs !== undefined) {
                this.blockUpdatePathLengths(segment.parent, constants_1.TreeMaintenanceSequenceNumber, constants_1.LocalClientId);
            }
            return removedRefs;
        }
    }
    createLocalReferencePosition(segment, offset, refType, properties, client) {
        var _a;
        if (isRemoved(segment)) {
            if (!(0, referencePositions_1.refTypeIncludesFlag)(refType, ops_1.ReferenceType.SlideOnRemove)) {
                throw new container_utils_1.UsageError("Can only create SlideOnRemove local reference position on a removed segment");
            }
        }
        const localRefs = (_a = segment.localRefs) !== null && _a !== void 0 ? _a : new localReference_1.LocalReferenceCollection(segment);
        segment.localRefs = localRefs;
        const segRef = localRefs.createLocalRef(offset, refType, properties, client);
        this.blockUpdatePathLengths(segment.parent, constants_1.TreeMaintenanceSequenceNumber, constants_1.LocalClientId);
        return segRef;
    }
    /**
     * @deprecated - use removeLocalReferencePosition
     */
    removeLocalReference(segment, lref) {
        if (segment.localRefs) {
            const removedRef = segment.localRefs.removeLocalRef(lref);
            if (removedRef) {
                this.blockUpdatePathLengths(segment.parent, constants_1.TreeMaintenanceSequenceNumber, constants_1.LocalClientId);
            }
        }
    }
    /**
     * @deprecated - use createLocalReference
     */
    addLocalReference(lref) {
        const segment = lref.segment;
        let localRefs = segment.localRefs;
        if (!localRefs) {
            localRefs = new localReference_1.LocalReferenceCollection(segment);
            segment.localRefs = localRefs;
        }
        localRefs.addLocalRef(lref);
        this.blockUpdatePathLengths(segment.parent, constants_1.TreeMaintenanceSequenceNumber, constants_1.LocalClientId);
    }
    blockUpdate(block) {
        var _a;
        let len = 0;
        const hierBlock = block.hierBlock();
        if (hierBlock) {
            hierBlock.rightmostTiles = (0, properties_1.createMap)();
            hierBlock.leftmostTiles = (0, properties_1.createMap)();
            hierBlock.rangeStacks = {};
        }
        for (let i = 0; i < block.childCount; i++) {
            const child = block.children[i];
            len += (_a = nodeTotalLength(this, child)) !== null && _a !== void 0 ? _a : 0;
            if (hierBlock) {
                hierBlock.addNodeReferences(this, child);
            }
            if (this.blockUpdateActions) {
                this.blockUpdateActions.child(block, i);
            }
        }
        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);
        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(this, node, seq, clientId, this.collabWindow);
            }
            else {
                node.partialLengths = partialLengths_1.PartialSequenceLengths.combine(this, node, this.collabWindow);
            }
        }
    }
    map(actions, refSeq, clientId, accum) {
        // TODO: optimize to avoid comparisons
        this.nodeMap(this.root, actions, 0, refSeq, clientId, accum);
    }
    mapRange(actions, refSeq, clientId, accum, start, end, splitRange = false) {
        if (splitRange) {
            if (start) {
                this.ensureIntervalBoundary(start, refSeq, clientId);
            }
            if (end) {
                this.ensureIntervalBoundary(end, refSeq, clientId);
            }
        }
        this.nodeMap(this.root, actions, 0, refSeq, clientId, accum, start, end);
    }
    incrementalBlockMap(stateStack) {
        var _a;
        while (!stateStack.empty()) {
            // We already check the stack is not empty
            const state = stateStack.top();
            if (state.op !== IncrementalExecOp.Go) {
                return;
            }
            if (state.childIndex === 0) {
                if (state.start === undefined) {
                    state.start = 0;
                }
                if (state.end === undefined) {
                    state.end = this.blockLength(state.block, state.refSeq, state.clientId);
                }
                if (state.actions.pre) {
                    state.actions.pre(state);
                }
            }
            if ((state.op === IncrementalExecOp.Go) && (state.childIndex < state.block.childCount)) {
                const child = state.block.children[state.childIndex];
                const len = (_a = this.nodeLength(child, state.refSeq, state.clientId)) !== null && _a !== void 0 ? _a : 0;
                if ((len > 0) && (state.start < len) && (state.end > 0)) {
                    if (!child.isLeaf()) {
                        const childState = new IncrementalMapState(child, state.actions, state.pos, state.refSeq, state.clientId, state.context, state.start, state.end, 0);
                        stateStack.push(childState);
                    }
                    else {
                        state.actions.leaf(child, state);
                    }
                }
                state.pos += len;
                state.start -= len;
                state.end -= len;
                state.childIndex++;
            }
            else {
                if (state.childIndex === state.block.childCount) {
                    if ((state.op === IncrementalExecOp.Go) && state.actions.post) {
                        state.actions.post(state);
                    }
                    stateStack.pop();
                }
            }
        }
    }
    nodeMap(node, actions, pos, refSeq, clientId, accum, start, end) {
        var _a;
        let _start = start;
        let _end = end;
        let _pos = pos;
        if (_start === undefined) {
            _start = 0;
        }
        if (_end === undefined) {
            _end = this.blockLength(node, refSeq, clientId);
        }
        let go = true;
        if (actions.pre) {
            go = actions.pre(node, _pos, refSeq, clientId, _start, _end, accum);
            if (!go) {
                // Cancel this node but not entire traversal
                return true;
            }
        }
        const children = node.children;
        for (let childIndex = 0; childIndex < node.childCount; childIndex++) {
            const child = children[childIndex];
            const len = (_a = this.nodeLength(child, refSeq, clientId)) !== null && _a !== void 0 ? _a : 0;
            if (go && (_end > 0) && (len > 0) && (_start < len)) {
                // Found entry containing pos
                if (!child.isLeaf()) {
                    if (go) {
                        go = this.nodeMap(child, actions, _pos, refSeq, clientId, accum, _start, _end);
                    }
                }
                else {
                    if (actions.leaf) {
                        go = actions.leaf(child, _pos, refSeq, clientId, _start, _end, accum);
                    }
                }
            }
            if (!go) {
                break;
            }
            if (actions.shift) {
                actions.shift(child, _pos, refSeq, clientId, _start, _end, accum);
            }
            _pos += len;
            _start -= len;
            _end -= len;
        }
        if (go && actions.post) {
            go = actions.post(node, _pos, refSeq, clientId, _start, _end, accum);
        }
        return go;
    }
    // Invokes the leaf action for all segments.  Note that *all* segments are visited
    // regardless of if they would be visible to the current `clientId` and `refSeq`.
    walkAllSegments(block, action, accum) {
        let go = true;
        const children = block.children;
        for (let childIndex = 0; go && childIndex < block.childCount; childIndex++) {
            const child = children[childIndex];
            go = child.isLeaf()
                ? action(child, accum)
                : this.walkAllSegments(child, action, accum);
        }
        return go;
    }
    // Straight call every segment; goes until leaf action returns false
    nodeMapReverse(block, leafAction, pos, refSeq, clientId) {
        let go = true;
        const children = block.children;
        for (let childIndex = block.childCount - 1; childIndex >= 0; childIndex--) {
            const child = children[childIndex];
            if (go) {
                // Found entry containing pos
                if (!child.isLeaf()) {
                    if (go) {
                        go = this.nodeMapReverse(child, leafAction, pos, refSeq, clientId);
                    }
                }
                else {
                    go = leafAction(child, pos, refSeq, clientId, 0, 0, undefined);
                }
            }
            if (!go) {
                break;
            }
        }
        return go;
    }
}
exports.MergeTree = MergeTree;
MergeTree.zamboniSegmentsMaxCount = 2;
MergeTree.options = {
    incrementalUpdate: true,
    insertAfterRemovedSegs: true,
    zamboniSegments: true,
};
MergeTree.theUnfinishedNode = { childCount: -1 };
//# sourceMappingURL=mergeTree.js.map