"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.getPossibleTypes = exports.mapTreeFromNodeData = exports.cursorFromFieldData = exports.cursorFromNodeData = void 0;
const internal_1 = require("@fluidframework/core-utils/internal");
const internal_2 = require("@fluidframework/telemetry-utils/internal");
const index_js_1 = require("../core/index.js");
const index_js_2 = require("../feature-libraries/index.js");
const index_js_3 = require("../util/index.js");
const leafNodeSchema_js_1 = require("./leafNodeSchema.js");
const schemaTypes_js_1 = require("./schemaTypes.js");
const index_js_4 = require("../feature-libraries/index.js");
const proxyBinding_js_1 = require("./proxyBinding.js");
function cursorFromNodeData(data, allowedTypes, context, schemaValidationPolicy) {
    if (data === undefined) {
        return undefined;
    }
    const mappedContent = nodeDataToMapTree(data, (0, schemaTypes_js_1.normalizeAllowedTypes)(allowedTypes), schemaValidationPolicy);
    addDefaultsToMapTree(mappedContent, allowedTypes, context);
    return (0, index_js_2.cursorForMapTreeNode)(mappedContent);
}
exports.cursorFromNodeData = cursorFromNodeData;
function cursorFromFieldData(data, schema, context, schemaValidationPolicy) {
    if (data === undefined) {
        return undefined;
    }
    // TODO: array node content should not go through here since sequence fields don't exist at this abstraction layer.
    const mappedContent = Array.isArray(data)
        ? arrayToMapTreeFields(data, schema.allowedTypeSet, schemaValidationPolicy)
        : [nodeDataToMapTree(data, schema.allowedTypeSet, schemaValidationPolicy)];
    for (const content of mappedContent) {
        addDefaultsToMapTree(content, schema.allowedTypes, context);
    }
    return (0, index_js_2.cursorForMapTreeField)(mappedContent);
}
exports.cursorFromFieldData = cursorFromFieldData;
function mapTreeFromNodeData(data, allowedTypes, context, schemaValidationPolicy) {
    if (data === undefined) {
        return undefined;
    }
    const mapTree = nodeDataToMapTree(data, (0, schemaTypes_js_1.normalizeAllowedTypes)(allowedTypes), schemaValidationPolicy);
    addDefaultsToMapTree(mapTree, allowedTypes, context);
    return mapTree;
}
exports.mapTreeFromNodeData = mapTreeFromNodeData;
function nodeDataToMapTree(data, allowedTypes, schemaValidationPolicy) {
    if (data === undefined) {
        return undefined;
    }
    // A special cache path for processing unhydrated nodes.
    // They already have the mapTree, so there is no need to recompute it.
    const flexNode = (0, proxyBinding_js_1.tryGetFlexNode)(data);
    if (flexNode !== undefined) {
        if ((0, index_js_2.isMapTreeNode)(flexNode)) {
            return flexNode.mapTree;
        }
        else {
            // The node is already hydrated, meaning that it already got inserted into the tree previously
            throw new internal_2.UsageError("A node may not be inserted into the tree more than once");
        }
    }
    const schema = getType(data, allowedTypes);
    let result;
    switch (schema.kind) {
        case schemaTypes_js_1.NodeKind.Leaf:
            result = leafToMapTree(data, schema, allowedTypes);
            break;
        case schemaTypes_js_1.NodeKind.Array:
            result = arrayToMapTree(data, schema);
            break;
        case schemaTypes_js_1.NodeKind.Map:
            result = mapToMapTree(data, schema);
            break;
        case schemaTypes_js_1.NodeKind.Object:
            result = objectToMapTree(data, schema);
            break;
        default:
            (0, index_js_3.fail)(`Unrecognized schema kind: ${schema.kind}.`);
    }
    if (schemaValidationPolicy?.policy.validateSchema === true) {
        const maybeError = (0, index_js_4.isNodeInSchema)(result, schemaValidationPolicy);
        if (maybeError !== 0 /* SchemaValidationErrors.NoError */) {
            throw new internal_2.UsageError("Tree does not conform to schema.");
        }
    }
    return result;
}
/**
 * Transforms data under a Leaf schema.
 * @param data - The tree data to be transformed. Must be a {@link TreeValue}.
 * @param schema - The schema associated with the value.
 * @param allowedTypes - The allowed types specified by the parent.
 * Used to determine which fallback values may be appropriate.
 */
