/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { TypedEventEmitter } from "@fluid-internal/client-utils";
import { assert, unreachableCase, isObject } from "@fluidframework/core-utils/internal";
import { MessageType, } from "@fluidframework/driver-definitions/internal";
import { toDeltaManagerInternal } from "@fluidframework/runtime-utils/internal";
import { LoggingError, UsageError, } from "@fluidframework/telemetry-utils/internal";
import { MergeTreeTextHelper } from "./MergeTreeTextHelper.js";
import { RedBlackTree } from "./collections/index.js";
import { NonCollabClient, UniversalSequenceNumber } from "./constants.js";
import { SlidingPreference } from "./localReference.js";
import { MergeTree, errorIfOptionNotTrue, getSlideToSegoff, isRemovedAndAcked, } from "./mergeTree.js";
import { walkAllChildSegments } from "./mergeTreeNodeWalk.js";
import { compareStrings, isSegmentLeaf, } from "./mergeTreeNodes.js";
import { createAdjustRangeOp, createAnnotateMarkerOp, createAnnotateRangeOp, createGroupOp, createInsertSegmentOp, createObliterateRangeOp, createObliterateRangeOpSided, createRemoveRangeOp, } from "./opBuilder.js";
import { MergeTreeDeltaType, ReferenceType, } from "./ops.js";
import { LocalReconnectingPerspective, PriorPerspective, } from "./perspective.js";
import { DetachedReferencePosition } from "./referencePositions.js";
import { isInserted, isRemoved, overwriteInfo, toRemovalInfo, } from "./segmentInfos.js";
import { Side } from "./sequencePlace.js";
import { SnapshotLoader } from "./snapshotLoader.js";
import { SnapshotV1 } from "./snapshotV1.js";
import { SnapshotLegacy } from "./snapshotlegacy.js";
import * as opstampUtils from "./stamps.js";
const UNBOUND_SEGMENT_ERROR = "The provided segment is not bound to this DDS.";
/**
 * This class encapsulates a merge-tree, and provides a local client specific view over it and
 * the capability to modify it as the local client. Additionally it provides
 * binding for processing remote ops on the encapsulated merge tree, and projects local and remote events
 * caused by all modification to the underlying merge-tree.
 *
 * @internal
 */
