"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.Client = void 0;
const protocol_definitions_1 = require("@fluidframework/protocol-definitions");
const common_utils_1 = require("@fluidframework/common-utils");
const telemetry_utils_1 = require("@fluidframework/telemetry-utils");
const container_utils_1 = require("@fluidframework/container-utils");
const collections_1 = require("./collections");
const constants_1 = require("./constants");
const mergeTreeNodes_1 = require("./mergeTreeNodes");
const opBuilder_1 = require("./opBuilder");
const ops_1 = require("./ops");
const snapshotlegacy_1 = require("./snapshotlegacy");
const snapshotLoader_1 = require("./snapshotLoader");
const snapshotV1_1 = require("./snapshotV1");
const referencePositions_1 = require("./referencePositions");
const mergeTree_1 = require("./mergeTree");
const MergeTreeTextHelper_1 = require("./MergeTreeTextHelper");
const mergeTreeNodeWalk_1 = require("./mergeTreeNodeWalk");
class Client extends common_utils_1.TypedEventEmitter {
    constructor(
    // Passing this callback would be unnecessary if Client were merged with SharedSegmentSequence
    specToSegment, logger, options) {
        var _a, _b;
        super();
        this.specToSegment = specToSegment;
        this.logger = logger;
        this.clientNameToIds = new collections_1.RedBlackTree(mergeTreeNodes_1.compareStrings);
        this.shortClientIdMap = [];
        this.pendingConsensus = new Map();
        this.lastNormalizationRefSeq = 0;
        this._mergeTree = new mergeTree_1.MergeTree(options);
        this._mergeTree.mergeTreeDeltaCallback = (opArgs, deltaArgs) => {
            this.emit("delta", opArgs, deltaArgs, this);
        };
        this._mergeTree.mergeTreeMaintenanceCallback = (args, opArgs) => {
            this.emit("maintenance", args, opArgs, this);
        };
        if ((_a = options === null || options === void 0 ? void 0 : options.attribution) === null || _a === void 0 ? void 0 : _a.track) {
            const policy = (_b = this._mergeTree) === null || _b === void 0 ? void 0 : _b.attributionPolicy;
            if (policy === undefined) {
                throw new container_utils_1.UsageError("Attribution policy must be provided when attribution tracking is requested.");
            }
            policy.attach(this);
        }
    }
    /**
     * The merge tree maintains a queue of segment groups for each local operation.
     * These segment groups track segments modified by an operation.
     * This method peeks the tail of that queue, and returns the segments groups there.
     * It is used to get the segment group(s) for the previous operations.
     * @param count - The number segment groups to get peek from the tail of the queue. Default 1.
     */
    peekPendingSegmentGroups(count = 1) {
        const pending = this._mergeTree.pendingSegments;
        let node = pending === null || pending === void 0 ? void 0 : pending.last;
        if (count === 1 || pending === undefined) {
            return node === null || node === void 0 ? void 0 : node.data;
        }
        const taken = new Array(Math.min(count, pending.length));
        for (let i = taken.length - 1; i >= 0; i--) {
            taken[i] = node.data;
            node = node.prev;
        }
        return taken;
    }
    /**
     * Annotate a marker and call the callback on consensus.
     * @param marker - The marker to annotate
     * @param props - The properties to annotate the marker with
     * @param consensusCallback - The callback called when consensus is reached
     * @returns The annotate op if valid, otherwise undefined
     */
    annotateMarkerNotifyConsensus(marker, props, consensusCallback) {
        const combiningOp = {
            name: "consensus",
        };
        const annotateOp = this.annotateMarker(marker, props, combiningOp);
        if (annotateOp) {
            const consensusInfo = {
                callback: consensusCallback,
                marker,
            };
            this.pendingConsensus.set(marker.getId(), consensusInfo);
            return annotateOp;
        }
        else {
            return undefined;
        }
    }
    /**
     * Annotates the markers with the provided properties
     * @param marker - The marker to annotate
     * @param props - The properties to annotate the marker with
     * @param combiningOp - Optional. Specifies how to combine values for the property, such as "incr" for increment.
     * @returns The annotate op if valid, otherwise undefined
     */
    annotateMarker(marker, props, combiningOp) {
        const annotateOp = (0, opBuilder_1.createAnnotateMarkerOp)(marker, props, combiningOp);
        return this.applyAnnotateRangeOp({ op: annotateOp }) ? annotateOp : undefined;
    }
    /**
     * Annotates the range with the provided 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 - Specifies how to combine values for the property, such as "incr" for increment.
     * @returns The annotate op if valid, otherwise undefined
     */
    annotateRangeLocal(start, end, props, combiningOp) {
        const annotateOp = (0, opBuilder_1.createAnnotateRangeOp)(start, end, props, combiningOp);
        if (this.applyAnnotateRangeOp({ op: annotateOp })) {
            return annotateOp;
        }
        return undefined;
    }
    /**
     * Removes the range
     *
     * @param start - The inclusive start of the range to remove
     * @param end - The exclusive end of the range to remove
     */
    removeRangeLocal(start, end) {
        const removeOp = (0, opBuilder_1.createRemoveRangeOp)(start, end);
        this.applyRemoveRangeOp({ op: removeOp });
        return removeOp;
    }
    /**
     * @param pos - The position to insert the segment at
     * @param segment - The segment to insert
     */
    insertSegmentLocal(pos, segment) {
        if (segment.cachedLength <= 0) {
            return undefined;
        }
        const insertOp = (0, opBuilder_1.createInsertSegmentOp)(pos, segment);
        if (this.applyInsertOp({ op: insertOp })) {
            return insertOp;
        }
        return undefined;
    }
    /**
     * @param refPos - The reference position to insert the segment at
     * @param segment - The segment to insert
     */
    insertAtReferencePositionLocal(refPos, segment) {
        const pos = this._mergeTree.referencePositionToLocalPosition(refPos, this.getCurrentSeq(), this.getClientId());
        if (pos === referencePositions_1.DetachedReferencePosition) {
            return undefined;
        }
        const op = (0, opBuilder_1.createInsertSegmentOp)(pos, segment);
        const opArgs = { op };
        this._mergeTree.insertAtReferencePosition(refPos, segment, opArgs);
        return op;
    }
    walkSegments(handler, start, end, accum, splitRange = false) {
        this._mergeTree.mapRange(handler, this.getCurrentSeq(), this.getClientId(), accum, start, end, splitRange);
    }
    walkAllSegments(action, accum) {
        return (0, mergeTreeNodeWalk_1.walkAllChildSegments)(this._mergeTree.root, accum === undefined ? action : (seg) => action(seg, accum));
    }
    /**
     * Serializes the data required for garbage collection. The IFluidHandles stored in all segments that haven't
     * been removed represent routes to other objects. We serialize the data in these segments using the passed in
     * serializer which keeps track of all serialized handles.
     */
    serializeGCData(handle, handleCollectingSerializer) {
        let localInserts = 0;
        let localRemoves = 0;
        (0, mergeTreeNodeWalk_1.walkAllChildSegments)(this._mergeTree.root, (seg) => {
            if (seg.seq === constants_1.UnassignedSequenceNumber) {
                localInserts++;
            }
            if (seg.removedSeq === constants_1.UnassignedSequenceNumber) {
                localRemoves++;
            }
            // Only serialize segments that have not been removed.
            if (seg.removedSeq === undefined) {
                handleCollectingSerializer.stringify(seg.clone().toJSONObject(), handle);
            }
            return true;
        });
        if (localInserts > 0 || localRemoves > 0) {
            this.logger.sendErrorEvent({
                eventName: "LocalEditsInProcessGCData",
                localInserts,
                localRemoves,
            });
        }
    }
    getCollabWindow() {
        return this._mergeTree.collabWindow;
    }
    /**
     * Returns the current position of a segment, and -1 if the segment
     * does not exist in this merge tree
     * @param segment - The segment to get the position of
     */
    getPosition(segment, localSeq) {
        if ((segment === null || segment === void 0 ? void 0 : segment.parent) === undefined) {
            return -1;
        }
        return this._mergeTree.getPosition(segment, this.getCurrentSeq(), this.getClientId(), localSeq);
    }
    /**
     * Creates a `LocalReferencePosition` on this client. If the refType does not include ReferenceType.Transient,
     * the returned reference will be added to the localRefs on the provided segment.
     * @param segment - Segment to add the local reference on
     * @param offset - Offset on the segment at which to place the local reference
     * @param refType - ReferenceType for the created local reference
     * @param properties - PropertySet to place on the created local reference
     */
    createLocalReferencePosition(segment, offset, refType, properties, slidingPreference) {
        return this._mergeTree.createLocalReferencePosition(segment, offset !== null && offset !== void 0 ? offset : 0, refType, properties, slidingPreference);
    }
    /**
     * Removes a `LocalReferencePosition` from this client.
     */
    removeLocalReferencePosition(lref) {
        return this._mergeTree.removeLocalReferencePosition(lref);
    }
    /**
     * Resolves a `ReferencePosition` into a character position using this client's perspective.
     */
    localReferencePositionToPosition(lref) {
        return this._mergeTree.referencePositionToLocalPosition(lref);
    }
    /**
     * 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.
     */
    posFromRelativePos(relativePos) {
        return this._mergeTree.posFromRelativePos(relativePos);
    }
    getMarkerFromId(id) {
        return this._mergeTree.getMarkerFromId(id);
    }
    /**
     * Revert an op
     */
    rollback(op, localOpMetadata) {
        this._mergeTree.rollback(op, localOpMetadata);
    }
    /**
     * Performs the remove based on the provided op
     * @param opArgs - The ops args for the op
     * @returns True if the remove was applied. False if it could not be.
     */
    applyRemoveRangeOp(opArgs) {
        (0, common_utils_1.assert)(opArgs.op.type === ops_1.MergeTreeDeltaType.REMOVE, 0x02d /* "Unexpected op type on range remove!" */);
        const op = opArgs.op;
        const clientArgs = this.getClientSequenceArgs(opArgs);
        const range = this.getValidOpRange(op, clientArgs);
        this._mergeTree.markRangeRemoved(range.start, range.end, clientArgs.referenceSequenceNumber, clientArgs.clientId, clientArgs.sequenceNumber, false, opArgs);
        return true;
    }
    /**
     * Performs the annotate based on the provided op
     * @param opArgs - The ops args for the op
     * @returns True if the annotate was applied. False if it could not be.
     */
    applyAnnotateRangeOp(opArgs) {
        (0, common_utils_1.assert)(opArgs.op.type === ops_1.MergeTreeDeltaType.ANNOTATE, 0x02e /* "Unexpected op type on range annotate!" */);
        const op = opArgs.op;
        const clientArgs = this.getClientSequenceArgs(opArgs);
        const range = this.getValidOpRange(op, clientArgs);
        if (!range) {
            return false;
        }
        this._mergeTree.annotateRange(range.start, range.end, op.props, op.combiningOp, clientArgs.referenceSequenceNumber, clientArgs.clientId, clientArgs.sequenceNumber, opArgs);
        return true;
    }
    /**
     * Performs the insert based on the provided op
     * @param opArgs - The ops args for the op
     * @returns True if the insert was applied. False if it could not be.
     */
    applyInsertOp(opArgs) {
        (0, common_utils_1.assert)(opArgs.op.type === ops_1.MergeTreeDeltaType.INSERT, 0x02f /* "Unexpected op type on range insert!" */);
        const op = opArgs.op;
        const clientArgs = this.getClientSequenceArgs(opArgs);
        const range = this.getValidOpRange(op, clientArgs);
        if (!range) {
            return false;
        }
        let segments;
        if (op.seg) {
            segments = [this.specToSegment(op.seg)];
        }
        if (!segments || segments.length === 0) {
            return false;
        }
        this._mergeTree.insertSegments(range.start, segments, clientArgs.referenceSequenceNumber, clientArgs.clientId, clientArgs.sequenceNumber, opArgs);
        return true;
    }
    /**
     * Returns a valid range for the op, or undefined
     * @param op - The op to generate the range for
     * @param clientArgs - The client args for the op
     */
    getValidOpRange(op, clientArgs) {
        let start = op.pos1;
        if (start === undefined && op.relativePos1) {
            start = this._mergeTree.posFromRelativePos(op.relativePos1, clientArgs.referenceSequenceNumber, clientArgs.clientId);
        }
        let end = op.pos2;
        if (end === undefined && op.relativePos2) {
            end = this._mergeTree.posFromRelativePos(op.relativePos2, clientArgs.referenceSequenceNumber, clientArgs.clientId);
        }
        // Validate if local op
        if (clientArgs.clientId === this.getClientId()) {
            const length = this.getLength();
            const invalidPositions = [];
            // Validate start position
            //
            if (start === undefined ||
                start < 0 ||
                start > length ||
                (start === length && op.type !== ops_1.MergeTreeDeltaType.INSERT)) {
                invalidPositions.push("start");
            }
            // Validate end if not insert, or insert has end
            //
            if (op.type !== ops_1.MergeTreeDeltaType.INSERT || end !== undefined) {
                if (end === undefined || end <= start) {
                    invalidPositions.push("end");
                }
            }
            if (invalidPositions.length > 0) {
                throw new telemetry_utils_1.LoggingError("RangeOutOfBounds", {
                    usageError: true,
                    end,
                    invalidPositions: invalidPositions.toString(),
                    length,
                    opPos1: op.pos1,
                    opPos1Relative: op.relativePos1 !== undefined,
                    opPos2: op.pos2,
                    opPos2Relative: op.relativePos2 !== undefined,
                    opType: op.type,
                    start,
                });
            }
        }
        // start and end are guaranteed to be non-null here, otherwise we throw above.
        // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
        return { start, end };
    }
    /**
     * Gets the client args from the op if remote, otherwise uses the local clients info
     * @param sequencedMessage - The sequencedMessage to get the client sequence args for
     */
    getClientSequenceArgsForMessage(sequencedMessage) {
        // If there this no sequenced message, then the op is local
        // and unacked, so use this clients sequenced args
        //
        if (!sequencedMessage) {
            const segWindow = this.getCollabWindow();
            return {
                clientId: segWindow.clientId,
                referenceSequenceNumber: segWindow.currentSeq,
                sequenceNumber: this.getLocalSequenceNumber(),
            };
        }
        else {
            return {
                clientId: this.getOrAddShortClientIdFromMessage(sequencedMessage),
                referenceSequenceNumber: sequencedMessage.referenceSequenceNumber,
                // Note: return value satisfies overload signatures despite the cast, as if input argument doesn't contain sequenceNumber,
                // return value isn't expected to have it either.
                sequenceNumber: sequencedMessage.sequenceNumber,
            };
        }
    }
    /**
     * Gets the client args from the op if remote, otherwise uses the local clients info
     * @param opArgs - The op arg to get the client sequence args for
     */
    getClientSequenceArgs(opArgs) {
        return this.getClientSequenceArgsForMessage(opArgs.sequencedMessage);
    }
    ackPendingSegment(opArgs) {
        const ackOp = (deltaOpArgs) => {
            this._mergeTree.ackPendingSegment(deltaOpArgs);
            if (deltaOpArgs.op.type === ops_1.MergeTreeDeltaType.ANNOTATE) {
                if (deltaOpArgs.op.combiningOp && deltaOpArgs.op.combiningOp.name === "consensus") {
                    this.updateConsensusProperty(deltaOpArgs.op, deltaOpArgs.sequencedMessage);
                }
            }
        };
        if (opArgs.op.type === ops_1.MergeTreeDeltaType.GROUP) {
            for (const memberOp of opArgs.op.ops) {
                ackOp({
                    groupOp: opArgs.op,
                    op: memberOp,
                    sequencedMessage: opArgs.sequencedMessage,
                });
            }
        }
        else {
            ackOp(opArgs);
        }
    }
    // as functions are modified move them above the eslint-disabled waterline and lint them
    cloneFromSegments() {
        const clone = new Client(this.specToSegment, this.logger, this._mergeTree.options);
        const segments = [];
        const newRoot = this._mergeTree.blockClone(this._mergeTree.root, segments);
        clone._mergeTree.root = newRoot;
        return clone;
    }
    getOrAddShortClientId(longClientId) {
        if (!this.clientNameToIds.get(longClientId)) {
            this.addLongClientId(longClientId);
        }
        return this.getShortClientId(longClientId);
    }
    getShortClientId(longClientId) {
        return this.clientNameToIds.get(longClientId).data;
    }
    getLongClientId(shortClientId) {
        return shortClientId >= 0 ? this.shortClientIdMap[shortClientId] : "original";
    }
    addLongClientId(longClientId) {
        this.clientNameToIds.put(longClientId, this.shortClientIdMap.length);
        this.shortClientIdMap.push(longClientId);
    }
    getOrAddShortClientIdFromMessage(msg) {
        var _a;
        return this.getOrAddShortClientId((_a = msg.clientId) !== null && _a !== void 0 ? _a : "server");
    }
    /**
     * During reconnect, we must find the positions to pending segments
     * relative to other pending segments. This methods computes that
     * position relative to a localSeq. Pending segments above the localSeq
     * will be ignored.
     *
     * @param segment - The segment to find the position for
     * @param localSeq - The localSeq to find the position of the segment at
     */
    findReconnectionPosition(segment, localSeq) {
        (0, common_utils_1.assert)(localSeq <= this._mergeTree.collabWindow.localSeq, 0x032 /* "localSeq greater than collab window" */);
        const { currentSeq, clientId } = this.getCollabWindow();
        return this._mergeTree.getPosition(segment, currentSeq, clientId, localSeq);
    }
    resetPendingDeltaToOps(resetOp, segmentGroup) {
        var _a, _b;
        (0, common_utils_1.assert)(!!segmentGroup, 0x033 /* "Segment group undefined" */);
        const NACKedSegmentGroup = (_a = this._mergeTree.pendingSegments.shift()) === null || _a === void 0 ? void 0 : _a.data;
        (0, common_utils_1.assert)(segmentGroup === NACKedSegmentGroup, 0x034 /* "Segment group not at head of merge tree pending queue" */);
        const opList = [];
        // We need to sort the segments by ordinal, as the segments are not sorted in the segment group.
        // The reason they need them sorted, as they have the same local sequence number and which means
        // farther segments will  take into account nearer segments when calculating their position.
        // By sorting we ensure the nearer segment will be applied and sequenced before the farther segments
        // so their recalculated positions will be correct.
        for (const segment of segmentGroup.segments.sort((a, b) => a.ordinal < b.ordinal ? -1 : 1)) {
            const segmentSegGroup = segment.segmentGroups.dequeue();
            (0, common_utils_1.assert)(segmentGroup === segmentSegGroup, 0x035 /* "Segment group not at head of segment pending queue" */);
            const segmentPosition = this.findReconnectionPosition(segment, segmentGroup.localSeq);
            let newOp;
            switch (resetOp.type) {
                case ops_1.MergeTreeDeltaType.ANNOTATE:
                    (0, common_utils_1.assert)(((_b = segment.propertyManager) === null || _b === void 0 ? void 0 : _b.hasPendingProperties()) === true, 0x036 /* "Segment has no pending properties" */);
                    // if the segment has been removed, there's no need to send the annotate op
                    // unless the remove was local, in which case the annotate must have come
                    // before the remove
                    if (segment.removedSeq === undefined ||
                        (segment.localRemovedSeq !== undefined &&
                            segment.removedSeq === constants_1.UnassignedSequenceNumber)) {
                        newOp = (0, opBuilder_1.createAnnotateRangeOp)(segmentPosition, segmentPosition + segment.cachedLength, resetOp.props, resetOp.combiningOp);
                    }
                    break;
                case ops_1.MergeTreeDeltaType.INSERT:
                    (0, common_utils_1.assert)(segment.seq === constants_1.UnassignedSequenceNumber, 0x037 /* "Segment already has assigned sequence number" */);
                    let segInsertOp = segment;
                    if (typeof resetOp.seg === "object" && resetOp.seg.props !== undefined) {
                        segInsertOp = segment.clone();
                        segInsertOp.properties = resetOp.seg.props;
                    }
                    newOp = (0, opBuilder_1.createInsertSegmentOp)(segmentPosition, segInsertOp);
                    break;
                case ops_1.MergeTreeDeltaType.REMOVE:
                    if (segment.localRemovedSeq !== undefined &&
                        segment.removedSeq === constants_1.UnassignedSequenceNumber) {
                        newOp = (0, opBuilder_1.createRemoveRangeOp)(segmentPosition, segmentPosition + segment.cachedLength);
                    }
                    break;
                default:
                    throw new Error(`Invalid op type`);
            }
            if (newOp) {
                const newSegmentGroup = {
                    segments: [],
                    localSeq: segmentGroup.localSeq,
                    refSeq: this.getCollabWindow().currentSeq,
                };
                segment.segmentGroups.enqueue(newSegmentGroup);
                this._mergeTree.pendingSegments.push(newSegmentGroup);
                opList.push(newOp);
            }
        }
        return opList;
    }
    applyRemoteOp(opArgs) {
        const op = opArgs.op;
        const msg = opArgs.sequencedMessage;
        this.getOrAddShortClientIdFromMessage(msg);
        switch (op.type) {
            case ops_1.MergeTreeDeltaType.INSERT:
                this.applyInsertOp(opArgs);
                break;
            case ops_1.MergeTreeDeltaType.REMOVE:
                this.applyRemoveRangeOp(opArgs);
                break;
            case ops_1.MergeTreeDeltaType.ANNOTATE:
                this.applyAnnotateRangeOp(opArgs);
                break;
            case ops_1.MergeTreeDeltaType.GROUP: {
                for (const memberOp of op.ops) {
                    this.applyRemoteOp({
                        op: memberOp,
                        groupOp: op,
                        sequencedMessage: msg,
                    });
                }
                break;
            }
            default:
                break;
        }
    }
    applyStashedOp(op) {
        let metadata;
        switch (op.type) {
            case ops_1.MergeTreeDeltaType.INSERT:
                this.applyInsertOp({ op });
                metadata = this.peekPendingSegmentGroups();
                break;
            case ops_1.MergeTreeDeltaType.REMOVE:
                this.applyRemoveRangeOp({ op });
                metadata = this.peekPendingSegmentGroups();
                break;
            case ops_1.MergeTreeDeltaType.ANNOTATE:
                this.applyAnnotateRangeOp({ op });
                metadata = this.peekPendingSegmentGroups();
                break;
            case ops_1.MergeTreeDeltaType.GROUP:
                return op.ops.map((o) => this.applyStashedOp(o));
            default:
                (0, common_utils_1.unreachableCase)(op, "unrecognized op type");
        }
        (0, common_utils_1.assert)(!!metadata, 0x2db /* "Applying op must generate a pending segment" */);
        return metadata;
    }
    applyMsg(msg, local = false) {
        var _a;
        // Ensure client ID is registered
        this.getOrAddShortClientIdFromMessage(msg);
        // Apply if an operation message
        if (msg.type === protocol_definitions_1.MessageType.Operation) {
            const opArgs = {
                op: msg.contents,
                sequencedMessage: msg,
            };
            if (((_a = opArgs.sequencedMessage) === null || _a === void 0 ? void 0 : _a.clientId) === this.longClientId || local) {
                this.ackPendingSegment(opArgs);
            }
            else {
                this.applyRemoteOp(opArgs);
            }
        }
        this.updateSeqNumbers(msg.minimumSequenceNumber, msg.sequenceNumber);
    }
    updateSeqNumbers(min, seq) {
        const collabWindow = this.getCollabWindow();
        // Equal is fine here due to SharedSegmentSequence<>.snapshotContent() potentially updating with same #
        (0, common_utils_1.assert)(collabWindow.currentSeq <= seq, 0x038 /* "Incoming op sequence# < local collabWindow's currentSequence#" */);
        collabWindow.currentSeq = seq;
        (0, common_utils_1.assert)(min <= seq, 0x039 /* "Incoming op sequence# < minSequence#" */);
        this.updateMinSeq(min);
    }
    /**
     * Resolves a remote client's position against the local sequence
     * and returns the remote client's position relative to the local
     * sequence
     * @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) {
        const shortRemoteClientId = this.getOrAddShortClientId(remoteClientId);
        return this._mergeTree.resolveRemoteClientPosition(remoteClientPosition, remoteClientRefSeq, shortRemoteClientId);
    }
    /**
     * Given an pending operation and segment group, regenerate the op, so it
     * can be resubmitted
     * @param resetOp - The op to reset
     * @param segmentGroup - The segment group associated with the op
     */
    regeneratePendingOp(resetOp, segmentGroup) {
        const rebaseTo = this.getCollabWindow().currentSeq;
        if (rebaseTo !== this.lastNormalizationRefSeq) {
            this.emit("normalize", this);
            this._mergeTree.normalizeSegmentsOnRebase();
            this.lastNormalizationRefSeq = rebaseTo;
        }
        const opList = [];
        if (resetOp.type === ops_1.MergeTreeDeltaType.GROUP) {
            if (Array.isArray(segmentGroup)) {
                (0, common_utils_1.assert)(resetOp.ops.length === segmentGroup.length, 0x03a /* "Number of ops in 'resetOp' must match the number of segment groups provided." */);
                for (let i = 0; i < resetOp.ops.length; i++) {
                    opList.push(...this.resetPendingDeltaToOps(resetOp.ops[i], segmentGroup[i]));
                }
            }
            else {
                // A group op containing a single op will pass a direct reference to 'segmentGroup'
                // rather than an array of segment groups.  (See 'peekPendingSegmentGroups()')
                (0, common_utils_1.assert)(resetOp.ops.length === 1, 0x03b /* "Number of ops in 'resetOp' must match the number of segment groups provided." */);
                opList.push(...this.resetPendingDeltaToOps(resetOp.ops[0], segmentGroup));
            }
        }
        else {
            (0, common_utils_1.assert)(resetOp.type !== ops_1.MergeTreeDeltaType.GROUP, 0x03c /* "Reset op has 'group' delta type!" */);
            (0, common_utils_1.assert)(!Array.isArray(segmentGroup), 0x03d /* "segmentGroup is array rather than singleton!" */);
            opList.push(...this.resetPendingDeltaToOps(resetOp, segmentGroup));
        }
        return opList.length === 1 ? opList[0] : (0, opBuilder_1.createGroupOp)(...opList);
    }
    createTextHelper() {
        return new MergeTreeTextHelper_1.MergeTreeTextHelper(this._mergeTree);
    }
    summarize(runtime, handle, serializer, catchUpMsgs) {
        var _a;
        const deltaManager = runtime.deltaManager;
        const minSeq = deltaManager.minimumSequenceNumber;
        // Catch up to latest MSN, if we have not had a chance to do it.
        // Required for case where FluidDataStoreRuntime.attachChannel()
        // generates summary right after loading data store.
        this.updateSeqNumbers(minSeq, deltaManager.lastSequenceNumber);
        // One of the summaries (from SPO) I observed to have chunk.chunkSequenceNumber > minSeq!
        // Not sure why - need to catch it sooner
        (0, common_utils_1.assert)(this.getCollabWindow().minSeq === minSeq, 0x03e /* "minSeq mismatch between collab window and delta manager!" */);
        // Must continue to support legacy
        //       (See https://github.com/microsoft/FluidFramework/issues/84)
        if (((_a = this._mergeTree.options) === null || _a === void 0 ? void 0 : _a.newMergeTreeSnapshotFormat) === true) {
            (0, common_utils_1.assert)(catchUpMsgs === undefined || catchUpMsgs.length === 0, 0x03f /* "New format should not emit catchup ops" */);
            const snap = new snapshotV1_1.SnapshotV1(this._mergeTree, this.logger, (id) => this.getLongClientId(id));
            snap.extractSync();
            return snap.emit(serializer, handle);
        }
        else {
            const snap = new snapshotlegacy_1.SnapshotLegacy(this._mergeTree, this.logger);
            snap.extractSync();
            return snap.emit(catchUpMsgs, serializer, handle);
        }
    }
    async load(runtime, storage, serializer) {
        const loader = new snapshotLoader_1.SnapshotLoader(runtime, this, this._mergeTree, this.logger, serializer);
        return loader.initialize(storage);
    }
    getStackContext(startPos, rangeLabels) {
        return this._mergeTree.getStackContext(startPos, this.getCollabWindow().clientId, rangeLabels);
    }
    getLocalSequenceNumber() {
        const segWindow = this.getCollabWindow();
        return segWindow.collaborating ? constants_1.UnassignedSequenceNumber : constants_1.UniversalSequenceNumber;
    }
    localTransaction(groupOp) {
        for (const op of groupOp.ops) {
            const opArgs = {
                op,
                groupOp,
            };
            switch (op.type) {
                case ops_1.MergeTreeDeltaType.INSERT:
                    this.applyInsertOp(opArgs);
                    break;
                case ops_1.MergeTreeDeltaType.ANNOTATE:
                    this.applyAnnotateRangeOp(opArgs);
                    break;
                case ops_1.MergeTreeDeltaType.REMOVE:
                    this.applyRemoveRangeOp(opArgs);
                    break;
                default:
                    break;
            }
        }
    }
    updateConsensusProperty(op, msg) {
        const markerId = op.relativePos1.id;
        const consensusInfo = this.pendingConsensus.get(markerId);
        if (consensusInfo) {
            consensusInfo.marker.addProperties(op.props, op.combiningOp, msg.sequenceNumber);
        }
        this._mergeTree.addMinSeqListener(msg.sequenceNumber, () => consensusInfo.callback(consensusInfo.marker));
    }
    updateMinSeq(minSeq) {
        this._mergeTree.setMinSeq(minSeq);
    }
    getContainingSegment(pos, sequenceArgs, localSeq) {
        const { referenceSequenceNumber, clientId } = this.getClientSequenceArgsForMessage(sequenceArgs);
        return this._mergeTree.getContainingSegment(pos, referenceSequenceNumber, clientId, localSeq);
    }
    /**
     * Returns the position to slide a reference to if a slide is required.
     * @param segoff - The segment and offset to slide from
     * @returns - segment and offset to slide the reference to
     */
    getSlideToSegment(segoff) {
        if (segoff.segment === undefined) {
            return segoff;
        }
        const segment = this._mergeTree._getSlideToSegment(segoff.segment);
        if (segment === segoff.segment) {
            return segoff;
        }
        const offset = segment && segment.ordinal < segoff.segment.ordinal ? segment.cachedLength - 1 : 0;
        return {
            segment,
            offset,
        };
    }
    getPropertiesAtPosition(pos) {
        let propertiesAtPosition;
        const segoff = this.getContainingSegment(pos);
        const seg = segoff.segment;
        if (seg) {
            propertiesAtPosition = seg.properties;
        }
        return propertiesAtPosition;
    }
    getRangeExtentsOfPosition(pos) {
        let posStart;
        let posAfterEnd;
        const segoff = this.getContainingSegment(pos);
        const seg = segoff.segment;
        if (seg) {
            posStart = this.getPosition(seg);
            posAfterEnd = posStart + seg.cachedLength;
        }
        return { posStart, posAfterEnd };
    }
    getCurrentSeq() {
        return this.getCollabWindow().currentSeq;
    }
    getClientId() {
        return this.getCollabWindow().clientId;
    }
    getLength() {
        return this._mergeTree.length;
    }
    startOrUpdateCollaboration(longClientId, minSeq = 0, currentSeq = 0) {
        // we should always have a client id if we are collaborating
        // if the client id is undefined we are likely bound to a detached
        // container, so we should keep going in local mode. once
        // the container attaches this will be called again on connect with the
        // client id
        if (longClientId !== undefined) {
            if (this.longClientId === undefined) {
                this.longClientId = longClientId;
                this.addLongClientId(this.longClientId);
                this._mergeTree.startCollaboration(this.getShortClientId(this.longClientId), minSeq, currentSeq);
            }
            else {
                const oldClientId = this.longClientId;
                const oldData = this.clientNameToIds.get(oldClientId).data;
                this.longClientId = longClientId;
                this.clientNameToIds.put(longClientId, oldData);
                this.shortClientIdMap[oldData] = longClientId;
            }
        }
    }
    findTile(startPos, tileLabel, preceding = true) {
        const clientId = this.getClientId();
        return this._mergeTree.findTile(startPos, clientId, tileLabel, preceding);
    }
}
exports.Client = Client;
//# sourceMappingURL=client.js.map