"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.debugMarkerToString = exports.compareStrings = exports.compareNumbers = exports.CollaborationWindow = exports.Marker = exports.reservedMarkerSimpleTypeKey = exports.reservedMarkerIdKey = exports.BaseSegment = exports.seqLTE = exports.MergeBlock = exports.MaxNodesInBlock = exports.MergeNode = exports.toMoveInfo = exports.toRemovalInfo = void 0;
/* eslint-disable @typescript-eslint/no-non-null-assertion */
const core_utils_1 = require("@fluidframework/core-utils");
const constants_1 = require("./constants.cjs");
const localReference_1 = require("./localReference.cjs");
const mergeTreeTracking_1 = require("./mergeTreeTracking.cjs");
const ops_1 = require("./ops.cjs");
const ordinal_1 = require("./ordinal.cjs");
// eslint-disable-next-line import/no-deprecated
const properties_1 = require("./properties.cjs");
const referencePositions_1 = require("./referencePositions.cjs");
const segmentGroupCollection_1 = require("./segmentGroupCollection.cjs");
const segmentPropertiesManager_1 = require("./segmentPropertiesManager.cjs");
/**
 * @internal
 */
function toRemovalInfo(maybe) {
    if (maybe?.removedClientIds !== undefined && maybe?.removedSeq !== undefined) {
        return maybe;
    }
    (0, core_utils_1.assert)(maybe?.removedClientIds === undefined && maybe?.removedSeq === undefined, 0x2bf /* "both removedClientIds and removedSeq should be set or not set" */);
}
exports.toRemovalInfo = toRemovalInfo;
function toMoveInfo(maybe) {
    if (maybe?.movedClientIds !== undefined && maybe?.movedSeq !== undefined) {
        return maybe;
    }
    (0, core_utils_1.assert)(maybe?.movedClientIds === undefined &&
        maybe?.movedSeq === undefined &&
        maybe?.movedSeqs === undefined &&
        maybe?.wasMovedOnInsert === undefined, 0x86d /* movedClientIds, movedSeq, wasMovedOnInsert, and movedSeqs should all be either set or not set */);
}
exports.toMoveInfo = toMoveInfo;
/**
 * @alpha
 */
class MergeNode {
    constructor() {
        this.index = 0;
        this.ordinal = "";
        this.cachedLength = 0;
    }
    isLeaf() {
        return false;
    }
}
exports.MergeNode = MergeNode;
/**
 * 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.)
 * @internal
 */
exports.MaxNodesInBlock = 8;
/**
 * @internal
 */