export class Client extends TypedEventEmitter {
    /**
     * @param specToSegment - Rehydrates a segment from its JSON representation
     * @param logger - Telemetry logger for diagnostics
     * @param options - Options for this client. See {@link IMergeTreeOptions} for details.
     * @param getMinInFlightRefSeq - Upon applying a message (see {@link Client.applyMsg}), client purges collab-window information which
     * is no longer necessary based on that message's minimum sequence number.
     * However, if the user of this client has in-flight messages which refer to positions in this Client,
     * they may wish to preserve additional merge information.
     * The effective minimum sequence number will be the minimum of the message's minimumSequenceNumber and the result of this function.
     * If this function returns undefined, the message's minimumSequenceNumber will be used.
     *
     * @privateRemarks
     * - Passing specToSegment would be unnecessary if Client were merged with SharedSegmentSequence
     * - AB#6866 tracks a more unified approach to collab window min seq handling.
     */
    constructor(specToSegment, logger, options, getMinInFlightRefSeq = () => undefined) {
        super();
        this.specToSegment = specToSegment;
        this.logger = logger;
        this.getMinInFlightRefSeq = getMinInFlightRefSeq;
        this.clientNameToIds = new RedBlackTree(compareStrings);
        this.shortClientIdMap = [];
        this.cachedObliterateRebases = new Map();
        this._mergeTree = new 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 (options?.attribution?.track) {
            const policy = this._mergeTree?.attributionPolicy;
            if (policy === undefined) {
                throw new 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?.last;
        if (count === 1 || pending === undefined) {
            return node?.data;
        }
        const taken = Array.from({ length: Math.min(count, pending.length) });
        for (let i = taken.length - 1; i >= 0; i--) {
            taken[i] = node.data;
            node = node.prev;
        }
        return taken;
    }
    /**
     * Annotates the markers with the provided properties
     * @param marker - The marker to annotate
     * @param props - The properties to annotate the marker with
     * @returns The annotate op if valid, otherwise undefined
     */
    annotateMarker(marker, props) {
        const annotateOp = createAnnotateMarkerOp(marker, props);
        this.applyAnnotateRangeOp({ op: annotateOp });
        return annotateOp;
    }
    /**
     * 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
     * @returns The annotate op if valid, otherwise undefined
     */
    annotateRangeLocal(start, end, props) {
        const annotateOp = createAnnotateRangeOp(start, end, props);
        this.applyAnnotateRangeOp({ op: annotateOp });
        return annotateOp;
    }
    /**
     * adjusts a value
     */
    annotateAdjustRangeLocal(start, end, adjust) {
        const annotateOp = createAdjustRangeOp(start, end, adjust);
        for (const [key, value] of Object.entries(adjust)) {
            if (value.min !== undefined && value.max !== undefined && value.min > value.max) {
                throw new UsageError(`min is greater than max for ${key}`);
            }
        }
        this.applyAnnotateRangeOp({ op: annotateOp });
        return annotateOp;
    }
    /**
     * 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 = createRemoveRangeOp(start, end);
        this.applyRemoveRangeOp({ op: removeOp });
        return removeOp;
    }
    /**
     * Obliterates the range. This is similar to removing the range, but also
     * includes any concurrently inserted content.
     *
     * @param start - The start of the range to obliterate. Inclusive is side is Before (default).
     * @param end - The end of the range to obliterate. Exclusive is side is After
     * (default is to be after the last included character, but number index is exclusive).
     */
    obliterateRangeLocal(start, end) {
        // eslint-disable-next-line import/no-deprecated
        let obliterateOp;
        if (this._mergeTree.options?.mergeTreeEnableSidedObliterate) {
            obliterateOp = createObliterateRangeOpSided(start, end);
        }
        else {
            assert(typeof start === "number" && typeof end === "number", 0xa42 /* Start and end must be numbers if mergeTreeEnableSidedObliterate is not enabled. */);
            obliterateOp = createObliterateRangeOp(start, end);
        }
        this.applyObliterateRangeOp({ op: obliterateOp });
        return obliterateOp;
    }
    /**
     * Create and insert a segment at the specified position.
     * @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 = createInsertSegmentOp(pos, segment);
        this.applyInsertOp({ op: insertOp });
        return insertOp;
    }
    /**
     * Create and insert a segment at the specified reference position.
     * @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 === DetachedReferencePosition) {
            throw new UsageError("Cannot insert at detached local reference.");
        }
        return this.insertSegmentLocal(pos, segment);
    }
    walkSegments(handler, start, end, accum, splitRange = false) {
        this._mergeTree.mapRange(handler, this._mergeTree.localPerspective, accum, start, end, splitRange);
    }
    walkAllSegments(action, accum) {
        return 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;
        let localObliterates = 0;
        walkAllChildSegments(this._mergeTree.root, (seg) => {
            if (isInserted(seg) && opstampUtils.isLocal(seg.insert)) {
                localInserts++;
            }
            if (isRemoved(seg) && opstampUtils.isLocal(seg.removes[seg.removes.length - 1])) {
                if (seg.removes[seg.removes.length - 1].type === "setRemove") {
                    localRemoves++;
                }
                else {
                    localObliterates++;
                }
            }
            // Only serialize segments that have not been removed.
            if (!isRemoved(seg)) {
                handleCollectingSerializer.stringify(seg.clone().toJSONObject(), handle);
            }
            return true;
        });
        if (localInserts > 0 || localRemoves > 0) {
            this.logger.sendErrorEvent({
                eventName: "LocalEditsInProcessGCData",
                localInserts,
                localRemoves,
                localObliterates,
            });
        }
    }
    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 (!isSegmentLeaf(segment)) {
            return -1;
        }
        const perspective = localSeq === undefined
            ? this._mergeTree.localPerspective
            : new LocalReconnectingPerspective(this.getCurrentSeq(), this.getClientId(), localSeq);
        return this._mergeTree.getPosition(segment, perspective);
    }
    /**
     * 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
     * @param canSlideToEndpoint - Whether or not the created local reference can
     * slide onto one of the special endpoint segments denoting the position
     * before the start of or after the end of the tree
     */
    createLocalReferencePosition(segment, offset, refType, properties, slidingPreference, canSlideToEndpoint) {
        if (!isSegmentLeaf(segment) && typeof segment !== "string") {
            throw new UsageError(UNBOUND_SEGMENT_ERROR);
        }
        return this._mergeTree.createLocalReferencePosition(segment, offset ?? 0, refType, properties, slidingPreference, canSlideToEndpoint);
    }
    /**
     * 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.
     *
     * Reference positions that point to a character that has been removed will
     * always return the position of the nearest non-removed character, regardless
     * of {@link ReferenceType}. To handle this case specifically, one may wish
     * to look at the segment returned by {@link ReferencePosition.getSegment}.
     */
    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, this._mergeTree.localPerspective);
    }
    getMarkerFromId(id) {
        return this._mergeTree.getMarkerFromId(id);
    }
    /**
     * Revert an op
     */
    rollback(op, localOpMetadata) {
        this._mergeTree.rollback(op, localOpMetadata);
    }
    applyObliterateRangeOp(opArgs) {
        const { op, sequencedMessage } = opArgs;
        assert(op.type === MergeTreeDeltaType.OBLITERATE ||
            op.type === MergeTreeDeltaType.OBLITERATE_SIDED, 0x866 /* Unexpected op type on range obliterate! */);
        const perspective = this.getOperationPerspective(sequencedMessage);
        const stamp = this.getOperationStamp(sequencedMessage);
        if (this._mergeTree.options?.mergeTreeEnableSidedObliterate) {
            const { start, end } = this.getValidSidedRange(op, perspective);
            this._mergeTree.obliterateRange(start, end, perspective, stamp, opArgs);
        }
        else {
            assert(op.type === MergeTreeDeltaType.OBLITERATE, 0xa43 /* Unexpected sided obliterate while mergeTreeEnableSidedObliterate is disabled */);
            const { start, end } = this.getValidOpRange(op, perspective);
            this._mergeTree.obliterateRange(start, end, perspective, stamp, opArgs);
        }
    }
    getOperationPerspective(sequencedMessage) {
        if (!sequencedMessage) {
            return this._mergeTree.localPerspective;
        }
        const clientId = this.getOrAddShortClientIdFromMessage(sequencedMessage);
        const { referenceSequenceNumber: refSeq } = sequencedMessage;
        return new PriorPerspective(refSeq, clientId);
    }
    /**
     * Returns the operation stamp to apply for a change, minting a new one local one if necessary.
     */
    getOperationStamp(sequencedMessage) {
        if (!sequencedMessage) {
            return this.getCollabWindow().mintNextLocalOperationStamp();
        }
        const { sequenceNumber: seq } = sequencedMessage;
        const clientId = this.getOrAddShortClientIdFromMessage(sequencedMessage);
        return {
            seq,
            clientId,
        };
    }
    /**
     * Performs the remove based on the provided op
     * @param opArgs - The ops args for the op
     */
    applyRemoveRangeOp(opArgs) {
        const { op, sequencedMessage } = opArgs;
        assert(op.type === MergeTreeDeltaType.REMOVE, 0x02d /* "Unexpected op type on range remove!" */);
        const perspective = this.getOperationPerspective(sequencedMessage);
        const stamp = this.getOperationStamp(sequencedMessage);
        const range = this.getValidOpRange(op, perspective);
        this._mergeTree.markRangeRemoved(range.start, range.end, perspective, stamp, opArgs);
    }
    /**
     * Performs the annotate based on the provided op
     * @param opArgs - The ops args for the op
     */
    applyAnnotateRangeOp(opArgs) {
        const { op, sequencedMessage } = opArgs;
        assert(op.type === MergeTreeDeltaType.ANNOTATE, 0x02e /* "Unexpected op type on range annotate!" */);
        const perspective = this.getOperationPerspective(sequencedMessage);
        const stamp = this.getOperationStamp(sequencedMessage);
        const range = this.getValidOpRange(op, perspective);
        this._mergeTree.annotateRange(range.start, range.end, op, perspective, stamp, opArgs);
    }
    /**
     * 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) {
        const { op, sequencedMessage } = opArgs;
        assert(op.type === MergeTreeDeltaType.INSERT, 0x02f /* "Unexpected op type on range insert!" */);
        const perspective = this.getOperationPerspective(sequencedMessage);
        const stamp = this.getOperationStamp(sequencedMessage);
        const range = this.getValidOpRange(op, perspective);
        // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
        const segments = [this.specToSegment(op.seg)];
        this._mergeTree.insertSegments(range.start, segments, perspective, stamp, opArgs);
    }
    /**
     * Returns a valid range for the op, or throws if the range is invalid
     * @param op - The op to generate the range for
     * @param clientArgs - The client args for the op
     * @throws LoggingError if the range is invalid
     */
    getValidSidedRange(
    // eslint-disable-next-line import/no-deprecated
    op, perspective) {
        const invalidPositions = [];
        let start;
        let end;
        if (op.pos1 === undefined) {
            invalidPositions.push("start");
        }
        else {
            start =
                typeof op.pos1 === "object"
                    ? { pos: op.pos1.pos, side: op.pos1.before ? Side.Before : Side.After }
                    : { pos: op.pos1, side: Side.Before };
        }
        if (op.pos2 === undefined) {
            invalidPositions.push("end");
        }
        else {
            end =
                typeof op.pos2 === "object"
                    ? { pos: op.pos2.pos, side: op.pos2.before ? Side.Before : Side.After }
                    : { pos: op.pos2 - 1, side: Side.After };
        }
        // Validate if local op
        if (perspective.clientId === this.getClientId()) {
            const length = this._mergeTree.getLength(this._mergeTree.localPerspective);
            if (start !== undefined && (start.pos >= length || start.pos < 0)) {
                // start out of bounds
                invalidPositions.push("start");
            }
            if (end !== undefined && (end.pos >= length || end.pos < 0)) {
                invalidPositions.push("end");
            }
            if (start !== undefined &&
                end !== undefined &&
                (start.pos > end.pos ||
                    (start.pos === end.pos && start.side !== end.side && start.side === Side.After))) {
                // end is before start
                invalidPositions.push("inverted");
            }
            if (invalidPositions.length > 0) {
                throw new LoggingError("InvalidRange", {
                    usageError: true,
                    invalidPositions: invalidPositions.toString(),
                    length,
                    opType: op.type,
                    opPos1Relative: op.relativePos1 !== undefined,
                    opPos2Relative: op.relativePos2 !== undefined,
                    opPos1: JSON.stringify(op.pos1),
                    opPos2: JSON.stringify(op.pos2),
                    start: JSON.stringify(start),
                    end: JSON.stringify(end),
                });
            }
        }
        assert(start !== undefined && end !== undefined, 0xa44 /* Missing start or end of range */);
        return { start, end };
    }
    /**
     * 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, perspective) {
        let start = op.pos1;
        if (start === undefined && op.relativePos1) {
            start = this._mergeTree.posFromRelativePos(op.relativePos1, perspective);
        }
        let end = op.pos2;
        if (end === undefined && op.relativePos2) {
            end = this._mergeTree.posFromRelativePos(op.relativePos2, perspective);
        }
        // Validate if local op
        if (perspective.clientId === this.getClientId()) {
            const length = this.getLength();
            const invalidPositions = [];
            // Validate start position
            //
            if (start === undefined ||
                start < 0 ||
                start > length ||
                (start === length && op.type !== MergeTreeDeltaType.INSERT)) {
                invalidPositions.push("start");
            }
            // Validate end if not insert, or insert has end
            if ((op.type !== MergeTreeDeltaType.INSERT || end !== undefined) &&
                (end === undefined || end <= start)) {
                invalidPositions.push("end");
            }
            if (op.type === MergeTreeDeltaType.OBLITERATE && end !== undefined && end > length) {
                invalidPositions.push("end");
            }
            if (invalidPositions.length > 0) {
                throw new 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.
        return { start: start, end: end };
    }
    ackPendingSegment(opArgs) {
        if (opArgs.op.type === MergeTreeDeltaType.GROUP) {
            for (const memberOp of opArgs.op.ops) {
                this._mergeTree.ackOp({
                    groupOp: opArgs.op,
                    op: memberOp,
                    sequencedMessage: opArgs.sequencedMessage,
                });
            }
        }
        else {
            this._mergeTree.ackOp(opArgs);
        }
    }
    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) {
        return this.getOrAddShortClientId(msg.clientId ?? "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) {
        assert(localSeq <= this._mergeTree.collabWindow.localSeq, 0x032 /* "localSeq greater than collab window" */);
        const { currentSeq, clientId } = this.getCollabWindow();
        if (!isSegmentLeaf(segment)) {
            throw new UsageError(UNBOUND_SEGMENT_ERROR);
        }
        const perspective = new LocalReconnectingPerspective(currentSeq, clientId, localSeq);
        return this._mergeTree.getPosition(segment, perspective);
    }
    /**
     * Rebases a sided local reference to the best fitting position in the current tree.
     */
    rebaseSidedLocalReference(ref, side, reconnectingPerspective, slidePreference) {
        const oldSegment = ref.getSegment();
        const oldOffset = ref.getOffset();
        assert(oldSegment !== undefined && oldOffset !== undefined, 0xb61 /* Invalid old reference */);
        const useNewSlidingBehavior = true;
        // Destructuring segment + offset is convenient and segment is reassigned
        // eslint-disable-next-line prefer-const
        let { segment: newSegment, offset: newOffset } = getSlideToSegoff({ segment: oldSegment, offset: oldOffset }, slidePreference, reconnectingPerspective, useNewSlidingBehavior);
        newSegment ??=
            slidePreference === SlidingPreference.FORWARD
                ? this._mergeTree.endOfTree
                : this._mergeTree.startOfTree;
        assert(isSegmentLeaf(newSegment) && newOffset !== undefined, 0xb62 /* Invalid new segment on rebase */);
        const newSide = newSegment === oldSegment
            ? side
            : // If the reference slid to a new position, the closest fit to the original position will be independent of
                // the original side and "in the direction of where the reference was".
                slidePreference === SlidingPreference.FORWARD
                    ? Side.Before
                    : Side.After;
        return { segment: newSegment, offset: newOffset, side: newSide };
    }
    computeNewObliterateEndpoints(obliterateInfo) {
        const { currentSeq, clientId } = this.getCollabWindow();
        const reconnectingPerspective = new LocalReconnectingPerspective(currentSeq, clientId, obliterateInfo.stamp.localSeq - 1);
        const newStart = this.rebaseSidedLocalReference(obliterateInfo.start, obliterateInfo.startSide, reconnectingPerspective, SlidingPreference.FORWARD);
        const newEnd = this.rebaseSidedLocalReference(obliterateInfo.end, obliterateInfo.endSide, reconnectingPerspective, SlidingPreference.BACKWARD);
        return {
            start: newStart,
            end: newEnd,
        };
    }
    resetPendingDeltaToOps(resetOp, segmentGroup) {
        assert(!!segmentGroup, 0x033 /* "Segment group undefined" */);
        const NACKedSegmentGroup = this.pendingRebase?.shift()?.data;
        assert(segmentGroup === NACKedSegmentGroup, 0x034 /* "Segment group not at head of pending rebase queue" */);
        assert(segmentGroup.localSeq !== undefined, 0x867 /* expected segment group localSeq to be defined */);
        if (this.pendingRebase?.empty) {
            this.pendingRebase = undefined;
        }
        if (resetOp.type === MergeTreeDeltaType.OBLITERATE ||
            resetOp.type === MergeTreeDeltaType.OBLITERATE_SIDED) {
            errorIfOptionNotTrue(this._mergeTree.options, "mergeTreeEnableObliterateReconnect");
            // sliceRemove reconnect logic is characteristically different from other ops (which can only apply to segments they originally saw).
            // This is because the ranges that other ops apply to can be broken up by concurrent insertions, so even though setRemoves are originally
            // applied to a contiguous set of segments, at resubmission time they may no longer be.
            // On the other hand, the closest analog to a `sliceRemove` that we can submit is obtained by resolving the "closest" start and end points
            // for that slice, updating the local obliterate metadata to reflect that slice, and submitting a single op.
            const obliterateInfo = segmentGroup.obliterateInfo;
            assert(obliterateInfo !== undefined, 0xb63 /* Resubmitting obliterate op without obliterate info in segment group */);
            assert(obliterateInfo.stamp.localSeq === segmentGroup.localSeq, 0xb64 /* Local seq mismatch */);
            const cachedNewPositions = this.cachedObliterateRebases.get(obliterateInfo.stamp.localSeq);
            assert(cachedNewPositions !== undefined, 0xb65 /* didn't compute new positions for obliterate on reconnect early enough */);
            const { start: { segment: newStartSegment, offset: newStartOffset, side: newStartSide }, end: { segment: newEndSegment, offset: newEndOffset, side: newEndSide }, } = cachedNewPositions;
            const { currentSeq, clientId } = this.getCollabWindow();
            if (newEndSegment.ordinal < newStartSegment.ordinal) {
                for (const segment of segmentGroup.segments) {
                    assert(isRemovedAndAcked(segment), 0xb66 /* On reconnect, obliterate applied to new segments even though original ones were not removed. */);
                    const lastRemove = segment.removes[segment.removes.length - 1];
                    assert(lastRemove.type === "sliceRemove" && lastRemove.localSeq === segmentGroup.localSeq, 0xb67 /* Last remove should be the obliterate that is being resubmitted. */);
                    // The original obliterate affected this segment, but it has since been removed and overlapping removes
                    // are only possible when they are concurrent. We adjust the metadata on that segment now to reflect
                    // the fact that the obliterate no longer affects it.
                    segment.removes.pop();
                }
                this._mergeTree.rebaseObliterateTo(obliterateInfo, undefined);
                return [];
            }
            assert(obliterateInfo.tiebreakTrackingGroup !== undefined, 0xb68 /* Tiebreak tracking group missing */);
            const newObliterate = {
                // Recreate the start position using the perspective that other clients will see.
                // This may not be at the same position as the original reference, since the segment the original reference was on could have been removed.
                start: this._mergeTree.createLocalReferencePosition(newStartSegment, newStartOffset, ReferenceType.StayOnRemove, undefined),
                startSide: newStartSide,
                end: this._mergeTree.createLocalReferencePosition(newEndSegment, newEndOffset, ReferenceType.StayOnRemove, undefined),
                endSide: newEndSide,
                refSeq: currentSeq,
                // We reuse the localSeq from the original obliterate.
                stamp: obliterateInfo.stamp,
                segmentGroup: undefined,
                tiebreakTrackingGroup: obliterateInfo.tiebreakTrackingGroup,
            };
            newObliterate.start.addProperties({ obliterate: newObliterate });
            newObliterate.end.addProperties({ obliterate: newObliterate });
            newObliterate.segmentGroup = {
                segments: [],
                localSeq: segmentGroup.localSeq,
                refSeq: this.getCollabWindow().currentSeq,
                obliterateInfo: newObliterate,
            };
            for (const segment of segmentGroup.segments) {
                assert(segment.segmentGroups?.remove(segmentGroup) === true, 0x035 /* "Segment group not in segment pending queue" */);
                if ((segment.ordinal > newStartSegment.ordinal &&
                    segment.ordinal < newEndSegment.ordinal) ||
                    (segment === newStartSegment && newStartSide === Side.Before) ||
                    (segment === newEndSegment && newEndSide === Side.After)) {
                    segment.segmentGroups.enqueue(newObliterate.segmentGroup);
                }
                else {
                    assert(isRemovedAndAcked(segment), 0xb69 /* On reconnect, obliterate applied to new segments even though original ones were not removed. */);
                    const lastRemove = segment.removes[segment.removes.length - 1];
                    assert(lastRemove.type === "sliceRemove" && lastRemove.localSeq === segmentGroup.localSeq, 0xb6a /* Last remove should be the obliterate that is being resubmitted. */);
                    // The original obliterate affected this segment, but it has since been removed and it's impossible to apply the
                    // local obliterate so that is so. We adjust the metadata on that segment now.
                    segment.removes.pop();
                }
            }
            this._mergeTree.rebaseObliterateTo(obliterateInfo, newObliterate);
            this._mergeTree.pendingSegments.push(newObliterate.segmentGroup);
            const reconnectingPerspective = new LocalReconnectingPerspective(currentSeq, clientId, obliterateInfo.stamp.localSeq - 1);
            const newStartPos = this._mergeTree.getPosition(newStartSegment, reconnectingPerspective) + newStartOffset;
            const newEndPos = this._mergeTree.getPosition(newEndSegment, reconnectingPerspective) + newEndOffset;
            if (resetOp.type === MergeTreeDeltaType.OBLITERATE) {
                assert(newStartSide === Side.Before && newEndSide === Side.After, 0xb6b /* Non-sided obliterate should have start side before and end side after */);
                // Use a non-sided obliterate op if the original op was non-sided. Some combinations of feature flags disallow sided obliterate ops
                // but allow non-sided ones, and if we convert a non-sided op to a sided one on reconnect, we may cause errors.
                return [
                    createObliterateRangeOp(newStartPos, newEndPos +
                        1 /* to make the end exclusive, see corresponding -1 in `createObliterateRangeOpSided` on converting non-sided to sided. */),
                ];
            }
            return [
                createObliterateRangeOpSided({
                    pos: newStartPos,
                    side: newStartSide,
                }, {
                    pos: newEndPos,
                    side: newEndSide,
                }),
            ];
        }
        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)) {
            assert(segment.segmentGroups?.remove(segmentGroup) === true, 0xb6c /* Segment group not in segment pending queue */);
            const segmentPosition = this.findReconnectionPosition(segment, segmentGroup.localSeq);
            let newOp;
            switch (resetOp.type) {
                case MergeTreeDeltaType.ANNOTATE: {
                    assert(segment.propertyManager?.hasPendingProperties(resetOp.props ?? resetOp.adjust) ===
                        true, 0x036 /* "Segment has no pending properties" */);
                    // if the segment has been removed or obliterated, 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 (!isRemovedAndAcked(segment)) {
                        newOp =
                            resetOp.props === undefined
                                ? createAdjustRangeOp(segmentPosition, segmentPosition + segment.cachedLength, resetOp.adjust)
                                : createAnnotateRangeOp(segmentPosition, segmentPosition + segment.cachedLength, resetOp.props);
                    }
                    break;
                }
                case MergeTreeDeltaType.INSERT: {
                    assert(isInserted(segment) && opstampUtils.isLocal(segment.insert), 0x037 /* "Segment already has assigned sequence number" */);
                    const removeInfo = toRemovalInfo(segment);
                    if (removeInfo !== undefined && opstampUtils.isAcked(removeInfo.removes[0])) {
                        assert(removeInfo.removes[0].type === "sliceRemove", 0xb5c /* Remove on insertion must be caused by obliterate. */);
                        errorIfOptionNotTrue(this._mergeTree.options, "mergeTreeEnableObliterateReconnect");
                        // the segment was remotely obliterated, so is considered removed
                        // we set the seq to the universal seq and remove the local seq,
                        // so its length is not considered for subsequent local changes
                        // this allows us to not send the op as even the local client will ignore the segment
                        overwriteInfo(segment, {
                            insert: {
                                type: "insert",
                                seq: UniversalSequenceNumber,
                                localSeq: undefined,
                                clientId: NonCollabClient,
                            },
                        });
                        break;
                    }
                    const segInsertOp = segment.clone();
                    const opProps = isObject(resetOp.seg) && "props" in resetOp.seg && isObject(resetOp.seg.props)
                        ? { ...resetOp.seg.props }
                        : undefined;
                    segInsertOp.properties = opProps;
                    newOp = createInsertSegmentOp(segmentPosition, segInsertOp);
                    break;
                }
                case MergeTreeDeltaType.REMOVE: {
                    // Only bother resubmitting if nobody else has removed it in the meantime.
                    // When that happens, the first removal will have been acked.
                    if (isRemoved(segment) && opstampUtils.isLocal(segment.removes[0])) {
                        newOp = 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 MergeTreeDeltaType.INSERT: {
                this.applyInsertOp(opArgs);
                break;
            }
            case MergeTreeDeltaType.REMOVE: {
                this.applyRemoveRangeOp(opArgs);
                break;
            }
            case MergeTreeDeltaType.ANNOTATE: {
                this.applyAnnotateRangeOp(opArgs);
                break;
            }
            case MergeTreeDeltaType.OBLITERATE:
            case MergeTreeDeltaType.OBLITERATE_SIDED: {
                this.applyObliterateRangeOp(opArgs);
                break;
            }
            case MergeTreeDeltaType.GROUP: {
                for (const memberOp of op.ops) {
                    this.applyRemoteOp({
                        op: memberOp,
                        groupOp: op,
                        sequencedMessage: msg,
                    });
                }
                break;
            }
            default: {
                break;
            }
        }
    }
    applyStashedOp(op) {
        switch (op.type) {
            case MergeTreeDeltaType.INSERT: {
                this.applyInsertOp({ op });
                break;
            }
            case MergeTreeDeltaType.REMOVE: {
                this.applyRemoveRangeOp({ op });
                break;
            }
            case MergeTreeDeltaType.ANNOTATE: {
                this.applyAnnotateRangeOp({ op });
                break;
            }
            case MergeTreeDeltaType.OBLITERATE_SIDED:
            case MergeTreeDeltaType.OBLITERATE: {
                this.applyObliterateRangeOp({ op });
                break;
            }
            case MergeTreeDeltaType.GROUP: {
                op.ops.map((o) => this.applyStashedOp(o));
                break;
            }
            default: {
                unreachableCase(op, "unrecognized op type");
            }
        }
    }
    applyMsg(msg, local = false) {
        // Ensure client ID is registered
        this.getOrAddShortClientIdFromMessage(msg);
        // Apply if an operation message
        if (msg.type === MessageType.Operation) {
            const opArgs = {
                op: msg.contents,
                sequencedMessage: msg,
            };
            if (opArgs.sequencedMessage?.clientId === this.longClientId || local) {
                this.ackPendingSegment(opArgs);
            }
            else {
                this.applyRemoteOp(opArgs);
            }
        }
        const min = Math.min(this.getMinInFlightRefSeq() ?? Number.POSITIVE_INFINITY, msg.minimumSequenceNumber);
        this.updateSeqNumbers(min, msg.sequenceNumber);
    }
    updateSeqNumbers(min, seq) {
        const collabWindow = this.getCollabWindow();
        // Equal is fine here due to SharedSegmentSequence<>.snapshotContent() potentially updating with same #
        assert(collabWindow.currentSeq <= seq, 0x038 /* "Incoming op sequence# < local collabWindow's currentSequence#" */);
        collabWindow.currentSeq = seq;
        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 a 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, localOpMetadata) {
        const segmentGroup = localOpMetadata;
        if (this.pendingRebase === undefined || this.pendingRebase.empty) {
            let firstGroup;
            if (Array.isArray(segmentGroup)) {
                if (segmentGroup.length === 0) {
                    // sometimes we rebase to an empty op
                    return createGroupOp();
                }
                firstGroup = segmentGroup[0];
            }
            else {
                firstGroup = segmentGroup;
            }
            const firstGroupNode = this._mergeTree.pendingSegments.find((node) => node.data === firstGroup);
            assert(firstGroupNode !== undefined, 0x70e /* segment group must exist in pending list */);
            this.pendingRebase = this._mergeTree.pendingSegments.splice(firstGroupNode);
        }
        const collabWindow = this.getCollabWindow();
        if (this.lastNormalization === undefined ||
            collabWindow.currentSeq !== this.lastNormalization.refSeq ||
            collabWindow.localSeq !== this.lastNormalization.localRefSeq) {
            // Compute obliterate endpoint destinations before segments are normalized.
            // Segment normalization can affect what should be the semantically correct segments for the endpoints to be placed on.
            this.cachedObliterateRebases.clear();
            for (const group of [...this._mergeTree.pendingSegments, ...this.pendingRebase]) {
                const { obliterateInfo } = group.data;
                if (obliterateInfo !== undefined) {
                    const { start, end } = this.computeNewObliterateEndpoints(obliterateInfo);
                    const { localSeq } = obliterateInfo.stamp;
                    assert(localSeq !== undefined, 0xb6d /* Local seq must be defined */);
                    this.cachedObliterateRebases.set(localSeq, { start, end });
                }
            }
            this.emit("normalize", this);
            this._mergeTree.normalizeSegmentsOnRebase();
            this.lastNormalization = {
                refSeq: collabWindow.currentSeq,
                localRefSeq: collabWindow.localSeq,
            };
        }
        const opList = [];
        if (resetOp.type === MergeTreeDeltaType.GROUP) {
            if (Array.isArray(segmentGroup)) {
                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()')
                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 {
            assert(resetOp.type !== MergeTreeDeltaType.GROUP, 0x03c /* "Reset op has 'group' delta type!" */);
            assert(!Array.isArray(segmentGroup), 0x03d /* "segmentGroup is array rather than singleton!" */);
            opList.push(...this.resetPendingDeltaToOps(resetOp, segmentGroup));
        }
        return opList.length === 1 ? opList[0] : createGroupOp(...opList);
    }
    createTextHelper() {
        return new MergeTreeTextHelper(this._mergeTree);
    }
    summarize(runtime, handle, serializer, catchUpMsgs) {
        const deltaManager = toDeltaManagerInternal(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
        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 (this._mergeTree.options?.newMergeTreeSnapshotFormat === true) {
            assert(catchUpMsgs === undefined || catchUpMsgs.length === 0, 0x03f /* "New format should not emit catchup ops" */);
            const snap = new SnapshotV1(this._mergeTree, this.logger, (id) => this.getLongClientId(id));
            snap.extractSync();
            return snap.emit(serializer, handle);
        }
        else {
            const snap = new SnapshotLegacy(this._mergeTree, this.logger);
            snap.extractSync();
            return snap.emit(catchUpMsgs, serializer, handle);
        }
    }
    async load(runtime, storage, serializer) {
        const loader = new SnapshotLoader(runtime, this, this._mergeTree, this.logger, serializer);
        return loader.initialize(storage);
    }
    // eslint-disable-next-line import/no-deprecated
    localTransaction(groupOp) {
        for (const op of groupOp.ops) {
            const opArgs = {
                op,
                groupOp,
            };
            switch (op.type) {
                case MergeTreeDeltaType.INSERT: {
                    this.applyInsertOp(opArgs);
                    break;
                }
                case MergeTreeDeltaType.ANNOTATE: {
                    this.applyAnnotateRangeOp(opArgs);
                    break;
                }
                case MergeTreeDeltaType.REMOVE: {
                    this.applyRemoveRangeOp(opArgs);
                    break;
                }
                case MergeTreeDeltaType.OBLITERATE_SIDED:
                case MergeTreeDeltaType.OBLITERATE: {
                    this.applyObliterateRangeOp(opArgs);
                    break;
                }
                default: {
                    break;
                }
            }
        }
    }
    updateMinSeq(minSeq) {
        this._mergeTree.setMinSeq(minSeq);
    }
    getContainingSegment(pos, sequenceArgs, localSeq) {
        let perspective;
        const clientId = sequenceArgs === undefined
            ? this.getClientId()
            : this.getOrAddShortClientIdFromMessage(sequenceArgs);
        const refSeq = sequenceArgs?.referenceSequenceNumber ?? this.getCollabWindow().currentSeq;
        if (localSeq !== undefined) {
            perspective = new LocalReconnectingPerspective(refSeq, clientId, localSeq);
        }
        else if (sequenceArgs === undefined) {
            perspective = this._mergeTree.localPerspective;
        }
        else {
            perspective = new PriorPerspective(refSeq, clientId);
        }
        return this._mergeTree.getContainingSegment(pos, perspective);
    }
    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 ?? 0;
    }
    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;
            }
        }
    }
    /**
     * Searches a string for the nearest marker in either direction to a given start position.
     * The search will include the start position, so markers at the start position are valid
     * results of the search. Makes use of block-accelerated search functions for log(n) complexity.
     *
     * @param startPos - Position at which to start the search
     * @param markerLabel - Label of the marker to search for
     * @param forwards - Whether the desired marker comes before (false) or after (true) `startPos`
     */
    searchForMarker(startPos, markerLabel, forwards = true) {
        return this._mergeTree.searchForMarker(startPos, markerLabel, forwards);
    }
}
//# sourceMappingURL=client.js.map