/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { assert } from "@fluidframework/core-utils/internal";
import { SummaryTreeBuilder } from "@fluidframework/runtime-utils/internal";
import { createChildLogger, } from "@fluidframework/telemetry-utils/internal";
import { NonCollabClient, UnassignedSequenceNumber } from "./constants.js";
import { matchProperties } from "./properties.js";
import { isInserted, isRemoved } from "./segmentInfos.js";
import { serializeAsMinSupportedVersion, } from "./snapshotChunks.js";
/**
 * @internal
 */
export class SnapshotLegacy {
    constructor(mergeTree, logger, filename, onCompletion) {
        this.mergeTree = mergeTree;
        this.filename = filename;
        this.onCompletion = onCompletion;
        this.logger = createChildLogger({ logger, namespace: "Snapshot" });
        this.chunkSize =
            mergeTree?.options?.mergeTreeSnapshotChunkSize ?? SnapshotLegacy.sizeOfFirstChunk;
    }
    getSeqLengthSegs(allSegments, approxSequenceLength, startIndex = 0) {
        const segs = [];
        let sequenceLength = 0;
        let segCount = 0;
        let segsWithAttribution = 0;
        while (sequenceLength < approxSequenceLength &&
            startIndex + segCount < allSegments.length) {
            const pseg = allSegments[startIndex + segCount];
            segs.push(pseg);
            if (pseg.attribution) {
                segsWithAttribution++;
            }
            sequenceLength += pseg.cachedLength;
            segCount++;
        }
        assert(segsWithAttribution === 0 || segsWithAttribution === segCount, 0x4bf /* all or no segments should have attribution */);
        const attributionSerializer = this.mergeTree.attributionPolicy?.serializer;
        assert(segsWithAttribution === 0 || attributionSerializer !== undefined, 0x559 /* attribution serializer must be provided when there are segments with attribution. */);
        return {
            version: undefined,
            chunkStartSegmentIndex: startIndex,
            chunkSegmentCount: segCount,
            chunkLengthChars: sequenceLength,
            totalLengthChars: this.header.segmentsTotalLength,
            totalSegmentCount: allSegments.length,
            chunkSequenceNumber: this.header.seq,
            segmentTexts: segs.map((seg) => seg.toJSONObject()),
            attribution: segsWithAttribution > 0 || this.mergeTree.attributionPolicy?.isAttached
                ? attributionSerializer?.serializeAttributionCollections(segs)
                : undefined,
        };
    }
    /**
     * Emits the snapshot to an ISummarizeResult. If provided the optional IFluidSerializer will be used when
     * serializing the summary data rather than JSON.stringify.
     */
    emit(catchUpMsgs, serializer, bind) {
        const chunk1 = this.getSeqLengthSegs(this.segments, this.chunkSize);
        let length = chunk1.chunkLengthChars;
        let segments = chunk1.chunkSegmentCount;
        const builder = new SummaryTreeBuilder();
        builder.addBlob(SnapshotLegacy.header, serializeAsMinSupportedVersion(SnapshotLegacy.header, chunk1, this.logger, this.mergeTree.options, serializer, bind));
        if (chunk1.chunkSegmentCount < chunk1.totalSegmentCount) {
            const chunk2 = this.getSeqLengthSegs(this.segments, this.header.segmentsTotalLength, chunk1.chunkSegmentCount);
            length += chunk2.chunkLengthChars;
            segments += chunk2.chunkSegmentCount;
            builder.addBlob(SnapshotLegacy.body, serializeAsMinSupportedVersion(SnapshotLegacy.body, chunk2, this.logger, this.mergeTree.options, serializer, bind));
        }
        assert(length === this.header.segmentsTotalLength, 0x05d /* "emit: mismatch in segmentsTotalLength" */);
        assert(segments === chunk1.totalSegmentCount, 0x05e /* "emit: mismatch in totalSegmentCount" */);
        if (catchUpMsgs !== undefined && catchUpMsgs.length > 0) {
            // Messages used to have a "term" property which has since been removed.
            // It is benign so it doesn't really need to be deleted here, but doing so permits snapshot tests
            // to pass with an exact match (and matching the updated definition of ISequencedDocumentMessage).
            for (const message of catchUpMsgs) {
                // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
                delete message.term;
            }
            builder.addBlob(this.mergeTree.options?.catchUpBlobName ?? SnapshotLegacy.catchupOps, serializer ? serializer.stringify(catchUpMsgs, bind) : JSON.stringify(catchUpMsgs));
        }
        return builder.getSummaryTree();
    }
    extractSync() {
        const collabWindow = this.mergeTree.collabWindow;
        const seq = (this.seq = collabWindow.minSeq);
        this.header = {
            segmentsTotalLength: this.mergeTree.getLength(this.mergeTree.collabWindow.minSeq, NonCollabClient),
            seq: this.mergeTree.collabWindow.minSeq,
        };
        let originalSegments = 0;
        const segs = [];
        let prev;
        const extractSegment = (segment) => {
            if (isInserted(segment) &&
                segment.seq !== UnassignedSequenceNumber &&
                segment.seq <= seq &&
                (!isRemoved(segment) ||
                    segment.removedSeq === UnassignedSequenceNumber ||
                    segment.removedSeq > seq)) {
                originalSegments += 1;
                const properties = segment.propertyManager?.getAtSeq(segment.properties, seq) ?? segment.properties;
                if (prev?.canAppend(segment) && matchProperties(prev.properties, properties)) {
                    prev.append(segment.clone());
                }
                else {
                    prev = segment.clone();
                    prev.properties = properties;
                    segs.push(prev);
                }
            }
            return true;
        };
        this.mergeTree.mapRange(extractSegment, this.seq, NonCollabClient, undefined);
        this.segments = [];
        let totalLength = 0;
        segs.map((segment) => {
            totalLength += segment.cachedLength;
            if (segment.properties !== undefined && Object.keys(segment.properties).length === 0) {
                segment.properties = undefined;
            }
            this.segments.push(segment);
        });
        // To reduce potential spam from this telemetry, we sample only a small
        // percentage of summaries
        if (Math.abs(originalSegments - segs.length) > 500 && Math.random() < 0.005) {
            this.logger.sendTelemetryEvent({
                eventName: "MergeTreeLegacySummarizeSegmentCount",
                originalSegments,
                segmentsAfterCombine: segs.length,
                segmentsLen: this.segments.length,
            });
        }
        // We observed this.header.segmentsTotalLength < totalLength to happen in some cases
        // When this condition happens, we might not write out all segments in getSeqLengthSegs()
        // when writing out "body". Issue #1995 tracks following up on the core of the problem.
        // In the meantime, this code makes sure we will write out all segments properly
        if (this.header.segmentsTotalLength !== totalLength) {
            this.logger.sendErrorEvent({
                eventName: "SegmentsTotalLengthMismatch",
                totalLength,
                segmentsTotalLength: this.header.segmentsTotalLength,
            });
            this.header.segmentsTotalLength = totalLength;
        }
        return this.segments;
    }
}
SnapshotLegacy.header = "header";
SnapshotLegacy.body = "body";
SnapshotLegacy.catchupOps = "catchupOps";
// Split snapshot into two entries - headers (small) and body (overflow) for faster loading initial content
// Please note that this number has no direct relationship to anything other than size of raw text (characters).
// As we produce json for the blob (and then send over the wire compressed), this number
// is really hard to correlate with any actual metric that matters (like bytes over the wire).
// For test with small number of chunks it would be closer to blob size,
// for very chunky text, blob size can easily be 4x-8x of that number.
SnapshotLegacy.sizeOfFirstChunk = 10000;
//# sourceMappingURL=snapshotlegacy.js.map