"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.compareStrings = exports.compareNumbers = exports.CollaborationWindow = exports.getMinSeqPerspective = exports.getMinSeqStamp = exports.Marker = exports.reservedMarkerSimpleTypeKey = exports.reservedMarkerIdKey = exports.BaseSegment = exports.seqLTE = exports.assignChild = exports.MergeBlock = exports.MaxNodesInBlock = exports.segmentIsRemoved = exports.assertSegmentLeaf = exports.toSegmentLeaf = exports.isSegmentLeaf = void 0;
const internal_1 = require("@fluidframework/core-utils/internal");
const constants_js_1 = require("./constants.js");
const localReference_js_1 = require("./localReference.js");
const mergeTreeTracking_js_1 = require("./mergeTreeTracking.js");
const ordinal_js_1 = require("./ordinal.js");
const perspective_js_1 = require("./perspective.js");
const properties_js_1 = require("./properties.js");
const segmentInfos_js_1 = require("./segmentInfos.js");
/**
 * A type-guard which determines if the segment has segment leaf, and
 * returns true if it does, along with applying strong typing.
 * @param nodeLike - The segment-like object to check.
 * @returns True if the segment is a segment leaf, otherwise false.
 */
const isSegmentLeaf = (segmentLike) => (0, segmentInfos_js_1.isInserted)(segmentLike) && (0, segmentInfos_js_1.isMergeNodeInfo)(segmentLike);
exports.isSegmentLeaf = isSegmentLeaf;
/**
 * Converts a segment-like object to a segment leaf object if possible.
 *
 * @param segmentLike - The segment-like object to convert.
 * @returns The segment leaf if the conversion is possible, otherwise undefined.
 */
const toSegmentLeaf = (segmentLike) => (0, exports.isSegmentLeaf)(segmentLike) ? segmentLike : undefined;
exports.toSegmentLeaf = toSegmentLeaf;
/**
 * Asserts that the segment is a segment leaf. Usage of this function should not produce a user facing error.
 *
 * @param segmentLike - The segment-like object to check.
 * @throws Will throw an error if the segment is not a segment leaf.
 */
const assertSegmentLeaf = (segmentLike) => (0, internal_1.assert)((0, exports.isSegmentLeaf)(segmentLike), 0xaab /* must be segment leaf */);
exports.assertSegmentLeaf = assertSegmentLeaf;
/**
 * Determine if a segment has been removed.
 * @legacy
 * @alpha
 */
function segmentIsRemoved(segment) {
    return (0, segmentInfos_js_1.isRemoved)(segment);
}
exports.segmentIsRemoved = segmentIsRemoved;
/**
 * 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 {
    isLeaf() {
        return false;
    }
    constructor(childCount) {
        this.childCount = childCount;
        this.index = 0;
        this.ordinal = "";
        this.cachedLength = 0;
        // Suppression needed due to the way the merge tree children are initalized - we
        // allocate 8 children blocks, but any unused blocks are not counted in the childCount.
        // Using Array.from leads to unused children being undefined, which are counted in childCount.
        // eslint-disable-next-line unicorn/no-new-array
        this.children = new Array(exports.MaxNodesInBlock);
        this.rightmostTiles = (0, properties_js_1.createMap)();
        this.leftmostTiles = (0, properties_js_1.createMap)();
    }
    setOrdinal(child, index) {
        const childCount = this.childCount;
        (0, internal_1.assert)(childCount >= 1 && childCount <= exports.MaxNodesInBlock, 0x040 /* "Child count is not within [1,8] range!" */);
        child.ordinal = (0, ordinal_js_1.computeHierarchicalOrdinal)(exports.MaxNodesInBlock, childCount, this.ordinal, index === 0 ? undefined : this.children[index - 1]?.ordinal);
    }
}
exports.MergeBlock = MergeBlock;
function assignChild(parent, child, index, updateOrdinal = true) {
    const node = Object.assign(child, {
        parent,
        index,
        ordinal: (0, segmentInfos_js_1.hasProp)(child, "ordinal", "string") ? child.ordinal : "",
    });
    if (updateOrdinal) {
        parent.setOrdinal(node, index);
    }
    parent.children[index] = node;
}
exports.assignChild = assignChild;
function seqLTE(seq, minOrRefSeq) {
    return seq !== constants_js_1.UnassignedSequenceNumber && seq <= minOrRefSeq;
}
exports.seqLTE = seqLTE;
/**
 * @legacy
 * @alpha
 */
