"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.NodeDecoder = exports.anyDecoder = exports.IncrementalChunkDecoder = exports.InlineArrayDecoder = exports.NestedArrayDecoder = exports.aggregateChunks = exports.deaggregateChunks = exports.readValue = exports.SpecializedNodeDecoder = exports.applySpecialization = exports.normalizeToNodeShape = exports.decode = void 0;
const internal_1 = require("@fluidframework/core-utils/internal");
const internal_2 = require("@fluidframework/id-compressor/internal");
const uuid_1 = require("uuid");
const index_js_1 = require("../../../codec/index.js");
const index_js_2 = require("../../../util/index.js");
const basicChunk_js_1 = require("../basicChunk.js");
const emptyChunk_js_1 = require("../emptyChunk.js");
const sequenceChunk_js_1 = require("../sequenceChunk.js");
const chunkCodecUtilities_js_1 = require("./chunkCodecUtilities.js");
const chunkDecodingGeneric_js_1 = require("./chunkDecodingGeneric.js");
const index_js_3 = require("./format/index.js");
/**
 * Random v4 UUID generated as a namespace for the "heal an unresolvable identifier into a stable UUID"
 * path in {@link readValue}. This scheme requires consensus across all clients to function.
 */
const healingNamespace = "f8a89df3-6882-400f-b913-4c1f6f0157bd";
/**
 * Decode `chunk` into a TreeChunk.
 */
function decode(chunk, idDecodingContext, incrementalDecoder) {
    return (0, chunkDecodingGeneric_js_1.decode)(decoderLibrary, new chunkDecodingGeneric_js_1.DecoderContext(chunk.identifiers, chunk.shapes, idDecodingContext, incrementalDecoder), chunk, exports.anyDecoder);
}
exports.decode = decode;
/**
 * Resolves `shapeIndex` to a fully-resolved {@link EncodedNodeShape}, normalizing away any
 * specialized node shapes (`f`) along the way by applying their overlays via
 * {@link applySpecialization} until a concrete node shape is reached.
 *
 * @param input - The index of the shape to resolve, which must be a concrete or specialized node shape.
 * @param context - The decoding context containing the shape definitions.
 * @param pendingResolution - (Internal) A set of shape indices visited so far in the current resolution chain, used to detect cycles in the specialization chain. Most callers should not provide this argument.
 *
 * @remarks
 * Exported for testing.
 */
function normalizeToNodeShape(input, context, pendingResolution = new Set()) {
    if (!("base" in input)) {
        return input;
    }
    const baseIndex = input.base;
    (0, internal_1.assert)(!pendingResolution.has(baseIndex), 0xcfb /* cyclic specialized node shape chain */);
    pendingResolution.add(baseIndex);
    const encoded = context.shapes[baseIndex];
    (0, internal_1.assert)(encoded !== undefined, 0xcfc /* shape index out of bounds */);
    const baseShape = encoded.c ?? ("f" in encoded ? encoded.f : undefined);
    (0, internal_1.assert)(baseShape !== undefined, 0xcfd /* shape at index must be a concrete (c) or specialized (f) node shape */);
    return applySpecialization(normalizeToNodeShape(baseShape, context, pendingResolution), input, context);
}
exports.normalizeToNodeShape = normalizeToNodeShape;
/**
 * Produces a specialized {@link EncodedNodeShape} by overlaying `overrides` onto `base`.
 *
 * See {@link EncodedSpecializedNodeShape} for the override/inherit/clear semantics.
 *
 * @remarks
 * Exported for testing.
 */
