/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { UnassignedSequenceNumber } from "./constants.js";
import { MergeTreeMaintenanceType } from "./mergeTreeDeltaCallback.js";
import { assignChild, getMinSeqStamp, Marker, MaxNodesInBlock, } from "./mergeTreeNodes.js";
import { matchProperties } from "./properties.js";
import { toRemovalInfo, removeMergeNodeInfo } from "./segmentInfos.js";
import * as opstampUtils from "./stamps.js";
export const zamboniSegmentsMax = 2;
function underflow(node) {
    return node.childCount < MaxNodesInBlock / 2;
}
// blockUpdatePathLengths requires an OperationStamp but it is unused when passing `newStructure: true`.
const dummyStamp = { seq: UnassignedSequenceNumber, clientId: -1 };
export function zamboniSegments(mergeTree, zamboniSegmentsMaxCount = zamboniSegmentsMax) {
    if (!mergeTree.collabWindow.collaborating) {
        return;
    }
    for (let i = 0; i < zamboniSegmentsMaxCount; i++) {
        let segmentToScour = mergeTree.segmentsToScour.peek()?.value;
        segmentToScour?.segment?.propertyManager?.updateMsn(mergeTree.collabWindow.minSeq);
        if (!segmentToScour || segmentToScour.maxSeq > mergeTree.collabWindow.minSeq) {
            break;
        }
        segmentToScour = mergeTree.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 = [];
            scourNode(block, childrenCopy, mergeTree);
            // 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++) {
                    assignChild(block, childrenCopy[j], j, false);
                }
                if (underflow(block) && block.parent) {
                    packParent(block.parent, mergeTree);
                }
                else {
                    mergeTree.nodeUpdateOrdinals(block);
                    mergeTree.blockUpdatePathLengths(block, dummyStamp, true);
                }
            }
        }
    }
}
// Interior node with all node children
export function packParent(parent, mergeTree) {
    const children = parent.children;
    let childIndex;
    let childBlock;
    const holdNodes = [];
    for (childIndex = 0; childIndex < parent.childCount; childIndex++) {
        // Debug assert not isLeaf()
        childBlock = children[childIndex];
        scourNode(childBlock, holdNodes, mergeTree);
        // Will replace this block with a packed block
        childBlock.parent = undefined;
    }
    if (holdNodes.length > 0) {
        const totalNodeCount = holdNodes.length;
        const halfOfMaxNodeCount = MaxNodesInBlock / 2;
        let childCount = Math.min(MaxNodesInBlock - 1, Math.floor(totalNodeCount / halfOfMaxNodeCount));
        if (childCount < 1) {
            childCount = 1;
        }
        const baseNodesInBlockCount = Math.floor(totalNodeCount / childCount);
        let remainderCount = totalNodeCount % childCount;
        const packedBlocks = Array.from({ length: MaxNodesInBlock });
        let childrenPackedCount = 0;
        for (let nodeIndex = 0; nodeIndex < childCount; nodeIndex++) {
            let nodeCount = baseNodesInBlockCount;
            if (remainderCount > 0) {
                nodeCount++;
                remainderCount--;
            }
            const packedBlock = mergeTree.makeBlock(nodeCount);
            for (let packedNodeIndex = 0; packedNodeIndex < nodeCount; packedNodeIndex++) {
                const nodeToPack = holdNodes[childrenPackedCount++];
                assignChild(packedBlock, nodeToPack, packedNodeIndex, false);
            }
            packedBlock.parent = parent;
            packedBlocks[nodeIndex] = packedBlock;
            mergeTree.nodeUpdateLengthNewStructure(packedBlock);
        }
        parent.children = packedBlocks;
        for (let j = 0; j < childCount; j++) {
            assignChild(parent, packedBlocks[j], j, false);
        }
        parent.childCount = childCount;
    }
    else {
        parent.children = [];
        parent.childCount = 0;
    }
    if (underflow(parent) && parent.parent) {
        packParent(parent.parent, mergeTree);
    }
    else {
        mergeTree.nodeUpdateOrdinals(parent);
        mergeTree.blockUpdatePathLengths(parent, dummyStamp, true);
    }
}
function scourNode(node, holdNodes, mergeTree) {
    // The previous segment is tracked while scouring for the purposes of merging adjacent segments
    // when possible.
    let prevSegment;
    for (let k = 0; k < node.childCount; k++) {
        // TODO Non null asserting, why is this not null?
        const childNode = node.children[k];
        if (!childNode.isLeaf() || childNode.segmentGroups?.empty === false) {
            holdNodes.push(childNode);
            prevSegment = undefined;
            continue;
        }
        const segment = childNode;
        const removalInfo = toRemovalInfo(segment);
        const minSeqStamp = getMinSeqStamp(mergeTree.collabWindow);
        if (removalInfo === undefined) {
            if (opstampUtils.lte(segment.insert, minSeqStamp)) {
                const segmentHasPositiveLength = (mergeTree.leafLength(segment) ?? 0) > 0;
                const canAppend = prevSegment?.canAppend(segment) &&
                    matchProperties(prevSegment.properties, segment.properties) &&
                    prevSegment.trackingCollection.matches(segment.trackingCollection) &&
                    segmentHasPositiveLength;
                if (canAppend) {
                    prevSegment.append(segment);
                    mergeTree.mergeTreeMaintenanceCallback?.({
                        operation: MergeTreeMaintenanceType.APPEND,
                        deltaSegments: [{ segment: prevSegment }, { segment }],
                    }, undefined);
                    for (const tg of segment.trackingCollection.trackingGroups)
                        tg.unlink(segment);
                    removeMergeNodeInfo(segment);
                }
                else {
                    holdNodes.push(segment);
                    prevSegment = segmentHasPositiveLength ? segment : undefined;
                }
            }
            else {
                holdNodes.push(segment);
                prevSegment = undefined;
            }
        }
        else {
            const firstRemove = removalInfo.removes[0];
            // If the segment's removal is below the MSN and it's not being held onto by a tracking group,
            // it can be unlinked (i.e. removed from the merge-tree)
            if (!!firstRemove &&
                opstampUtils.lte(firstRemove, minSeqStamp) &&
                segment.trackingCollection.empty) {
                mergeTree.mergeTreeMaintenanceCallback?.({
                    operation: MergeTreeMaintenanceType.UNLINK,
                    deltaSegments: [{ segment }],
                }, undefined);
                if (Marker.is(segment)) {
                    mergeTree.unlinkMarker(segment);
                }
                removeMergeNodeInfo(segment);
            }
            else {
                holdNodes.push(segment);
            }
            prevSegment = undefined;
        }
    }
}
//# sourceMappingURL=zamboni.js.map