function leafToMapTree(data, schema, allowedTypes) {
    (0, internal_1.assert)(schema.kind === schemaTypes_js_1.NodeKind.Leaf, 0x921 /* Expected a leaf schema. */);
    if (!(0, index_js_2.isTreeValue)(data)) {
        // This rule exists to protect against useless `toString` output like `[object Object]`.
        // In this case, that's actually reasonable behavior, since object input is not compatible with Leaf schemas.
        // eslint-disable-next-line @typescript-eslint/no-base-to-string
        throw new internal_2.UsageError(`Input data is incompatible with leaf schema: ${data}`);
    }
    const mappedValue = mapValueWithFallbacks(data, allowedTypes);
    const mappedSchema = getType(mappedValue, allowedTypes);
    (0, internal_1.assert)(allowsValue(mappedSchema, mappedValue), 0x84a /* Unsupported schema for provided primitive. */);
    return {
        value: mappedValue,
        type: (0, index_js_3.brand)(mappedSchema.identifier),
        fields: new Map(),
    };
}
/**
 * Checks an incoming value to ensure it is compatible with our serialization format.
 * For unsupported values with a schema-compatible replacement, return the replacement value.
 * For unsupported values without a schema-compatible replacement, throw.
 * For supported values, return the input.
 */
function mapValueWithFallbacks(value, allowedTypes) {
    switch (typeof value) {
        case "number": {
            if (Object.is(value, -0)) {
                // Our serialized data format does not support -0.
                // Map such input to +0.
                return 0;
            }
            else if (Number.isNaN(value) || !Number.isFinite(value)) {
                // Our serialized data format does not support NaN nor +/-∞.
                // If the schema supports `null`, fall back to that. Otherwise, throw.
                // This is intended to match JSON's behavior for such values.
                if (allowedTypes.has(leafNodeSchema_js_1.nullSchema)) {
                    return null;
                }
                else {
                    throw new TypeError(`Received unsupported numeric value: ${value}.`);
                }
            }
            else {
                return value;
            }
        }
        default:
            return value;
    }
}
/**
 * Transforms data under an Array schema.
 * @param data - The tree data to be transformed.
 * @param allowedTypes - The set of types allowed by the parent context. Used to validate the input tree.
 * @param schemaValidationPolicy - The stored schema and policy to be used for validation, if the policy says schema
 * validation should happen. If it does, the input tree will be validated against this schema + policy, and an error will
 * be thrown if the tree does not conform to the schema. If undefined, no validation against the stored schema is done.
 */
function arrayToMapTreeFields(data, allowedTypes, schemaValidationPolicy) {
    const mappedData = [];
    for (const child of data) {
        // We do not support undefined sequence entries.
        // If we encounter an undefined entry, use null instead if supported by the schema, otherwise throw.
        let childWithFallback = child;
        if (child === undefined) {
            if (allowedTypes.has(leafNodeSchema_js_1.nullSchema)) {
                childWithFallback = null;
            }
            else {
                throw new TypeError(`Received unsupported array entry value: ${child}.`);
            }
        }
        const mappedChild = nodeDataToMapTree(childWithFallback, allowedTypes, schemaValidationPolicy);
        mappedData.push(mappedChild);
    }
    return mappedData;
}
/**
 * Transforms data under an Array schema.
 * @param data - The tree data to be transformed. Must be an array.
 * @param schema - The schema associated with the value.
 * @param schemaValidationPolicy - The stored schema and policy to be used for validation, if the policy says schema
 * validation should happen. If it does, the input tree will be validated against this schema + policy, and an error will
 * be thrown if the tree does not conform to the schema. If undefined, no validation against the stored schema is done.
 */
