/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { assert, debugAssert } from "@fluidframework/core-utils/internal";
import { getConfigForMinVersionForCollabIterable, lowestMinVersionForCollab, } from "@fluidframework/runtime-utils/internal";
import { UsageError } from "@fluidframework/telemetry-utils/internal";
import { gt } from "semver-ts";
import { pkgVersion } from "../../packageVersion.js";
import { JsonCompatibleReadOnlySchema, } from "../../util/index.js";
import { withSchemaValidation, } from "../codec.js";
import { Versioned } from "./format.js";
/**
 * Validate that the version is one of the supported values.
 * @remarks
 * If supportedVersions contains undefined, data with no version field is also accepted despite the return type indicating otherwise.
 * This is for legacy compatibility where older data may not have a version field.
 */
function makeVersionedCodec(supportedVersions, { jsonValidator: validator }, inner) {
    const codec = {
        encode: (data, context) => {
            const encoded = inner.encode(data, context);
            assert(supportedVersions.has(encoded.version), 0x88b /* version being encoded should be supported */);
            return encoded;
        },
        decode: (data, context) => {
            const versioned = data; // Validated by withSchemaValidation
            if (!supportedVersions.has(versioned.version)) {
                throw new UsageError(`Unsupported version ${versioned.version} encountered while decoding data. Supported versions for this data are: ${[...supportedVersions].join(", ")}.
The client which encoded this data likely specified an "minVersionForCollab" value which corresponds to a version newer than the version of this client ("${pkgVersion}").`);
            }
            const decoded = inner.decode(data, context);
            return decoded;
        },
    };
    // If undefined is a supported version, skip using withSchemaValidation to enforce there is a version field.
    // Codec will still assert the content of the field is in supportedVersions, so it is still somewhat validated, just in a different way.
    if (supportedVersions.has(undefined)) {
        return codec;
    }
    return withSchemaValidation(Versioned, codec, validator);
}
/**
 * Wrap a codec with version checking and schema validation.
 * @remarks
 * The passed in codec should not perform its own schema validation.
 * The schema validation gets added here.
 */
function makeVersionedValidatedCodec(options, supportedVersions, schema, codec) {
    return {
        ...makeVersionedCodec(supportedVersions, options, withSchemaValidation(schema, codec, options.jsonValidator)),
        schema,
    };
}
/**
 * Creates a codec version which always throws a UsageError when encoding or decoding, indicating that the format version is discontinued.
 */
export function makeDiscontinuedCodecAndSchema(discontinuedVersion, discontinuedSince) {
    return {
        minVersionForCollab: undefined,
        formatVersion: discontinuedVersion,
        codec: {
            schema: JsonCompatibleReadOnlySchema,
            encode: (_data) => {
                throw new UsageError(`Cannot encode data to format ${discontinuedVersion}. The codec was discontinued in Fluid Framework client version ${discontinuedSince}.`);
            },
            decode: (data) => {
                throw new UsageError(`Cannot decode data in format ${discontinuedVersion}. The codec was discontinued in Fluid Framework client version ${discontinuedSince}.`);
            },
        },
    };
}
/**
 * Normalize the codec to a single format.
 * @remarks
 * Bakes in schema validation, so output no longer exposes the schema.
 */
function normalizeCodecVersion(codecVersion) {
    const codecBuilder = typeof codecVersion.codec === "function"
        ? codecVersion.codec
        : () => codecVersion.codec;
    const codec = (options) => {
        const built = codecBuilder(options);
        return makeVersionedValidatedCodec(options, new Set([codecVersion.formatVersion]), built.schema, built);
    };
    return {
        minVersionForCollab: codecVersion.minVersionForCollab,
        formatVersion: codecVersion.formatVersion,
        codec,
    };
}
/**
 * Creates a codec which dispatches to the appropriate member of a codec family based on the `minVersionForCollab` for encode and the
 * version number in data it encounters for decode.
 * @privateRemarks
 * This is a two stage builder so the first stage can encapsulate all codec specific details and the second can bring in configuration.
 */