function applySpecialization(base, overrides, context) {
    const fields = [...(base.fields ?? [])];
    const indexFromKey = new Map();
    for (const [i, [keyEncoded]] of fields.entries()) {
        const key = context.identifier(keyEncoded);
        (0, internal_1.assert)(!indexFromKey.has(key), 0xcfe /* duplicate field key in base node shape */);
        indexFromKey.set(key, i);
    }
    // Replace fields in base with overrides, append new keys in overrides in the order they are specified.
    const seenOverrideKeys = new Set();
    for (const [keyEncoded, shapeIndex] of overrides.fields ?? []) {
        const key = context.identifier(keyEncoded);
        (0, internal_1.assert)(!seenOverrideKeys.has(key), 0xcff /* duplicate field key in specialized node shape */);
        seenOverrideKeys.add(key);
        const existingIndex = indexFromKey.get(key);
        if (existingIndex === undefined) {
            fields.push([keyEncoded, shapeIndex]);
        }
        else {
            const index = fields[existingIndex];
            (0, internal_1.assert)(index !== undefined, 0xd00 /* expected existing field index */);
            fields[existingIndex] = [index[0], shapeIndex];
        }
    }
    return {
        type: base.type,
        value: resolveOverride(overrides.value, base.value),
        fields: fields.length > 0 ? fields : undefined,
        extraFields: resolveOverride(overrides.extraFields, base.extraFields),
    };
}
exports.applySpecialization = applySpecialization;
/**
 * Resolves an override against a base value.
 *
 * @param override - `undefined` means the override is absent (inherit from base); `null` is the
 * explicit-clear sentinel needed because JSON.stringify drops `undefined`-valued properties, making
 * property-presence indistinguishable from absent on the wire.
 * @param baseValue - The value to inherit when the override is absent.
 */
function resolveOverride(
// eslint-disable-next-line @rushstack/no-new-null
override, baseValue) {
    if (override === undefined) {
        return baseValue;
    }
    if (override === null) {
        return undefined;
    }
    return override;
}
/**
 * Decoder for {@link EncodedSpecializedNodeShape}s.
 * Applies the specialization's field overrides to the resolved base node shape, then delegates
 * to a {@link NodeDecoder} built from the resulting shape.
 */
class SpecializedNodeDecoder {
    constructor(shape, context) {
        this.inner = new NodeDecoder(normalizeToNodeShape(shape, context), context);
    }
    decode(decoders, stream) {
        return this.inner.decode(decoders, stream);
    }
}
exports.SpecializedNodeDecoder = SpecializedNodeDecoder;
const decoderLibrary = new index_js_1.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 exports.anyDecoder;
    },
    e(shape, context) {
        return new IncrementalChunkDecoder(context);
    },
    f(shape, context) {
        return new SpecializedNodeDecoder(shape, context);
    },
});
/**
 * Decode a node's value from `stream` using its shape.
 */
function readValue(stream, shape, idDecodingContext) {
    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, internal_1.assert)(shape.length === 1, 0x734 /* expected a single constant for value */);
            return shape[0];
        }
        else if (shape === index_js_3.SpecialField.Identifier) {
            // This case is a special case handling the decoding of identifier fields.
            const streamValue = (0, chunkCodecUtilities_js_1.readStream)(stream);
            (0, internal_1.assert)(typeof streamValue === "number" || typeof streamValue === "string", 0x997 /* identifier must be string or number. */);
            if (typeof streamValue === "string") {
                return streamValue;
            }
            const idCompressor = idDecodingContext.idCompressor;
            // OpSpaceCompressedIds are negative, and require a session-id to compute their value.
            // Due to a bug, we have some special casing for them (see below).
            if (idDecodingContext.isSummary === true &&
                !(0, internal_2.isFinalId)(streamValue)) {
                if (idDecodingContext.healUnresolvableIdentifiersOnDecode === true &&
                    idDecodingContext.sharedObjectId !== undefined) {
                    // Documents written before the encode-side fix for non-finalized identifier
                    // values can persist negative op-space IDs that are no
                    // longer resolvable once the originating session's local state has been stripped.
                    // When loading such a summary with the heal-on-decode option on, synthesize a deterministic
                    // stable UUID so all readers of the same blob agree on the resulting value.
                    //
                    // The heal path is intentionally restricted to summary loads — an
                    // unresolvable ID encountered while applying an op should still surface as
                    // an error, since it indicates a real bug rather than a recoverable state.
                    return (0, uuid_1.v5)(`${idDecodingContext.sharedObjectId}|${streamValue}`, healingNamespace);
                }
                // See `SharedTreeOptionsBeta.healUnresolvableIdentifiersOnDecode` for details on this error.
                throw new Error("Summary could not be loaded due incorrectly encoded identifier. See SharedTreeOptionsBeta.healUnresolvableIdentifiersOnDecode for mitigation.");
            }
            return idCompressor.decompress(idCompressor.normalizeToSessionSpace(streamValue, idDecodingContext.originatorId));
        }
        else {
            // EncodedCounter case:
            (0, internal_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, internal_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, internal_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, internal_1.assert)(!(sub instanceof sequenceChunk_js_1.SequenceChunk), 0x737 /* unexpected nested sequence */);
            (0, internal_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] ?? (0, internal_1.oob)();
        }
        default: {
            return new sequenceChunk_js_1.SequenceChunk(chunks);
        }
    }
}
exports.aggregateChunks = aggregateChunks;
/**
 * Decoder for {@link EncodedNestedArrayShape}s.
 */
