"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.SnapshotLoader = void 0;
/* eslint-disable @typescript-eslint/no-non-null-assertion */
const client_utils_1 = require("@fluid-internal/client-utils");
const container_definitions_1 = require("@fluidframework/container-definitions");
const internal_1 = require("@fluidframework/core-utils/internal");
const internal_2 = require("@fluidframework/telemetry-utils/internal");
const constants_js_1 = require("./constants.js");
const segmentInfos_js_1 = require("./segmentInfos.js");
const snapshotChunks_js_1 = require("./snapshotChunks.js");
const snapshotV1_js_1 = require("./snapshotV1.js");
const snapshotlegacy_js_1 = require("./snapshotlegacy.js");
class SnapshotLoader {
    constructor(runtime, client, mergeTree, logger, serializer) {
        this.runtime = runtime;
        this.client = client;
        this.mergeTree = mergeTree;
        this.serializer = serializer;
        this.specToSegment = (spec) => {
            if ((0, snapshotChunks_js_1.hasMergeInfo)(spec)) {
                const seg = (0, segmentInfos_js_1.overwriteInfo)(this.client.specToSegment(spec.json), {
                    clientId: spec.client === undefined
                        ? constants_js_1.NonCollabClient
                        : this.client.getOrAddShortClientId(spec.client),
                    seq: spec.seq ?? constants_js_1.UniversalSequenceNumber,
                });
                if (spec.removedSeq !== undefined) {
                    // this format had a bug where it didn't store all the overlap clients
                    // this is for back compat, so we change the singular id to an array
                    // this will only cause problems if there is an overlapping delete
                    // spanning the snapshot, which should be rare
                    const specAsBuggyFormat = spec;
                    if (specAsBuggyFormat.removedClient !== undefined) {
                        spec.removedClientIds ??= [specAsBuggyFormat.removedClient];
                    }
                    (0, internal_1.assert)(spec.removedClientIds !== undefined, 0xaac /* must have removedClient ids */);
                    (0, segmentInfos_js_1.overwriteInfo)(seg, {
                        removedSeq: spec.removedSeq,
                        removedClientIds: spec.removedClientIds.map((id) => this.client.getOrAddShortClientId(id)),
                    });
                }
                if (spec.movedSeq !== undefined) {
                    (0, internal_1.assert)(spec.movedClientIds !== undefined && spec.movedSeqs !== undefined, 0xaa5 /* must have movedIds ids */);
                    (0, segmentInfos_js_1.overwriteInfo)(seg, {
                        movedSeq: spec.movedSeq,
                        movedSeqs: spec.movedSeqs,
                        movedClientIds: spec.movedClientIds.map((id) => this.client.getOrAddShortClientId(id)),
                        // TODO:AB#29553: This property should be derived from segment data, not hard-coded.
                        wasMovedOnInsert: false,
                    });
                }
                return seg;
            }
            return (0, segmentInfos_js_1.overwriteInfo)(this.client.specToSegment(spec), {
                seq: constants_js_1.UniversalSequenceNumber,
                clientId: constants_js_1.NonCollabClient,
            });
        };
        this.logger = (0, internal_2.createChildLogger)({ logger, namespace: "SnapshotLoader" });
    }
    async initialize(services) {
        const headerLoadedP = services.readBlob(snapshotlegacy_js_1.SnapshotLegacy.header).then((header) => {
            (0, internal_1.assert)(!!header, 0x05f /* "Missing blob header on legacy snapshot!" */);
            return this.loadHeader((0, client_utils_1.bufferToString)(header, "utf8"));
        });
        const catchupOpsP = this.loadBodyAndCatchupOps(headerLoadedP, services);
        catchupOpsP.catch((error) => this.logger.sendErrorEvent({ eventName: "CatchupOpsLoadFailure" }, error));
        await headerLoadedP;
        return { catchupOpsP };
    }
    async loadBodyAndCatchupOps(headerChunkP, services) {
        const blobsP = services.list("");
        const headerChunk = await headerChunkP;
        // TODO we shouldn't need to wait on the body being complete to finish initialization.
        // To fully support this we need to be able to process inbound ops for pending segments.
        await this.loadBody(headerChunk, services);
        const blobs = await blobsP;
        if (blobs.length === headerChunk.headerMetadata.orderedChunkMetadata.length + 1) {
            for (const md of headerChunk.headerMetadata.orderedChunkMetadata)
                blobs.splice(blobs.indexOf(md.id), 1);
            (0, internal_1.assert)(blobs.length === 1, 0x060 /* There should be only one blob with catch up ops */);
            // TODO: The 'Snapshot.catchupOps' tree entry is purely for backwards compatibility.
            //       (See https://github.com/microsoft/FluidFramework/issues/84)
            return this.loadCatchupOps(services.readBlob(blobs[0]), this.serializer);
        }
        else if (blobs.length !== headerChunk.headerMetadata.orderedChunkMetadata.length) {
            throw new Error("Unexpected blobs in snapshot");
        }
        return [];
    }
    loadHeader(header) {
        const chunk = snapshotV1_js_1.SnapshotV1.processChunk(snapshotlegacy_js_1.SnapshotLegacy.header, header, this.logger, this.mergeTree.options, this.serializer);
        const segs = chunk.segments.map((element) => this.specToSegment(element));
        this.extractAttribution(segs, chunk);
        this.mergeTree.reloadFromSegments(segs);
        if (chunk.headerMetadata === undefined) {
            throw new Error("header metadata not available");
        }
        // If we load a detached container from snapshot, then we don't supply a default clientId
        // because we don't want to start collaboration.
        if (this.runtime.attachState !== container_definitions_1.AttachState.Detached) {
            // specify a default client id, "snapshot" here as we
            // should enter collaboration/op sending mode if we load
            // a snapshot in any case (summary or attach message)
            // once we get a client id this will be called with that
            // clientId in the connected event
            this.client.startOrUpdateCollaboration(this.runtime.clientId ?? "snapshot", 
            // TODO: Make 'minSeq' non-optional once the new snapshot format becomes the default?
            //       (See https://github.com/microsoft/FluidFramework/issues/84)
            /* minSeq: */ chunk.headerMetadata.minSequenceNumber ??
                chunk.headerMetadata.sequenceNumber, 
            /* currentSeq: */ chunk.headerMetadata.sequenceNumber);
        }
        return chunk;
    }
    async loadBody(chunk1, services) {
        const headerMetadata = chunk1.headerMetadata;
        (0, internal_1.assert)(chunk1.length <= headerMetadata.totalLength, 0x061 /* "Mismatch in totalLength" */);
        (0, internal_1.assert)(chunk1.segmentCount <= headerMetadata.totalSegmentCount, 0x062 /* "Mismatch in totalSegmentCount" */);
        if (chunk1.segmentCount === headerMetadata.totalSegmentCount) {
            return;
        }
        let chunksWithAttribution = chunk1.attribution === undefined ? 0 : 1;
        const segs = [];
        let lengthSofar = chunk1.length;
        for (let chunkIndex = 1; chunkIndex < headerMetadata.orderedChunkMetadata.length; chunkIndex++) {
            const chunk = await snapshotV1_js_1.SnapshotV1.loadChunk(services, headerMetadata.orderedChunkMetadata[chunkIndex].id, this.logger, this.mergeTree.options, this.serializer);
            lengthSofar += chunk.length;
            // Deserialize each chunk segment and append it to the end of the MergeTree.
            const newSegs = chunk.segments.map((element) => this.specToSegment(element));
            this.extractAttribution(newSegs, chunk);
            chunksWithAttribution += chunk.attribution === undefined ? 0 : 1;
            segs.push(...newSegs);
        }
        (0, internal_1.assert)(chunksWithAttribution === 0 ||
            chunksWithAttribution === headerMetadata.orderedChunkMetadata.length, 0x4c0 /* all or no chunks should have attribution information */);
        (0, internal_1.assert)(lengthSofar === headerMetadata.totalLength, 0x063 /* "Mismatch in totalLength" */);
        (0, internal_1.assert)(chunk1.segmentCount + segs.length === headerMetadata.totalSegmentCount, 0x064 /* "Mismatch in totalSegmentCount" */);
        // Helper to insert segments at the end of the MergeTree.
        const mergeTree = this.mergeTree;
        const append = (segments, cli, seq) => {
            mergeTree.insertSegments(mergeTree.root.cachedLength ?? 0, segments, 
            /* refSeq: */ constants_js_1.UniversalSequenceNumber, cli, seq, undefined);
        };
        // Helpers to batch-insert segments that are below the min seq
        const batch = [];
        const flushBatch = () => {
            if (batch.length > 0) {
                append(batch, constants_js_1.NonCollabClient, constants_js_1.UniversalSequenceNumber);
            }
        };
        for (const seg of segs) {
            const { clientId, seq } = seg;
            // If the segment can be batch inserted, add it to the 'batch' array.  Otherwise, flush
            // any batched segments and then insert the current segment individually.
            if (clientId === constants_js_1.NonCollabClient && seq === constants_js_1.UniversalSequenceNumber) {
                batch.push(seg);
            }
            else {
                flushBatch();
                append([seg], clientId, seq);
            }
        }
        flushBatch();
    }
    extractAttribution(segments, chunk) {
        if (chunk.attribution) {
            const { attributionPolicy } = this.mergeTree;
            if (attributionPolicy === undefined) {
                throw new internal_2.UsageError("Attribution policy must be provided when loading a document with attribution information.");
            }
            const { isAttached, attach, serializer } = attributionPolicy;
            if (!isAttached) {
                attach(this.client);
            }
            serializer.populateAttributionCollections(segments, chunk.attribution);
        }
        else {
            const { attributionPolicy } = this.mergeTree;
            if (attributionPolicy?.isAttached) {
                attributionPolicy?.detach();
            }
        }
    }
    /**
     * If loading from a snapshot, get the catchup messages.
     * @param rawMessages - The messages in original encoding
     * @returns The decoded messages with parsed+hydrated handles.  Matches the format that will be passed in
     * SharedObject.processCore.
     */
    async loadCatchupOps(rawMessages, serializer) {
        return serializer.parse((0, client_utils_1.bufferToString)(await rawMessages, "utf8"));
    }
}
exports.SnapshotLoader = SnapshotLoader;
//# sourceMappingURL=snapshotLoader.js.map