/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable @typescript-eslint/prefer-optional-chain */
import { assert } from "@fluidframework/common-utils";
import { LocalClientId, UnassignedSequenceNumber, UniversalSequenceNumber, } from "./constants";
import { LocalReferenceCollection, } from "./localReference";
import { TrackingGroupCollection } from "./mergeTreeTracking";
import { MergeTreeDeltaType, ReferenceType, } from "./ops";
import { computeHierarchicalOrdinal } from "./ordinal";
import { clone, createMap, } from "./properties";
import { refTypeIncludesFlag, refGetRangeLabels, refGetTileLabels, } from "./referencePositions";
import { SegmentGroupCollection } from "./segmentGroupCollection";
import { PropertiesManager, PropertiesRollback } from "./segmentPropertiesManager";
export 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;
    }
    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" */);
}
export class MergeNode {
    constructor() {
        this.index = 0;
        this.ordinal = "";
        this.cachedLength = 0;
    }
    isLeaf() {
        return false;
    }
}
export function ordinalToArray(ord) {
    const a = [];
    if (ord) {
        for (let i = 0, len = ord.length; i < len; i++) {
            a.push(ord.charCodeAt(i));
        }
    }
    return a;
}
// 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.)
export const MaxNodesInBlock = 8;
export class MergeBlock extends MergeNode {
    constructor(childCount) {
        super();
        this.childCount = childCount;
        this.children = new Array(MaxNodesInBlock);
    }
    hierBlock() {
        return undefined;
    }
    setOrdinal(child, index) {
        var _a;
        const childCount = this.childCount;
        assert((childCount >= 1) && (childCount <= MaxNodesInBlock), 0x040 /* "Child count is not within [1,8] range!" */);
        child.ordinal = computeHierarchicalOrdinal(MaxNodesInBlock, childCount, this.ordinal, index === 0 ? undefined : (_a = this.children[index - 1]) === null || _a === void 0 ? void 0 : _a.ordinal);
    }
    assignChild(child, index, updateOrdinal = true) {
        child.parent = this;
        child.index = index;
        if (updateOrdinal) {
            this.setOrdinal(child, index);
        }
        this.children[index] = child;
    }
}
export class BaseSegment extends MergeNode {
    constructor() {
        super(...arguments);
        this.clientId = LocalClientId;
        this.seq = UniversalSequenceNumber;
        this.segmentGroups = new SegmentGroupCollection(this);
        this.trackingCollection = new TrackingGroupCollection(this);
    }
    addProperties(newProps, op, seq, collabWindow, rollback = PropertiesRollback.None) {
        if (!this.propertyManager) {
            this.propertyManager = new PropertiesManager();
        }
        if (!this.properties) {
            this.properties = createMap();
        }
        return this.propertyManager.addProperties(this.properties, newProps, op, seq, collabWindow && collabWindow.collaborating, rollback);
    }
    hasProperty(key) {
        return !!this.properties && (this.properties[key] !== undefined);
    }
    isLeaf() {
        return true;
    }
    cloneInto(b) {
        var _a, _b;
        b.clientId = this.clientId;
        // TODO: deep clone properties
        b.properties = 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;
        b.attribution = (_b = this.attribution) === null || _b === void 0 ? void 0 : _b.clone();
    }
    canAppend(segment) {
        return false;
    }
    addSerializedProps(jseg) {
        if (this.properties) {
            jseg.props = this.properties;
        }
    }
    ack(segmentGroup, opArgs) {
        const currentSegmentGroup = this.segmentGroups.dequeue();
        assert(currentSegmentGroup === segmentGroup, 0x043 /* "On ack, unexpected segmentGroup!" */);
        switch (opArgs.op.type) {
            case MergeTreeDeltaType.ANNOTATE:
                assert(!!this.propertyManager, 0x044 /* "On annotate ack, missing segment property manager!" */);
                this.propertyManager.ackPendingProperties(opArgs.op);
                return true;
            case MergeTreeDeltaType.INSERT:
                assert(this.seq === UnassignedSequenceNumber, 0x045 /* "On insert, seq number already assigned!" */);
                this.seq = opArgs.sequencedMessage.sequenceNumber;
                this.localSeq = undefined;
                return true;
            case MergeTreeDeltaType.REMOVE:
                const removalInfo = toRemovalInfo(this);
                assert(removalInfo !== undefined, 0x046 /* "On remove ack, missing removal info!" */);
                this.localRemovedSeq = undefined;
                if (removalInfo.removedSeq === 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 its 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);
                }
                if (this.attribution) {
                    leafSegment.attribution = this.attribution.splitAt(pos);
                }
            }
            return leafSegment;
        }
    }
    copyPropertiesTo(other) {
        if (this.propertyManager) {
            if (this.properties) {
                other.propertyManager = new 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.
        LocalReferenceCollection.append(this, other);
        if (this.attribution) {
            assert(other.attribution !== undefined, 0x4bd /* attribution should be set on appendee */);
            this.attribution.append(other.attribution);
        }
        else {
            assert(other.attribution === undefined, 0x4be /* attribution should not be set on appendee */);
        }
        this.cachedLength += other.cachedLength;
    }
}
export const reservedMarkerIdKey = "markerId";
export const reservedMarkerSimpleTypeKey = "markerSimpleType";
export 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[reservedMarkerSimpleTypeKey] === simpleTypeName;
    }
    getProperties() {
        return this.properties;
    }
    getId() {
        if (this.properties && this.properties[reservedMarkerIdKey]) {
            return this.properties[reservedMarkerIdKey];
        }
    }
    toString() {
        return `M${this.getId()}`;
    }
    createSplitSegmentAt(pos) {
        return undefined;
    }
    canAppend(segment) {
        return false;
    }
    append() { throw new Error("Can not append to marker"); }
}
Marker.type = "Marker";
export var IncrementalExecOp;
(function (IncrementalExecOp) {
    IncrementalExecOp[IncrementalExecOp["Go"] = 0] = "Go";
    IncrementalExecOp[IncrementalExecOp["Stop"] = 1] = "Stop";
    IncrementalExecOp[IncrementalExecOp["Yield"] = 2] = "Yield";
})(IncrementalExecOp || (IncrementalExecOp = {}));
export 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;
    }
}
export class CollaborationWindow {
    constructor() {
        this.clientId = 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;
    }
}
export const compareNumbers = (a, b) => a - b;
export const compareStrings = (a, b) => a.localeCompare(b);
const indentStrings = ["", " ", "  "];
export function internedSpaces(n) {
    if (indentStrings[n] === undefined) {
        indentStrings[n] = "";
        for (let i = 0; i < n; i++) {
            indentStrings[n] += " ";
        }
    }
    return indentStrings[n];
}
export function debugMarkerToString(marker) {
    let bbuf = "";
    if (refTypeIncludesFlag(marker, ReferenceType.Tile)) {
        bbuf += "Tile";
    }
    if (refTypeIncludesFlag(marker, ReferenceType.NestBegin)) {
        if (bbuf.length > 0) {
            bbuf += "; ";
        }
        bbuf += "RangeBegin";
    }
    if (refTypeIncludesFlag(marker, ReferenceType.NestEnd)) {
        if (bbuf.length > 0) {
            bbuf += "; ";
        }
        bbuf += "RangeEnd";
    }
    let lbuf = "";
    const id = marker.getId();
    if (id) {
        bbuf += ` (${id}) `;
    }
    const tileLabels = 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;
        }
    }
    const rangeLabels = refGetRangeLabels(marker);
    if (rangeLabels) {
        let rangeKind = "begin";
        if (refTypeIncludesFlag(marker, 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 (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}`;
}
//# sourceMappingURL=mergeTreeNodes.js.map