class MergeBlock extends MergeNode {
    constructor(childCount) {
        super();
        this.childCount = childCount;
        this.children = new Array(exports.MaxNodesInBlock);
    }
    hierBlock() {
        return undefined;
    }
    setOrdinal(child, index) {
        const childCount = this.childCount;
        (0, core_utils_1.assert)(childCount >= 1 && childCount <= exports.MaxNodesInBlock, 0x040 /* "Child count is not within [1,8] range!" */);
        child.ordinal = (0, ordinal_1.computeHierarchicalOrdinal)(exports.MaxNodesInBlock, childCount, this.ordinal, index === 0 ? undefined : this.children[index - 1]?.ordinal);
    }
    assignChild(child, index, updateOrdinal = true) {
        child.parent = this;
        child.index = index;
        if (updateOrdinal) {
            this.setOrdinal(child, index);
        }
        this.children[index] = child;
    }
}
exports.MergeBlock = MergeBlock;
function seqLTE(seq, minOrRefSeq) {
    return seq !== constants_1.UnassignedSequenceNumber && seq <= minOrRefSeq;
}
exports.seqLTE = seqLTE;
/**
 * @alpha
 */
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, seq, collaborating, rollback = segmentPropertiesManager_1.PropertiesRollback.None) {
        this.propertyManager ?? (this.propertyManager = new segmentPropertiesManager_1.PropertiesManager());
        // eslint-disable-next-line import/no-deprecated
        this.properties ?? (this.properties = (0, properties_1.createMap)());
        return this.propertyManager.addProperties(this.properties, newProps, seq, collaborating, rollback);
    }
    hasProperty(key) {
        return !!this.properties && this.properties[key] !== undefined;
    }
    isLeaf() {
        return true;
    }
    cloneInto(b) {
        b.clientId = this.clientId;
        // TODO: deep clone properties
        // eslint-disable-next-line import/no-deprecated
        b.properties = (0, properties_1.clone)(this.properties);
        b.removedClientIds = this.removedClientIds?.slice();
        // TODO: copy removed client overlap and branch removal info
        b.removedSeq = this.removedSeq;
        b.movedClientIds = this.movedClientIds?.slice();
        b.movedSeq = this.movedSeq;
        b.movedSeqs = this.movedSeqs;
        b.wasMovedOnInsert = this.wasMovedOnInsert;
        b.seq = this.seq;
        b.attribution = this.attribution?.clone();
    }
    canAppend(segment) {
        return false;
    }
    addSerializedProps(jseg) {
        if (this.properties) {
            jseg.props = this.properties;
        }
    }
    /***/
    ack(segmentGroup, opArgs) {
        const currentSegmentGroup = this.segmentGroups.dequeue();
        (0, core_utils_1.assert)(currentSegmentGroup === segmentGroup, 0x043 /* "On ack, unexpected segmentGroup!" */);
        switch (opArgs.op.type) {
            case ops_1.MergeTreeDeltaType.ANNOTATE:
                (0, core_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, core_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, core_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;
            case ops_1.MergeTreeDeltaType.OBLITERATE:
                const moveInfo = toMoveInfo(this);
                (0, core_utils_1.assert)(moveInfo !== undefined, 0x86e /* On obliterate ack, missing move info! */);
                this.localMovedSeq = undefined;
                const seqIdx = moveInfo.movedSeqs.indexOf(constants_1.UnassignedSequenceNumber);
                (0, core_utils_1.assert)(seqIdx !== -1, 0x86f /* expected movedSeqs to contain unacked seq */);
                moveInfo.movedSeqs[seqIdx] = opArgs.sequencedMessage.sequenceNumber;
                if (moveInfo.movedSeq === constants_1.UnassignedSequenceNumber) {
                    moveInfo.movedSeq = opArgs.sequencedMessage.sequenceNumber;
                    return true;
                }
                return false;
            default:
                throw new Error(`${opArgs.op.type} is in unrecognized operation type`);
        }
    }
    splitAt(pos) {
        if (pos <= 0) {
            return undefined;
        }
        const leafSegment = this.createSplitSegmentAt(pos);
        if (!leafSegment) {
            return undefined;
        }
        this.copyPropertiesTo(leafSegment);
        // eslint-disable-next-line @typescript-eslint/no-this-alias
        const thisAsMergeSegment = this;
        leafSegment.parent = thisAsMergeSegment.parent;
        // Give the leaf a temporary yet valid ordinal.
        // when this segment is put in the tree, it will get its real ordinal,
        // but this ordinal meets all the necessary invariants for now.
        leafSegment.ordinal = this.ordinal + String.fromCharCode(0);
        leafSegment.removedClientIds = this.removedClientIds?.slice();
        leafSegment.removedSeq = this.removedSeq;
        leafSegment.localRemovedSeq = this.localRemovedSeq;
        leafSegment.seq = this.seq;
        leafSegment.localSeq = this.localSeq;
        leafSegment.clientId = this.clientId;
        leafSegment.movedClientIds = this.movedClientIds?.slice();
        leafSegment.movedSeq = this.movedSeq;
        leafSegment.movedSeqs = this.movedSeqs?.slice();
        leafSegment.localMovedSeq = this.localMovedSeq;
        leafSegment.wasMovedOnInsert = this.wasMovedOnInsert;
        this.segmentGroups.copyTo(leafSegment);
        this.trackingCollection.copyTo(leafSegment);
        if (this.localRefs) {
            this.localRefs.split(pos, leafSegment);
        }
        if (this.attribution) {
            leafSegment.attribution = this.attribution.splitAt(pos);
        }
        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);
            }
        }
    }
    append(other) {
        // Note: Must call 'appendLocalRefs' before modifying this segment's length as
        //       'this.cachedLength' is used to adjust the offsets of the local refs.
        localReference_1.LocalReferenceCollection.append(this, other);
        if (this.attribution) {
            (0, core_utils_1.assert)(other.attribution !== undefined, 0x4bd /* attribution should be set on appendee */);
            this.attribution.append(other.attribution);
        }
        else {
            (0, core_utils_1.assert)(other.attribution === undefined, 0x4be /* attribution should not be set on appendee */);
        }
        this.cachedLength ?? (this.cachedLength = 0);
        this.cachedLength += other.cachedLength;
    }
}
exports.BaseSegment = BaseSegment;
/**
 * The special-cased property key that tracks the id of a {@link Marker}.
 *
 * @remarks In general, marker ids should be accessed using the inherent method
 * {@link Marker.getId}. Marker ids should not be updated after creation.
 * @internal
 */