class NestedArrayDecoder {
    constructor(shape) {
        this.shape = shape;
    }
    decode(decoders, stream) {
        const decoder = decoders[this.shape] ?? (0, internal_1.oob)();
        // 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, internal_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 EncodedInlineArrayShape}s.
 */
class InlineArrayDecoder {
    constructor(shape) {
        this.shape = shape;
    }
    decode(decoders, stream) {
        const length = this.shape.length;
        const decoder = decoders[this.shape.shape] ?? (0, internal_1.oob)();
        const chunks = [];
        for (let index = 0; index < length; index++) {
            chunks.push(decoder.decode(decoders, stream));
        }
        return aggregateChunks(chunks);
    }
}
exports.InlineArrayDecoder = InlineArrayDecoder;
/**
 * Decoder for {@link EncodedIncrementalChunkShape}s.
 */
class IncrementalChunkDecoder {
    constructor(context) {
        this.context = context;
    }
    decode(_, stream) {
        (0, internal_1.assert)(this.context.incrementalDecoder !== undefined, 0xc27 /* incremental decoder not available for incremental field decoding */);
        const chunkDecoder = (batch) => {
            (0, internal_1.assert)((0, index_js_3.supportsIncrementalEncoding)(batch.version), 0xc9f /* Unsupported FieldBatchFormatVersion for incremental chunks; must be v2 or higher */);
            const context = new chunkDecodingGeneric_js_1.DecoderContext(batch.identifiers, batch.shapes, this.context.idDecodingContext, this.context.incrementalDecoder);
            const chunks = (0, chunkDecodingGeneric_js_1.decode)(decoderLibrary, context, batch, exports.anyDecoder);
            return aggregateChunks(chunks);
        };
        const chunkReferenceId = (0, chunkCodecUtilities_js_1.readStreamNumber)(stream);
        return this.context.incrementalDecoder.decodeIncrementalChunk((0, index_js_2.brand)(chunkReferenceId), chunkDecoder);
    }
}
exports.IncrementalChunkDecoder = IncrementalChunkDecoder;
/**
 * 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 `context`).
 */
function fieldDecoder(context, key, shape) {
    (0, index_js_2.assertValidIndex)(shape, context.shapes);
    return (decoders, stream) => {
        const decoder = decoders[shape] ?? (0, internal_1.oob)();
        return [key, decoder.decode(decoders, stream)];
    };
}
/**
 * Decoder for {@link EncodedNodeShape}s.
 */
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 ?? (0, chunkDecodingGeneric_js_1.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] ?? (0, internal_1.oob)();
            const inner = (0, chunkCodecUtilities_js_1.readStreamStream)(stream);
            while (inner.offset !== inner.data.length) {
                const key = (0, chunkDecodingGeneric_js_1.readStreamIdentifier)(inner, this.context);
                addField(key, decoder.decode(decoders, inner));
            }
        }
        return new basicChunk_js_1.BasicChunk(type, fields, value);
    }
}
exports.NodeDecoder = NodeDecoder;
//# sourceMappingURL=chunkDecoding.js.map