/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { unreachableCase, fail } from "@fluidframework/core-utils/internal";
import { UsageError } from "@fluidframework/telemetry-utils/internal";
import { EmptyKey, LeafNodeStoredSchema, MapNodeStoredSchema, ObjectNodeStoredSchema, } from "../core/index.js";
import { FieldKinds } from "../feature-libraries/index.js";
import { brand, getOrCreate } from "../util/index.js";
import { allowedTypeFilter, convertAllowedTypes, getTreeNodeSchemaPrivateData, isClassBasedSchema, NodeKind, } from "./core/index.js";
import { FieldKind, FieldSchemaAlpha, normalizeFieldSchema, } from "./fieldSchema.js";
import { walkFieldSchema } from "./walkFieldSchema.js";
const viewToStoredCache = new WeakMap();
export const restrictiveStoredSchemaGenerationOptions = {
    includeStaged: () => false,
};
export const permissiveStoredSchemaGenerationOptions = {
    includeStaged: () => true,
};
/**
 * Converts a {@link ImplicitAnnotatedFieldSchema} into a {@link TreeStoredSchema} for use in schema upgrades.
 *
 * TODO: once upgrades are more flexible, this should take in more options, including the old schema and specific upgrades to enable.
 */
export function toUpgradeSchema(root) {
    return toStoredSchema(root, restrictiveStoredSchemaGenerationOptions);
}
/**
 * Converts a {@link ImplicitAnnotatedFieldSchema} into a {@link TreeStoredSchema} for use as initial document schema.
 */
export function toInitialSchema(root) {
    return toStoredSchema(root, restrictiveStoredSchemaGenerationOptions);
}
/**
 * Converts a {@link ImplicitAnnotatedFieldSchema} into a {@link TreeStoredSchema} to used for unhydrated nodes.
 * @remarks
 * This allows as much as possible, relying on further validation when inserting the content.
 *
 * TODO: this should get additional options to enable support for unknown optional fields.
 */
export const toUnhydratedSchema = permissiveStoredSchemaGenerationOptions;
/**
 * Converts a {@link ImplicitAnnotatedFieldSchema} into a {@link TreeStoredSchema}.
 *
 * @privateRemarks
 * TODO:#38722 When runtime schema upgrades are implemented, this will need to be updated to check if
 * a staged allowed type has been upgraded and if so, include it in the conversion.
 *
 * @throws
 * Throws a `UsageError` if multiple schemas are encountered with the same identifier.
 */
export function toStoredSchema(root, options) {
    const cache = getOrCreate(viewToStoredCache, options, () => new WeakMap());
    return getOrCreate(cache, root, () => {
        const normalized = normalizeFieldSchema(root);
        const nodeSchema = new Map();
        walkFieldSchema(normalized, {
            node(schema) {
                if (nodeSchema.has(brand(schema.identifier))) {
                    // Use JSON.stringify to quote and escape identifier string.
                    throw new UsageError(`Multiple schema encountered with the identifier ${JSON.stringify(schema.identifier)}. Remove or rename them to avoid the collision.`);
                }
                nodeSchema.set(brand(schema.identifier), getStoredSchema(schema, options));
            },
            allowedTypeFilter: (allowedType) => allowedTypeFilter(allowedType, options),
        });
        const result = {
            nodeSchema,
            rootFieldSchema: convertField(normalized, options),
        };
        return result;
    });
}
/**
 * Normalizes an {@link ImplicitFieldSchema} into a {@link TreeFieldSchema}.
 */
export function convertField(schema, options) {
    const kind = convertFieldKind.get(schema.kind)?.identifier ?? fail(0xae3 /* Invalid field kind */);
    let types;
    // eslint-disable-next-line unicorn/prefer-ternary
    if (schema instanceof FieldSchemaAlpha) {
        types = convertAllowedTypes(schema.annotatedAllowedTypesNormalized, options);
    }
    else {
        types = schema.allowedTypesIdentifiers;
    }
    return { kind, types, persistedMetadata: schema.persistedMetadata };
}
/**
 * A map that converts {@link FieldKind} to {@link FlexFieldKind}.
 */
export const convertFieldKind = new Map([
    [FieldKind.Optional, FieldKinds.optional],
    [FieldKind.Required, FieldKinds.required],
    [FieldKind.Identifier, FieldKinds.identifier],
]);
/**
 * Converts a {@link TreeNodeSchema} into a {@link TreeNodeStoredSchema}.
 * @privateRemarks
 * TODO: Persist node metadata once schema FormatV2 is supported.
 *
 * TODO: AB#43548: Using a stored schema for unhydrated flex trees does not handle schema evolution features like "allowUnknownOptionalFields".
 * Usage of this and the conversion which wrap it should be audited and reduced.
 */
export function getStoredSchema(schema, options) {
    if (isClassBasedSchema(schema)) {
        return getTreeNodeSchemaPrivateData(schema).toStored(options);
    }
    const kind = schema.kind;
    switch (kind) {
        case NodeKind.Leaf: {
            return new LeafNodeStoredSchema(schema.leafKind);
        }
        case NodeKind.Map:
        case NodeKind.Record: {
            const types = schema.allowedTypesIdentifiers;
            return new MapNodeStoredSchema({
                kind: FieldKinds.optional.identifier,
                types,
                persistedMetadata: schema.persistedMetadata,
            }, 
            // TODO: Find a way to avoid injecting persistedMetadata twice in these constructor calls.
            schema.persistedMetadata);
        }
        case NodeKind.Array: {
            const types = schema.allowedTypesIdentifiers;
            return arrayNodeStoredSchema(types, schema.persistedMetadata);
        }
        case NodeKind.Object: {
            const fields = new Map();
            for (const fieldSchema of schema.fields.values()) {
                fields.set(brand(fieldSchema.storedKey), convertField(fieldSchema, options));
            }
            return new ObjectNodeStoredSchema(fields, schema.persistedMetadata);
        }
        default: {
            unreachableCase(kind);
        }
    }
}
export function arrayNodeStoredSchema(types, persistedMetadata) {
    const field = {
        kind: FieldKinds.sequence.identifier,
        types,
        persistedMetadata,
    };
    const fields = new Map([[EmptyKey, field]]);
    return new ObjectNodeStoredSchema(fields, persistedMetadata);
}
//# sourceMappingURL=toStoredSchema.js.map