function arrayToMapTree(data, schema) {
    (0, internal_1.assert)(schema.kind === schemaTypes_js_1.NodeKind.Array, 0x922 /* Expected an array schema. */);
    if (!(0, index_js_3.isReadonlyArray)(data)) {
        throw new internal_2.UsageError(`Input data is incompatible with Array schema: ${data}`);
    }
    const allowedChildTypes = (0, schemaTypes_js_1.normalizeAllowedTypes)(schema.info);
    const mappedData = arrayToMapTreeFields(data, allowedChildTypes, undefined);
    // Array node children are represented as a single field entry denoted with `EmptyKey`
    const fieldsEntries = mappedData.length === 0 ? [] : [[index_js_1.EmptyKey, mappedData]];
    const fields = new Map(fieldsEntries);
    return {
        type: (0, index_js_3.brand)(schema.identifier),
        fields,
    };
}
/**
 * Transforms data under a Map schema.
 * @param data - The tree data to be transformed. Must be a TypeScript Map.
 * @param schema - The schema associated with the value.
 * @param schemaValidationPolicy - The stored schema and policy to be used for validation, if the policy says schema
 * validation should happen. If it does, the input tree will be validated against this schema + policy, and an error will
 * be thrown if the tree does not conform to the schema. If undefined, no validation against the stored schema is done.
 */
function mapToMapTree(data, schema) {
    (0, internal_1.assert)(schema.kind === schemaTypes_js_1.NodeKind.Map, 0x923 /* Expected a Map schema. */);
    if (!(data instanceof Map)) {
        throw new internal_2.UsageError(`Input data is incompatible with Map schema: ${data}`);
    }
    const allowedChildTypes = (0, schemaTypes_js_1.normalizeAllowedTypes)(schema.info);
    const transformedFields = new Map();
    for (const [key, value] of data) {
        (0, internal_1.assert)(!transformedFields.has((0, index_js_3.brand)(key)), 0x84c /* Keys should not be duplicated */);
        // Omit undefined values - an entry with an undefined value is equivalent to one that has been removed or omitted
        if (value !== undefined) {
            const mappedField = nodeDataToMapTree(value, allowedChildTypes, undefined);
            transformedFields.set((0, index_js_3.brand)(key), [mappedField]);
        }
    }
    return {
        type: (0, index_js_3.brand)(schema.identifier),
        fields: transformedFields,
    };
}
/**
 * Transforms data under an Object schema.
 * @param data - The tree data to be transformed. Must be a Record-like object.
 * @param schema - The schema associated with the value.
 */