export class ClientVersionDispatchingCodecBuilder {
    /**
     * Use {@link ClientVersionDispatchingCodecBuilder.build} to create an instance of this class.
     * @remarks
     * Inputs to this are assumed to be constants in the code controlled by the developers of this package,
     * and constructed at least once during tests.
     * Because of this, the validation of these inputs done with debugAssert should be sufficient,
     * and using debugAssert avoids bloating the bundle size for production users.
     */
    constructor(
    /**
     * See {@link CodecName}.
     */
    name, 
    /**
     * The registry of codecs which this builder can use to encode and decode data.
     */
    inputRegistry) {
        this.name = name;
        const normalizedRegistry = [];
        const formats = new Set();
        const versions = new Set();
        for (const codec of inputRegistry) {
            debugAssert(() => !formats.has(codec.formatVersion) ||
                `duplicate codec format ${name} ${codec.formatVersion}`);
            debugAssert(() => codec.minVersionForCollab === undefined ||
                typeof codec.formatVersion !== "string" ||
                `unstable format ${JSON.stringify(codec.formatVersion)} (string formats) must not have a minVersionForCollab in ${name}`);
            formats.add(codec.formatVersion);
            const normalizedCodec = normalizeCodecVersion(codec);
            normalizedRegistry.push(normalizedCodec);
            if (codec.minVersionForCollab !== undefined) {
                debugAssert(() => !versions.has(codec.minVersionForCollab) ||
                    `Codec ${name} has multiple entries for version ${JSON.stringify(codec.minVersionForCollab)}`);
                versions.add(codec.minVersionForCollab);
            }
        }
        debugAssert(() => versions.has(lowestMinVersionForCollab) ||
            `Codec ${name} is missing entry for lowestMinVersionForCollab`);
        this.registry = normalizedRegistry;
    }
    /**
     * Applies the provided options to the codec registry to produce a list of evaluated codecs.
     * @remarks
     * This is used by build, which is what production code should use.
     * This is only exposed for testing purposes.
     */
    applyOptions(options) {
        return this.registry.map((codec) => ({
            minVersionForCollab: codec.minVersionForCollab,
            formatVersion: codec.formatVersion,
            codec: codec.codec(options),
        }));
    }
    /**
     * Produce a single codec which can read any supported format, and writes a version selected based on the provided options.
     */
    build(options) {
        const [applied, decoder] = this.buildDecoderInternal(options);
        const writeVersion = getWriteVersion(this.name, options, applied);
        return {
            ...decoder,
            encode: (data, context) => {
                return writeVersion.codec.encode(data, context);
            },
            writeVersion: writeVersion.formatVersion,
        };
    }
    buildDecoderInternal(options) {
        const applied = this.applyOptions(options);
        const fromFormatVersion = new Map(applied.map((codec) => [codec.formatVersion, codec]));
        return [
            applied,
            {
                decode: (data, context) => {
                    const versioned = data;
                    const codec = fromFormatVersion.get(versioned.version);
                    if (codec === undefined) {
                        throw new UsageError(`Unsupported version ${versioned.version} encountered while decoding ${this.name} data. Supported versions for this data are: ${versionList(applied)}.
The client which encoded this data likely specified an "minVersionForCollab" value which corresponds to a version newer than the version of this client ("${pkgVersion}").`);
                    }
                    return codec.codec.decode(data, context);
                },
            },
        ];
    }
    /**
     * Produce a single codec which can read any supported format.
     */
    buildDecoder(options) {
        return this.buildDecoderInternal(options)[1];
    }
    getCodecTree(clientVersion) {
        // TODO: add support for children codecs.
        const selected = getWriteVersionNoOverrides(this.registry, clientVersion);
        return {
            name: this.name,
            version: selected.formatVersion,
        };
    }
    /**
     * Builds a ClientVersionDispatchingCodecBuilder from the provided registry.
     * @remarks
     * This static method infers the types of the builder from the provided registry,
     * making it easier to create builders without needing to explicitly specify all type parameters.
     * This gets better type inference than the constructor.
     */
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
    static build(name, inputRegistry) {
        const input = inputRegistry;
        const builder = new ClientVersionDispatchingCodecBuilder(name, input);
        return builder;
    }
}
/**
 * Selects which format should be used when writing data.
 * @remarks
 * This either uses the override specified in the options, or selects the newest format compatible with the provided minVersionForCollab.
 */
function getWriteVersion(name, options, versions) {
    if (options.writeVersionOverrides?.has(name) === true) {
        const selectedFormatVersion = options.writeVersionOverrides.get(name);
        const selected = versions.find((codec) => codec.formatVersion === selectedFormatVersion);
        if (selected === undefined) {
            throw new UsageError(`Codec "${name}" does not support requested format version ${JSON.stringify(selectedFormatVersion)}. Supported versions are: ${versionList(versions)}.`);
        }
        else if (options.allowPossiblyIncompatibleWriteVersionOverrides !== true) {
            const selectedMinVersionForCollab = selected.minVersionForCollab;
            if (selectedMinVersionForCollab === undefined) {
                throw new UsageError(`Codec "${name}" does not support requested format version ${JSON.stringify(selectedFormatVersion)} because it has minVersionForCollab undefined. Use "allowPossiblyIncompatibleWriteVersionOverrides" to suppress this error if appropriate.`);
            }
            else if (gt(selectedMinVersionForCollab, options.minVersionForCollab)) {
                throw new UsageError(`Codec "${name}" does not support requested format version ${JSON.stringify(selectedFormatVersion)} because it is only compatible back to client version ${selectedMinVersionForCollab} and the requested oldest compatible client was ${options.minVersionForCollab}. Use "allowPossiblyIncompatibleWriteVersionOverrides" to suppress this error if appropriate.`);
            }
        }
        return selected;
    }
    return getWriteVersionNoOverrides(versions, options.minVersionForCollab);
}
/**
 * Selects which format should be used when writing data, without consider overrides.
 */
function getWriteVersionNoOverrides(versions, minVersionForCollab) {
    const stableVersions = [];
    for (const version of versions) {
        if (version.minVersionForCollab !== undefined) {
            stableVersions.push([version.minVersionForCollab, version]);
        }
    }
    const result = getConfigForMinVersionForCollabIterable(minVersionForCollab, stableVersions);
    return result;
}
/**
 * Formats a list of versions for use in UsageErrors.
 */
function versionList(versions) {
    return JSON.stringify(Array.from(versions, (codec) => codec.formatVersion));
}
//# sourceMappingURL=codec.js.map