exports.reservedMarkerIdKey = "markerId";
/**
 * @internal
 */
exports.reservedMarkerSimpleTypeKey = "markerSimpleType";
/**
 * Markers are a special kind of segment that do not hold any content.
 *
 * Markers with a reference type of {@link ReferenceType.Tile} support spatially
 * accelerated queries for finding the next marker to the left or right of it in
 * sub-linear time. This is useful, for example, in the case of jumping from the
 * start of a paragraph to the end, assuming a paragraph is bound by markers at
 * the start and end.
 *
 * @alpha
 */
class Marker extends BaseSegment {
    static is(segment) {
        return segment.type === Marker.type;
    }
    static make(refType, props) {
        const marker = new Marker(refType);
        if (props) {
            marker.addProperties(props);
        }
        return marker;
    }
    constructor(refType) {
        super();
        this.refType = refType;
        this.type = Marker.type;
        this.cachedLength = 1;
    }
    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;
    }
    getProperties() {
        return this.properties;
    }
    getId() {
        return this.properties?.[exports.reservedMarkerIdKey];
    }
    toString() {
        return `M${this.getId()}`;
    }
    createSplitSegmentAt(pos) {
        return undefined;
    }
    canAppend(segment) {
        return false;
    }
    append() {
        throw new Error("Can not append to marker");
    }
}
exports.Marker = Marker;
Marker.type = "Marker";
/**
 * @deprecated This functionality was not meant to be exported and will be removed in a future release
 * @alpha
 */
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 sequence number for this client.
         */
        this.currentSeq = 0;
        /**
         * Highest-numbered localSeq used for a pending segment.
         * Semantically, `localSeq`s provide an ordering on in-flight merge-tree operations:
         * for operations stamped with localSeqs `a` and `b`, `a < b` if and only if `a` was submitted before `b`.
         *
         * @remarks - This field is analogous to the `clientSequenceNumber` field on ops, but it's accessible to merge-tree
         * at op submission time rather than only at ack time. This enables more natural state tracking for in-flight ops.
         *
         * It's useful to stamp ops with such an incrementing counter because it enables reasoning about which segments existed from
         * the perspective of the local client at a given point in 'un-acked' time, which is necessary to support the reconnect flow.
         *
         * For example, imagine a client with initial state "123456" submits some ops to create the text "123456ABC".
         * If they insert the "C" first, then "B", then "A", their local segment state might look like this:
         * ```js
         * [
         *     { seq: 0, text: "1234" },
         *     { seq: 5, text: "56" },
         *     { localSeq: 3, seq: UnassignedSequenceNumber, text: "A" },
         *     { localSeq: 2, seq: UnassignedSequenceNumber, text: "B" },
         *     { localSeq: 1, seq: UnassignedSequenceNumber, text: "C" },
         * ]
         * ```
         * (note that {@link ISegment.localSeq} tracks the localSeq at which a segment was inserted)
         *
         * Suppose the client then disconnects and reconnects before any of its insertions are acked. The reconnect flow will necessitate
         * that the client regenerates and resubmits ops based on its current segment state as well as the original op that was sent.
         *
         * It will generate the ops
         * 1. \{ pos: 6, text: "C" \}
         * 2. \{ pos: 6, text: "B" \}
         * 3. \{ pos: 6, text: "A" \}
         *
         * since when submitting the first op, remote clients don't know that this client is about to submit the "A" and "B".
         *
         * On the other hand, imagine if the client had originally submitted the ops in the order "A", "B", "C"
         * such that the segments' local state was instead:
         *
         * ```js
         * [
         *     { seq: 0, text: "1234" },
         *     { seq: 5, text: "56" },
         *     { localSeq: 1, seq: UnassignedSequenceNumber, text: "A" },
         *     { localSeq: 2, seq: UnassignedSequenceNumber, text: "B" },
         *     { localSeq: 3, seq: UnassignedSequenceNumber, text: "C" },
         * ]
         * ```
         *
         * The resubmitted ops should instead be:
         * 1. \{ pos: 6, text: "A" \}
         * 2. \{ pos: 7, text: "B" \}
         * 3. \{ pos: 8, text: "C" \}
         *
         * since remote clients will have seen the "A" when processing the "B" as well as both the "A" and "B" when processing the "C".
         * As can be seen, the list of resubmitted ops is different in the two cases despite the merge-tree's segment state only differing
         * in `localSeq`.
         *
         * This example is a bit simplified from the general scenario: since no remote clients modified the merge-tree while the client
         * was disconnected, the resubmitted ops end up matching the original ops exactly.
         * However, this is not generally true: the production reconnect code takes into account visibility of segments based on both acked
         * and local information as appropriate.
         * Nonetheless, this simple scenario is enough to understand why it's useful to be able to determine if a segment should be visible
         * from a given (seq, localSeq) perspective.
         */
        this.localSeq = 0;
    }
    loadFrom(a) {
        this.clientId = a.clientId;
        this.collaborating = a.collaborating;
        this.minSeq = a.minSeq;
        this.currentSeq = a.currentSeq;
    }
}
exports.CollaborationWindow = CollaborationWindow;
/**
 * @internal
 */