function objectToMapTree(data, schema) {
    (0, internal_1.assert)(schema.kind === schemaTypes_js_1.NodeKind.Object, 0x924 /* Expected an Object schema. */);
    if (typeof data !== "object" || data === null) {
        throw new internal_2.UsageError(`Input data is incompatible with Object schema: ${data}`);
    }
    const fields = new Map();
    // Loop through field keys without data, and assign value from its default provider.
    for (const key of Object.keys(schema.info)) {
        const value = data[key];
        if (value !== undefined && Object.hasOwnProperty.call(data, key)) {
            setFieldValue(fields, value, getObjectFieldSchema(schema, key), key);
        }
    }
    return {
        type: (0, index_js_3.brand)(schema.identifier),
        fields,
    };
}
function setFieldValue(fields, fieldValue, fieldSchema, key) {
    if (fieldValue !== undefined) {
        const mappedChildTree = nodeDataToMapTree(fieldValue, fieldSchema.allowedTypeSet, undefined);
        const flexKey = (0, index_js_3.brand)((0, schemaTypes_js_1.getStoredKey)(key, fieldSchema));
        (0, internal_1.assert)(!fields.has(flexKey), 0x956 /* Keys must not be duplicated */);
        fields.set(flexKey, [mappedChildTree]);
    }
}
function getObjectFieldSchema(schema, key) {
    (0, internal_1.assert)(schema.kind === schemaTypes_js_1.NodeKind.Object, 0x926 /* Expected an Object schema. */);
    const fields = schema.info;
    if (fields[key] === undefined) {
        (0, index_js_3.fail)(`Field "${key}" not found in schema "${schema.identifier}".`);
    }
    else {
        return (0, schemaTypes_js_1.normalizeFieldSchema)(fields[key]);
    }
}
function getType(data, allowedTypes) {
    const possibleTypes = getPossibleTypes(allowedTypes, data);
    if (possibleTypes.length === 0) {
        throw new internal_2.UsageError(`The provided data is incompatible with all of the types allowed by the schema. The set of allowed types is: ${JSON.stringify([...allowedTypes].map((schema) => schema.identifier))}.`);
    }
    (0, internal_1.assert)(possibleTypes.length !== 0, 0x84e /* data is incompatible with all types allowed by the schema */);
    checkInput(possibleTypes.length === 1, () => `The provided data is compatible with more than one type allowed by the schema.
The set of possible types is ${JSON.stringify([
        ...possibleTypes.map((schema) => schema.identifier),
    ])}.
Explicitly construct an unhydrated node of the desired type to disambiguate.
For class-based schema, this can be done by replacing an expression like "{foo: 1}" with "new MySchema({foo: 1})".`);
    return possibleTypes[0];
}
/**
 * An invalid tree has been provided, presumably by the user of this package.
 * Throw and an error that properly preserves the message (unlike asserts which will get hard to read short codes intended for package internal logic errors).
 */
function invalidInput(message) {
    throw new internal_2.UsageError(message);
}
function checkInput(condition, message) {
    if (!condition) {
        invalidInput(typeof message === "string" ? message : message());
    }
}
/**
 * @returns all types for which the data is schema-compatible.
 */
function getPossibleTypes(allowedTypes, data) {
    const possibleTypes = [];
    for (const schema of allowedTypes) {
        if (shallowCompatibilityTest(schema, data)) {
            possibleTypes.push(schema);
        }
    }
    return possibleTypes;
}
exports.getPossibleTypes = getPossibleTypes;
/**
 * Checks if data might be schema-compatible.
 *
 * @returns false if `data` is incompatible with `type` based on a cheap/shallow check.
 *
 * Note that this may return true for cases where data is incompatible, but it must not return false in cases where the data is compatible.
 */
