"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.TreeDecoder = exports.anyDecoder = exports.InlineArrayDecoder = exports.NestedArrayDecoder = exports.aggregateChunks = exports.deaggregateChunks = exports.readValue = exports.decode = void 0;
const core_utils_1 = require("@fluidframework/core-utils");
const index_js_1 = require("../../../util/index.js");
const index_js_2 = require("../../../codec/index.js");
const basicChunk_js_1 = require("../basicChunk.js");
const sequenceChunk_js_1 = require("../sequenceChunk.js");
const emptyChunk_js_1 = require("../emptyChunk.js");
const chunkCodecUtilities_js_1 = require("./chunkCodecUtilities.js");
const chunkDecodingGeneric_js_1 = require("./chunkDecodingGeneric.js");
/**
 * Decode `chunk` into a TreeChunk.
 */
function decode(chunk) {
    return (0, chunkDecodingGeneric_js_1.decode)(decoderLibrary, new chunkDecodingGeneric_js_1.DecoderContext(chunk.identifiers, chunk.shapes), chunk, exports.anyDecoder);
}
exports.decode = decode;
const decoderLibrary = new index_js_2.DiscriminatedUnionDispatcher({
    a(shape, cache) {
        return new NestedArrayDecoder(shape);
    },
    b(shape, cache) {
        return new InlineArrayDecoder(shape);
    },
    c(shape, cache) {
        return new TreeDecoder(shape, cache);
    },
    d(shape) {
        return exports.anyDecoder;
    },
});
/**
 * Decode a node's value from `stream` using its shape.
 */
function readValue(stream, shape) {
    if (shape === undefined) {
        return (0, chunkCodecUtilities_js_1.readStreamBoolean)(stream) ? (0, chunkCodecUtilities_js_1.readStreamValue)(stream) : undefined;
    }
    else {
        if (shape === true) {
            return (0, chunkCodecUtilities_js_1.readStreamValue)(stream);
        }
        else if (shape === false) {
            return undefined;
        }
        else if (Array.isArray(shape)) {
            (0, core_utils_1.assert)(shape.length === 1, 0x734 /* expected a single constant for value */);
            return shape[0];
        }
        else {
            // EncodedCounter case:
            (0, core_utils_1.unreachableCase)(shape, "decoding values as deltas is not yet supported");
        }
    }
}
exports.readValue = readValue;
/**
 * Normalize a {@link TreeChunk} into an array.
 *
 * Unwraps {@link SequenceChunk}s, and wraps other chunks.
 */
function deaggregateChunks(chunk) {
    if (chunk === emptyChunk_js_1.emptyChunk) {
        return [];
    }
    // TODO: when handling of SequenceChunks has better performance (for example in cursors),
    // consider keeping SequenceChunks here if they are longer than some threshold.
    if (chunk instanceof sequenceChunk_js_1.SequenceChunk) {
        // Could return [] here, however the logic in this file is designed to never produce an empty SequenceChunk, so its better to throw an error here to detect bugs.
        (0, core_utils_1.assert)(chunk.subChunks.length > 0, 0x735 /* Unexpected empty sequence */);
        // Logic in this file is designed to never produce an unneeded (single item) SequenceChunks, so its better to throw an error here to detect bugs.
        (0, core_utils_1.assert)(chunk.subChunks.length > 1, 0x736 /* Unexpected single item sequence */);
        for (const sub of chunk.subChunks) {
            // The logic in this file is designed to never produce an nested SequenceChunks or emptyChunk, so its better to throw an error here to detect bugs.
            (0, core_utils_1.assert)(!(sub instanceof sequenceChunk_js_1.SequenceChunk), 0x737 /* unexpected nested sequence */);
            (0, core_utils_1.assert)(sub !== emptyChunk_js_1.emptyChunk, 0x738 /* unexpected empty chunk */);
            sub.referenceAdded();
        }
        chunk.referenceRemoved();
        return chunk.subChunks;
    }
    else {
        return [chunk];
    }
}
exports.deaggregateChunks = deaggregateChunks;
/**
 * Normalize a {@link TreeChunk}[] into a single TreeChunk.
 *
 * Avoids creating nested or less than 2 child {@link SequenceChunk}s.
 */
