"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 common_utils_1 = require("@fluidframework/common-utils");
const telemetry_utils_1 = require("@fluidframework/telemetry-utils");
const container_definitions_1 = require("@fluidframework/container-definitions");
const constants_1 = require("./constants");
const snapshotChunks_1 = require("./snapshotChunks");
const snapshotV1_1 = require("./snapshotV1");
const snapshotlegacy_1 = require("./snapshotlegacy");
const attributionCollection_1 = require("./attributionCollection");
class SnapshotLoader {
    constructor(runtime, client, mergeTree, logger, serializer) {
        this.runtime = runtime;
        this.client = client;
        this.mergeTree = mergeTree;
        this.serializer = serializer;
        this.specToSegment = (spec) => {
            var _a;
            let seg;
            if ((0, snapshotChunks_1.hasMergeInfo)(spec)) {
                seg = this.client.specToSegment(spec.json);
                // `specToSegment()` initializes `seg` with the LocalClientId.  Overwrite this with
                // the `spec` client (if specified).  Otherwise overwrite with `NonCollabClient`.
                seg.clientId = spec.client !== undefined
                    ? this.client.getOrAddShortClientId(spec.client)
                    : constants_1.NonCollabClient;
                seg.seq = spec.seq !== undefined
                    ? spec.seq
                    : constants_1.UniversalSequenceNumber;
                if (spec.removedSeq !== undefined) {
                    seg.removedSeq = spec.removedSeq;
                }
                // 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
                if (spec.removedClient !== undefined) {
                    seg.removedClientIds = [this.client.getOrAddShortClientId(spec.removedClient)];
                }
                if (spec.removedClientIds !== undefined) {
                    seg.removedClientIds = (_a = spec.removedClientIds) === null || _a === void 0 ? void 0 : _a.map((sid) => this.client.getOrAddShortClientId(sid));
                }
            }
            else {
                seg = this.client.specToSegment(spec);
                seg.seq = constants_1.UniversalSequenceNumber;
                // `specToSegment()` initializes `seg` with the LocalClientId.  We must overwrite this with
                // `NonCollabClient`.
                seg.clientId = constants_1.NonCollabClient;
            }
            return seg;
        };
        this.logger = telemetry_utils_1.ChildLogger.create(logger, "SnapshotLoader");
    }
    async initialize(services) {
        const headerLoadedP = services.readBlob(snapshotlegacy_1.SnapshotLegacy.header).then((header) => {
            (0, common_utils_1.assert)(!!header, 0x05f /* "Missing blob header on legacy snapshot!" */);
            return this.loadHeader((0, common_utils_1.bufferToString)(header, "utf8"));
        });
        const catchupOpsP = this.loadBodyAndCatchupOps(headerLoadedP, services);
        catchupOpsP.catch((err) => this.logger.sendErrorEvent({ eventName: "CatchupOpsLoadFailure" }, err));
        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) {
            headerChunk.headerMetadata.orderedChunkMetadata.forEach((md) => blobs.splice(blobs.indexOf(md.id), 1));
            (0, common_utils_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]));
        }
        else if (blobs.length !== headerChunk.headerMetadata.orderedChunkMetadata.length) {
            throw new Error("Unexpected blobs in snapshot");
        }
        return [];
    }
    loadHeader(header) {
        var _a;
        const chunk = snapshotV1_1.SnapshotV1.processChunk(snapshotlegacy_1.SnapshotLegacy.header, header, this.logger, this.mergeTree.options, this.serializer);
        const segs = chunk.segments.map(this.specToSegment);
        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((_a = this.runtime.clientId) !== null && _a !== void 0 ? _a : "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 !== undefined
                ? chunk.headerMetadata.minSequenceNumber
                : chunk.headerMetadata.sequenceNumber, 
            /* currentSeq: */ chunk.headerMetadata.sequenceNumber);
        }
        return chunk;
    }
    async loadBody(chunk1, services) {
        const headerMetadata = chunk1.headerMetadata;
        (0, common_utils_1.assert)(chunk1.length <= headerMetadata.totalLength, 0x061 /* "Mismatch in totalLength" */);
        (0, common_utils_1.assert)(chunk1.segmentCount <= headerMetadata.totalSegmentCount, 0x062 /* "Mismatch in totalSegmentCount" */);
        if (chunk1.segmentCount === headerMetadata.totalSegmentCount) {
            return;
        }
        let chunksWithAttribution = chunk1.attribution !== undefined ? 1 : 0;
        const segs = [];
        let lengthSofar = chunk1.length;
        for (let chunkIndex = 1; chunkIndex < headerMetadata.orderedChunkMetadata.length; chunkIndex++) {
            const chunk = await snapshotV1_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(this.specToSegment);
            this.extractAttribution(newSegs, chunk);
            chunksWithAttribution += chunk.attribution !== undefined ? 1 : 0;
            segs.push(...newSegs);
        }
        (0, common_utils_1.assert)(chunksWithAttribution === 0 || chunksWithAttribution === headerMetadata.orderedChunkMetadata.length, 0x4c0 /* all or no chunks should have attribution information */);
        (0, common_utils_1.assert)(lengthSofar === headerMetadata.totalLength, 0x063 /* "Mismatch in totalLength" */);
        (0, common_utils_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, segments, 
            /* refSeq: */ constants_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_1.NonCollabClient, constants_1.UniversalSequenceNumber);
            }
        };
        for (const seg of segs) {
            const cli = seg.clientId;
            const seq = seg.seq;
            // 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 (cli === constants_1.NonCollabClient && seq === constants_1.UniversalSequenceNumber) {
                batch.push(seg);
            }
            else {
                flushBatch();
                append([seg], cli, seq);
            }
        }
        flushBatch();
    }
    extractAttribution(segments, chunk) {
        var _a, _b;
        var _c, _d;
        (_a = (_c = this.mergeTree).options) !== null && _a !== void 0 ? _a : (_c.options = {});
        (_b = (_d = this.mergeTree.options).attribution) !== null && _b !== void 0 ? _b : (_d.attribution = {});
        if (chunk.attribution) {
            this.mergeTree.options.attribution.track = true;
            attributionCollection_1.AttributionCollection.populateAttributionCollections(segments, chunk.attribution);
        }
        else {
            this.mergeTree.options.attribution.track = false;
        }
    }
    /**
     * If loading from a snapshot, get the catchup messages.
     * @param rawMessages - The messages in original encoding
     * @returns The decoded messages, but handles aren't parsed.  Matches the format that will be passed in
     * SharedObject.processCore.
     */
    async loadCatchupOps(rawMessages) {
        return JSON.parse((0, common_utils_1.bufferToString)(await rawMessages, "utf8"));
    }
}
exports.SnapshotLoader = SnapshotLoader;
//# sourceMappingURL=snapshotLoader.js.map