/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { assert, unreachableCase, oob } from "@fluidframework/core-utils/internal";
import { DiscriminatedUnionDispatcher } from "../../../codec/index.js";
import { assertValidIndex, brand } from "../../../util/index.js";
import { BasicChunk } from "../basicChunk.js";
import { emptyChunk } from "../emptyChunk.js";
import { SequenceChunk } from "../sequenceChunk.js";
import { getChecked, readStream, readStreamBoolean, readStreamNumber, readStreamStream, readStreamValue, } from "./chunkCodecUtilities.js";
import { DecoderContext, decode as genericDecode, readStreamIdentifier, } from "./chunkDecodingGeneric.js";
import { FieldBatchFormatVersion, SpecialField, } from "./format.js";
/**
 * Decode `chunk` into a TreeChunk.
 */
export function decode(chunk, idDecodingContext, incrementalDecoder) {
    return genericDecode(decoderLibrary, new DecoderContext(chunk.identifiers, chunk.shapes, idDecodingContext, incrementalDecoder), chunk, anyDecoder);
}
const decoderLibrary = new DiscriminatedUnionDispatcher({
    a(shape, context) {
        return new NestedArrayDecoder(shape);
    },
    b(shape, context) {
        return new InlineArrayDecoder(shape);
    },
    c(shape, context) {
        return new NodeDecoder(shape, context);
    },
    d(shape) {
        return anyDecoder;
    },
    e(shape, cache) {
        return new IncrementalChunkDecoder(cache);
    },
});
/**
 * Decode a node's value from `stream` using its shape.
 */
export function readValue(stream, shape, idDecodingContext) {
    if (shape === undefined) {
        return readStreamBoolean(stream) ? readStreamValue(stream) : undefined;
    }
    else {
        if (shape === true) {
            return readStreamValue(stream);
        }
        else if (shape === false) {
            return undefined;
        }
        else if (Array.isArray(shape)) {
            assert(shape.length === 1, 0x734 /* expected a single constant for value */);
            return shape[0];
        }
        else if (shape === SpecialField.Identifier) {
            // This case is a special case handling the decoding of identifier fields.
            const streamValue = readStream(stream);
            assert(typeof streamValue === "number" || typeof streamValue === "string", 0x997 /* identifier must be string or number. */);
            const idCompressor = idDecodingContext.idCompressor;
            return typeof streamValue === "number"
                ? idCompressor.decompress(idCompressor.normalizeToSessionSpace(streamValue, idDecodingContext.originatorId))
                : streamValue;
        }
        else {
            // EncodedCounter case:
            unreachableCase(shape, "decoding values as deltas is not yet supported");
        }
    }
}
/**
 * Normalize a {@link TreeChunk} into an array.
 *
 * Unwraps {@link SequenceChunk}s, and wraps other chunks.
 */
export function deaggregateChunks(chunk) {
    if (chunk === 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) {
        // 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.
        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.
        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.
            assert(!(sub instanceof SequenceChunk), 0x737 /* unexpected nested sequence */);
            assert(sub !== emptyChunk, 0x738 /* unexpected empty chunk */);
            sub.referenceAdded();
        }
        chunk.referenceRemoved();
        return chunk.subChunks;
    }
    else {
        return [chunk];
    }
}
/**
 * Normalize a {@link TreeChunk}[] into a single TreeChunk.
 *
 * Avoids creating nested or less than 2 child {@link SequenceChunk}s.
 */
export function aggregateChunks(input) {
    const chunks = input.flatMap(deaggregateChunks);
    switch (chunks.length) {
        case 0:
            return emptyChunk;
        case 1:
            return chunks[0] ?? oob();
        default:
            return new SequenceChunk(chunks);
    }
}
/**
 * Decoder for {@link EncodedNestedArrayShape}s.
 */
export class NestedArrayDecoder {
    constructor(shape) {
        this.shape = shape;
    }
    decode(decoders, stream) {
        const decoder = decoders[this.shape] ?? oob();
        // TODO: uniform chunk fast path
        const chunks = [];
        const data = 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 {
            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);
    }
}
/**
 * Decoder for {@link EncodedInlineArrayShape}s.
 */
export class InlineArrayDecoder {
    constructor(shape) {
        this.shape = shape;
    }
    decode(decoders, stream) {
        const length = this.shape.length;
        const decoder = decoders[this.shape.shape] ?? oob();
        const chunks = [];
        for (let index = 0; index < length; index++) {
            chunks.push(decoder.decode(decoders, stream));
        }
        return aggregateChunks(chunks);
    }
}
/**
 * Decoder for {@link EncodedIncrementalChunkShape}s.
 */
export class IncrementalChunkDecoder {
    constructor(context) {
        this.context = context;
    }
    decode(_, stream) {
        assert(this.context.incrementalDecoder !== undefined, 0xc27 /* incremental decoder not available for incremental field decoding */);
        const chunkDecoder = (batch) => {
            assert(batch.version >= FieldBatchFormatVersion.v2, 0xc9f /* Unsupported FieldBatchFormatVersion for incremental chunks; must be v2 or higher */);
            const context = new DecoderContext(batch.identifiers, batch.shapes, this.context.idDecodingContext, this.context.incrementalDecoder);
            const chunks = genericDecode(decoderLibrary, context, batch, anyDecoder);
            return aggregateChunks(chunks);
        };
        const chunkReferenceId = readStreamNumber(stream);
        return this.context.incrementalDecoder.decodeIncrementalChunk(brand(chunkReferenceId), chunkDecoder);
    }
}
/**
 * Decoder for {@link EncodedAnyShape}s.
 */
export const anyDecoder = {
    decode(decoders, stream) {
        const shapeIndex = readStreamNumber(stream);
        const decoder = getChecked(decoders, shapeIndex);
        return decoder.decode(decoders, stream);
    },
};
/**
 * Get a decoder for fields of a provided (via `shape` and `context`) {@link EncodedChunkShape}.
 */
function fieldDecoder(context, key, shape) {
    assertValidIndex(shape, context.shapes);
    return (decoders, stream) => {
        const decoder = decoders[shape] ?? oob();
        return [key, decoder.decode(decoders, stream)];
    };
}
/**
 * Decoder for {@link EncodedNodeShape}s.
 */
export class NodeDecoder {
    constructor(shape, context) {
        this.shape = shape;
        this.context = context;
        this.type = shape.type === undefined ? undefined : context.identifier(shape.type);
        const fieldDecoders = [];
        for (const [fieldKey, fieldShape] of shape.fields ?? []) {
            const key = context.identifier(fieldKey);
            fieldDecoders.push(fieldDecoder(context, key, fieldShape));
        }
        this.fieldDecoders = fieldDecoders;
    }
    decode(decoders, stream) {
        const type = this.type ?? readStreamIdentifier(stream, this.context);
        // TODO: Consider typechecking against stored schema in here somewhere.
        const value = readValue(stream, this.shape.value, this.context.idDecodingContext);
        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 decoder of this.fieldDecoders) {
            const [key, content] = decoder(decoders, stream);
            addField(key, content);
        }
        if (this.shape.extraFields !== undefined) {
            const decoder = decoders[this.shape.extraFields] ?? oob();
            const inner = readStreamStream(stream);
            while (inner.offset !== inner.data.length) {
                const key = readStreamIdentifier(inner, this.context);
                addField(key, decoder.decode(decoders, inner));
            }
        }
        return new BasicChunk(type, fields, value);
    }
}
//# sourceMappingURL=chunkDecoding.js.map