function aggregateChunks(input) {
    const chunks = input.flatMap(deaggregateChunks);
    switch (chunks.length) {
        case 0:
            return emptyChunk_js_1.emptyChunk;
        case 1:
            return chunks[0];
        default:
            return new sequenceChunk_js_1.SequenceChunk(chunks);
    }
}
exports.aggregateChunks = aggregateChunks;
/**
 * Decoder for {@link EncodedNestedArray}s.
 */
class NestedArrayDecoder {
    constructor(shape) {
        this.shape = shape;
    }
    decode(decoders, stream) {
        const decoder = decoders[this.shape];
        // TODO: uniform chunk fast path
        const chunks = [];
        const data = (0, chunkCodecUtilities_js_1.readStream)(stream);
        if (typeof data === "number") {
            // This case means that the array contained only 0-sized items, and was thus encoded as the length of the array.
            const inner = { data: [], offset: 0 };
            for (let index = 0; index < data; index++) {
                chunks.push(decoder.decode(decoders, inner));
            }
        }
        else {
            (0, core_utils_1.assert)(Array.isArray(data), 0x739 /* expected number of array for encoding of nested array */);
            const inner = { data, offset: 0 };
            while (inner.offset !== inner.data.length) {
                chunks.push(decoder.decode(decoders, inner));
            }
        }
        return aggregateChunks(chunks);
    }
}
exports.NestedArrayDecoder = NestedArrayDecoder;
/**
 * Decoder for {@link EncodedInlineArray}s.
 */
class InlineArrayDecoder {
    constructor(shape) {
        this.shape = shape;
    }
    decode(decoders, stream) {
        const length = this.shape.length;
        const decoder = decoders[this.shape.shape];
        const chunks = [];
        for (let index = 0; index < length; index++) {
            chunks.push(decoder.decode(decoders, stream));
        }
        return aggregateChunks(chunks);
    }
}
exports.InlineArrayDecoder = InlineArrayDecoder;
/**
 * Decoder for {@link EncodedAnyShape}s.
 */
exports.anyDecoder = {
    decode(decoders, stream) {
        const shapeIndex = (0, chunkCodecUtilities_js_1.readStreamNumber)(stream);
        const decoder = (0, chunkCodecUtilities_js_1.getChecked)(decoders, shapeIndex);
        return decoder.decode(decoders, stream);
    },
};
/**
 * Get a decoder for fields of a provided (via `shape` and `cache`) {@link EncodedChunkShape}.
 */
function fieldDecoder(cache, key, shape) {
    (0, index_js_1.assertValidIndex)(shape, cache.shapes);
    return (decoders, stream) => [key, decoders[shape].decode(decoders, stream)];
}
/**
 * Decoder for {@link EncodedTreeShape}s.
 */
class TreeDecoder {
    constructor(shape, cache) {
        this.shape = shape;
        this.cache = cache;
        this.type = shape.type === undefined ? undefined : cache.identifier(shape.type);
        const fieldDecoders = [];
        for (const [fieldKey, fieldShape] of shape.fields ?? []) {
            const key = cache.identifier(fieldKey);
            fieldDecoders.push(fieldDecoder(cache, key, fieldShape));
        }
        this.fieldDecoders = fieldDecoders;
    }
    decode(decoders, stream) {
        const type = this.type ?? (0, chunkDecodingGeneric_js_1.readStreamIdentifier)(stream, this.cache);
        // TODO: Consider typechecking against stored schema in here somewhere.
        const value = readValue(stream, this.shape.value);
        const fields = new Map();
        // Helper to add fields, but with unneeded array chunks removed.
        function addField(key, data) {
            // TODO: when handling of ArrayChunks has better performance (for example in cursors),
            // consider keeping array chunks here if they are longer than some threshold.
            const chunks = deaggregateChunks(data);
            if (chunks.length !== 0) {
                fields.set(key, chunks);
            }
        }
        for (const field of this.fieldDecoders) {
            const [key, content] = field(decoders, stream);
            addField(key, content);
        }
        if (this.shape.extraFields !== undefined) {
            const decoder = decoders[this.shape.extraFields];
            const inner = (0, chunkCodecUtilities_js_1.readStreamStream)(stream);
            while (inner.offset !== inner.data.length) {
                const key = (0, chunkDecodingGeneric_js_1.readStreamIdentifier)(inner, this.cache);
                addField(key, decoder.decode(decoders, inner));
            }
        }
        return new basicChunk_js_1.BasicChunk(type, fields, value);
    }
}
exports.TreeDecoder = TreeDecoder;
//# sourceMappingURL=chunkDecoding.js.map