const compareNumbers = (a, b) => a - b;
exports.compareNumbers = compareNumbers;
/**
 * @internal
 */
const compareStrings = (a, b) => a.localeCompare(b);
exports.compareStrings = compareStrings;
/**
 * Get a human-readable string for a given {@link Marker}.
 *
 * @remarks This function is intended for debugging only. The exact format of
 * this string should not be relied upon between versions.
 * @internal
 */
function debugMarkerToString(marker) {
    let bbuf = "";
    if ((0, referencePositions_1.refTypeIncludesFlag)(marker, ops_1.ReferenceType.Tile)) {
        bbuf += "Tile";
    }
    let lbuf = "";
    const id = marker.getId();
    if (id) {
        bbuf += ` (${id}) `;
    }
    const tileLabels = (0, referencePositions_1.refGetTileLabels)(marker);
    if (tileLabels) {
        lbuf += "tile -- ";
        for (let i = 0, len = tileLabels.length; i < len; i++) {
            const tileLabel = tileLabels[i];
            if (i > 0) {
                lbuf += "; ";
            }
            lbuf += tileLabel;
        }
    }
    let pbuf = "";
    if (marker.properties) {
        pbuf += JSON.stringify(marker.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}`;
}
exports.debugMarkerToString = debugMarkerToString;
//# sourceMappingURL=mergeTreeNodes.cjs.map