class BaseSegment {
    constructor(properties) {
        this.cachedLength = 0;
        this.trackingCollection = new mergeTreeTracking_js_1.TrackingGroupCollection(this);
        if (properties !== undefined) {
            this.properties = (0, properties_js_1.clone)(properties);
        }
    }
    hasProperty(key) {
        return !!this.properties && this.properties[key] !== undefined;
    }
    isLeaf() {
        return true;
    }
    cloneInto(b) {
        const seg = b;
        if ((0, segmentInfos_js_1.isInserted)(this)) {
            (0, segmentInfos_js_1.overwriteInfo)(seg, {
                insert: this.insert,
            });
        }
        // TODO: deep clone properties
        seg.properties = (0, properties_js_1.clone)(this.properties);
        if ((0, segmentInfos_js_1.isRemoved)(this)) {
            (0, segmentInfos_js_1.overwriteInfo)(seg, {
                removes: [...this.removes],
            });
        }
        seg.attribution = this.attribution?.clone();
    }
    canAppend(segment) {
        return false;
    }
    addSerializedProps(jseg) {
        if (this.properties) {
            jseg.props = { ...this.properties };
        }
    }
    splitAt(pos) {
        if (pos <= 0) {
            return undefined;
        }
        const leafSegment = this.createSplitSegmentAt(pos);
        if (!leafSegment) {
            return undefined;
        }
        if ((0, segmentInfos_js_1.isMergeNodeInfo)(this)) {
            (0, segmentInfos_js_1.overwriteInfo)(leafSegment, {
                index: this.index + 1,
                // 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.
                // Ordinals exist purely for lexicographical sort order and use a small set of valid bytes for each string character.
                // The extra handling fromCodePoint has for things like surrogate pairs is therefore unnecessary.
                // eslint-disable-next-line unicorn/prefer-code-point
                ordinal: this.ordinal + String.fromCharCode(0),
                parent: this.parent,
            });
        }
        if ((0, segmentInfos_js_1.isInserted)(this)) {
            (0, segmentInfos_js_1.overwriteInfo)(leafSegment, { insert: this.insert });
        }
        if ((0, segmentInfos_js_1.isRemoved)(this)) {
            (0, segmentInfos_js_1.overwriteInfo)(leafSegment, {
                removes: [...this.removes],
            });
        }
        if ((0, segmentInfos_js_1.isInsideObliterate)(this)) {
            (0, segmentInfos_js_1.overwriteInfo)(leafSegment, {
                obliteratePrecedingInsertion: this.obliteratePrecedingInsertion,
                insertionRefSeqStamp: this.insertionRefSeqStamp,
            });
        }
        this.trackingCollection.copyTo(leafSegment);
        if (this.attribution) {
            leafSegment.attribution = this.attribution.splitAt(pos);
        }
        return leafSegment;
    }
    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_js_1.LocalReferenceCollection.append(this, other);
        if (this.attribution) {
            (0, internal_1.assert)(other.attribution !== undefined, 0x4bd /* attribution should be set on appendee */);
            this.attribution.append(other.attribution);
        }
        else {
            (0, internal_1.assert)(other.attribution === undefined, 0x4be /* attribution should not be set on appendee */);
        }
        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.
 * @legacy
 * @alpha
 */
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.
 *
 * @legacy
 * @alpha
 */
class Marker extends BaseSegment {
    static is(segment) {
        return segment.type === Marker.type;
    }
    static make(refType, props) {
        return new Marker(refType, props);
    }
    constructor(refType, props) {
        super(props);
        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";
/**
 * Returns a stamp that occurs at the minimum sequence number.
 * @privateRemarks
 * This is a free function over something obtainable on CollaborationWindow to avoid exposing Perspective
 * and OperationStamp from the package (even internally), at least for now.
 * If/when `Client`'s API is refactored to be structured similarly to MergeTree (so that SharedString passes in
 * things closer to `Perspective`s when calling methods on `Client` rather than refSeq/localSeq/clientId etc),
 * it may be more reasonable to expose this more directly on `CollaborationWindow`.
 */
function getMinSeqStamp(collabWindow) {
    return { seq: collabWindow.minSeq, clientId: constants_js_1.NonCollabClient };
}
exports.getMinSeqStamp = getMinSeqStamp;
/**
 * Returns a perspective representing a readonly client's view of the tree at the minimum sequence number.
 * @privateRemarks
 * This is a free function over something obtainable on CollaborationWindow to avoid exposing Perspective
 * and OperationStamp from the package (even internally), at least for now.
 * If/when `Client`'s API is refactored to be structured similarly to MergeTree (so that SharedString passes in
 * things closer to `Perspective`s when calling methods on `Client` rather than refSeq/localSeq/clientId etc),
 * it may be more reasonable to expose this more directly on `CollaborationWindow`.
 */
function getMinSeqPerspective(collabWindow) {
    return new perspective_js_1.PriorPerspective(collabWindow.minSeq, constants_js_1.NonCollabClient);
}
exports.getMinSeqPerspective = getMinSeqPerspective;
/**
 * This class is used to track facts about the current window of collaboration. This window is defined by the server
 * specified minimum sequence number to the last sequence number seen. Additionally, it track state for outstanding
 * local operations.
 * @internal
 */
class CollaborationWindow {
    constructor() {
        this.clientId = constants_js_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 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;
        this.localPerspective = new perspective_js_1.LocalDefaultPerspective(this.clientId);
    }
    loadFrom(a) {
        this.clientId = a.clientId;
        this.collaborating = a.collaborating;
        this.minSeq = a.minSeq;
        this.currentSeq = a.currentSeq;
    }
    mintNextLocalOperationStamp() {
        if (this.collaborating) {
            this.localSeq++;
        }
        return {
            seq: this.collaborating ? constants_js_1.UnassignedSequenceNumber : constants_js_1.UniversalSequenceNumber,
            clientId: this.clientId,
            localSeq: this.localSeq,
        };
    }
}
exports.CollaborationWindow = CollaborationWindow;
/**
 * Compares two numbers.
 */
const compareNumbers = (a, b) => a - b;
exports.compareNumbers = compareNumbers;
/**
 * Compares two strings.
 */
const compareStrings = (a, b) => a.localeCompare(b);
exports.compareStrings = compareStrings;
//# sourceMappingURL=mergeTreeNodes.js.map