function shallowCompatibilityTest(schema, data) {
    (0, internal_1.assert)(data !== undefined, 0x889 /* undefined cannot be used as contextually typed data. Use ContextuallyTypedFieldData. */);
    if ((0, index_js_2.isTreeValue)(data)) {
        return allowsValue(schema, data);
    }
    if (schema.kind === schemaTypes_js_1.NodeKind.Leaf) {
        return false;
    }
    if (index_js_2.typeNameSymbol in data) {
        return data[index_js_2.typeNameSymbol] === schema.identifier;
    }
    if ((0, index_js_3.isReadonlyArray)(data)) {
        return schema.kind === schemaTypes_js_1.NodeKind.Array;
    }
    if (schema.kind === schemaTypes_js_1.NodeKind.Array) {
        return false;
    }
    if (data instanceof Map) {
        return schema.kind === schemaTypes_js_1.NodeKind.Map;
    }
    if (schema.kind === schemaTypes_js_1.NodeKind.Map) {
        return false;
    }
    // Assume record-like object
    if (schema.kind !== schemaTypes_js_1.NodeKind.Object) {
        return false;
    }
    const fields = schema.info;
    // TODO: Improve type inference by making this logic more thorough. Handle at least:
    // * Types which are strict subsets of other types in the same polymorphic union
    // * Types which have the same keys but different types for those keys in the polymorphic union
    // * Types which have the same required fields but different optional fields and enough of those optional fields are populated to disambiguate
    // TODO#7441: Consider allowing data to be inserted which has keys that are extraneous/unknown to the schema (those keys are ignored)
    // If the schema has a required key which is not present in the input object, reject it.
    for (const [fieldKey, fieldSchema] of Object.entries(fields)) {
        const normalizedFieldSchema = (0, schemaTypes_js_1.normalizeFieldSchema)(fieldSchema);
        if (data[fieldKey] === undefined && normalizedFieldSchema.kind === schemaTypes_js_1.FieldKind.Required) {
            return false;
        }
    }
    return true;
}
function allowsValue(schema, value) {
    if (schema.kind === schemaTypes_js_1.NodeKind.Leaf) {
        return (0, index_js_2.valueSchemaAllows)(schema.info, value);
    }
    return false;
}
/**
 * Walk the given {@link MapTree} and provide any field defaults for fields that are missing in the tree but present in the schema.
 * @param mapTree - The tree to populate with defaults
 * @param allowedTypes - Some {@link TreeNodeSchema}, at least one of which the input tree must conform to
 * @param context - An optional context for generating defaults.
 * If present, all applicable defaults will be provided.
 * If absent, only defaults produced by a {@link ConstantFieldProvider} will be provided, and defaults produced by a {@link ContextualFieldProvider} will be ignored.
 * @remarks This function mutates the input tree by adding new fields to the field maps where applicable.
 * @privateRemarks TODO: Create a more established type for mutable MapTrees, and use where appropriate.
 */
function addDefaultsToMapTree(mapTree, allowedTypes, context) {
    const schema = (0, index_js_3.find)((0, schemaTypes_js_1.normalizeAllowedTypes)(allowedTypes), (s) => s.identifier === mapTree.type) ??
        (0, index_js_3.fail)("MapTree is incompatible with schema");
    switch (schema.kind) {
        case schemaTypes_js_1.NodeKind.Array:
        case schemaTypes_js_1.NodeKind.Map:
            {
                for (const field of mapTree.fields.values()) {
                    for (const child of field) {
                        addDefaultsToMapTree(child, schema.info, context);
                    }
                }
            }
            break;
        case schemaTypes_js_1.NodeKind.Object:
            {
                for (const [key, fieldSchema] of Object.entries(schema.info)) {
                    const field = mapTree.fields.get(key);
                    if (field !== undefined) {
                        for (const child of field) {
                            addDefaultsToMapTree(child, getObjectFieldSchema(schema, key).allowedTypes, context);
                        }
                    }
                    else if (fieldSchema instanceof schemaTypes_js_1.FieldSchema) {
                        const defaultProvider = fieldSchema.props?.defaultProvider;
                        if (defaultProvider !== undefined) {
                            const fieldProvider = (0, schemaTypes_js_1.extractFieldProvider)(defaultProvider);
                            const data = provideDefault(fieldProvider, context);
                            if (data !== undefined) {
                                const mutableMapTree = mapTree;
                                setFieldValue(mutableMapTree.fields, data, fieldSchema, key);
                                for (const child of mutableMapTree.fields.get(key) ??
                                    (0, index_js_3.fail)("Expected field to be populated")) {
                                    addDefaultsToMapTree(child, fieldSchema.allowedTypes, context);
                                }
                            }
                        }
                    }
                }
            }
            break;
        default:
            (0, internal_1.assert)(schema.kind === schemaTypes_js_1.NodeKind.Leaf, 0x989 /* Unrecognized schema kind */);
            break;
    }
}
function provideDefault(fieldProvider, context) {
    if (context !== undefined) {
        return fieldProvider(context);
    }
    else {
        if ((0, schemaTypes_js_1.isConstant)(fieldProvider)) {
            return fieldProvider();
        }
    }
}
//# sourceMappingURL=toMapTree.js.map