"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.fieldDataToMapTrees = exports.nodeDataToMapTree = exports.cursorFromFieldData = exports.cursorFromNodeData = void 0;
const core_utils_1 = require("@fluidframework/core-utils");
const telemetry_utils_1 = require("@fluidframework/telemetry-utils");
// Drilling into `domains` to reduce the magnitude of cycles introduced here
// eslint-disable-next-line import/no-internal-modules
const leafDomain_1 = require("../domains/leafDomain");
const feature_libraries_1 = require("../feature-libraries");
const util_1 = require("../util");
/**
 * Module notes:
 *
 * The flow of the below code is in terms of the structure of the input data. We then verify that the associated
 * schema is appropriate for that kind of data. This is fine while we have a 1:1 mapping of kind of input data to
 * the kind of schema we expect for it (e.g. an input that is an array always need to be associated with a sequence in
 * the schema). If/when we begin accepting kinds of input data that are ambiguous (e.g. accepting an input that is an
 * array of key/value tuples to instantiate a map) we may need to rethink the structure here to be based more on the
 * schema than on the input data.
 */
/**
 * Transforms an input {@link TypedNode} tree to a {@link MapTree}, and wraps the tree in a {@link CursorWithNode}.
 * @param data - The input tree to be converted.
 * @param context - Describes the context into which the data is being created. See {@link FlexTreeEntity.context}.
 * @param typeSet - The set of types allowed by the parent context. Used to validate the input tree.
 *
 * @returns A cursor (in nodes mode) for the mapped tree if the input data was defined. Otherwise, returns `undefined`.
 */
function cursorFromNodeData(data, context, typeSet) {
    if (data === undefined) {
        return undefined;
    }
    const mappedContent = nodeDataToMapTree(data, context, typeSet);
    return (0, feature_libraries_1.cursorForMapTreeNode)(mappedContent);
}
exports.cursorFromNodeData = cursorFromNodeData;
/**
 * Transforms an input {@link TreeField} tree to a list of {@link MapTree}s, and wraps the tree in a {@link CursorWithNode}.
 * @param data - The input tree to be converted.
 * @param context - Describes the context into which the data is being created. See {@link FlexTreeEntity.context}.
 */
function cursorFromFieldData(data, context, fieldSchema) {
    const mappedContent = fieldDataToMapTrees(data, context, fieldSchema);
    return (0, feature_libraries_1.cursorForMapTreeField)(mappedContent);
}
exports.cursorFromFieldData = cursorFromFieldData;
/**
 * Transforms an input {@link TypedNode} tree to a {@link MapTree}.
 * @param data - The input tree to be converted.
 * If the data is an unsupported value (e.g. NaN), a fallback value will be used when supported,
 * otherwise an error will be thrown.
 *
 * Fallbacks:
 *
 * * `NaN` =\> `null`
 *
 * * `+/-∞` =\> `null`
 *
 * * `-0` =\> `+0`
 *
 * @param context - Describes the context into which the data is being created. See {@link FlexTreeEntity.context}.
 * @param typeSet - The set of types allowed by the parent context. Used to validate the input tree.
 */
function nodeDataToMapTree(data, context, typeSet) {
    (0, core_utils_1.assert)(data !== undefined, 0x846 /* Cannot map undefined tree. */);
    if (data === null) {
        return valueToMapTree(data, context, typeSet);
    }
    switch (typeof data) {
        case "number":
        case "string":
        case "boolean":
            return valueToMapTree(data, context, typeSet);
        default: {
            if ((0, feature_libraries_1.isFluidHandle)(data)) {
                return valueToMapTree(data, context, typeSet);
            }
            else if (Array.isArray(data)) {
                return arrayToMapTree(data, context, typeSet);
            }
            else if (data instanceof Map) {
                return mapToMapTree(data, context, typeSet);
            }
            else {
                // Assume record-like object
                return recordToMapTree(data, context, typeSet);
            }
        }
    }
}
exports.nodeDataToMapTree = nodeDataToMapTree;
/**
 * Transforms an input {@link TreeField} tree to a list of {@link MapTree}s.
 * @param data - The input tree to be converted.
 * If the input is a sequence containing 1 or more `undefined` values, those values will be mapped as `null` if supported.
 * Othewise, an error will be thrown.
 * @param context - Describes the context into which the data is being created. See {@link FlexTreeEntity.context}.
 */
function fieldDataToMapTrees(data, context, fieldSchema) {
    const multiplicity = (0, feature_libraries_1.getFieldKind)(fieldSchema).multiplicity;
    if (data === undefined) {
        (0, core_utils_1.assert)(multiplicity === feature_libraries_1.Multiplicity.Forbidden || multiplicity === feature_libraries_1.Multiplicity.Optional, 0x847 /* `undefined` provided for a field that does not support `undefined` */);
        return [];
    }
    const typeSet = fieldSchema.types;
    if (multiplicity === feature_libraries_1.Multiplicity.Sequence) {
        (0, core_utils_1.assert)(Array.isArray(data), 0x848 /* Expected an array as sequence input. */);
        const children = Array.from(data, (child) => {
            // 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 (typeSet?.has(leafDomain_1.leaf.null.name) ?? false) {
                    childWithFallback = null;
                }
                else {
                    throw new TypeError(`Received unsupported list entry value: ${child}.`);
                }
            }
            return nodeDataToMapTree(childWithFallback, context, typeSet);
        });
        return children;
    }
    (0, core_utils_1.assert)(multiplicity === feature_libraries_1.Multiplicity.Single || multiplicity === feature_libraries_1.Multiplicity.Optional, 0x849 /* A single value was provided for an unsupported field */);
    return [nodeDataToMapTree(data, context, typeSet)];
}
exports.fieldDataToMapTrees = fieldDataToMapTrees;
function valueToMapTree(
// eslint-disable-next-line @rushstack/no-new-null
value, context, typeSet) {
    const mappedValue = mapValueWithFallbacks(value, typeSet);
    const type = getType(mappedValue, context, typeSet);
    const schema = getSchema(context, type);
    (0, core_utils_1.assert)((0, feature_libraries_1.allowsValue)(schema.leafValue, mappedValue), 0x84a /* Unsupported schema for provided primitive. */);
    return {
        value: mappedValue,
        type,
        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(
// eslint-disable-next-line @rushstack/no-new-null
value, typeSet) {
    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 (typeSet?.has(leafDomain_1.leaf.null.name) ?? false) {
                    return null;
                }
                else {
                    throw new TypeError(`Received unsupported numeric value: ${value}.`);
                }
            }
            else {
                return value;
            }
        }
        default:
            return value;
    }
}
function arrayToMapTree(data, context, typeSet) {
    const type = getType(data, context, typeSet);
    const schema = getSchema(context, type);
    const primaryField = (0, feature_libraries_1.getPrimaryField)(schema);
    (0, core_utils_1.assert)(primaryField !== undefined, 0x84b /* Array data reported comparable with the schema without a primary field. */);
    const mappedChildren = fieldDataToMapTrees(data, context, primaryField.schema);
    const fieldsEntries = mappedChildren.length === 0 ? [] : [[primaryField.key, mappedChildren]];
    // List children are represented as a single field entry denoted with `EmptyKey`
    const fields = new Map(fieldsEntries);
    return {
        type,
        fields,
    };
}
function mapToMapTree(data, context, typeSet) {
    const type = getType(data, context, typeSet);
    const schema = getSchema(context, type);
    const fields = new Map();
    for (const [key, value] of data) {
        (0, core_utils_1.assert)(!fields.has((0, util_1.brand)(key)), 0x84c /* Keys should not be duplicated */);
        // Omit undefined record entries - an entry with an undefined key is equivalent to no entry
        if (value !== undefined) {
            const childSchema = (0, feature_libraries_1.getFieldSchema)((0, util_1.brand)(key), schema);
            const mappedField = fieldDataToMapTrees(value, context, childSchema);
            fields.set((0, util_1.brand)(key), mappedField);
        }
    }
    return {
        type,
        fields,
    };
}
function recordToMapTree(data, context, typeSet) {
    const type = getType(data, context, typeSet);
    const schema = getSchema(context, type);
    const fields = new Map();
    // Filter keys to only those that are strings - our trees do not support symbol or numeric property keys
    const keys = Reflect.ownKeys(data).filter((key) => typeof key === "string");
    for (const key of keys) {
        (0, core_utils_1.assert)(!fields.has(key), 0x84d /* Keys should not be duplicated */);
        const value = data[key];
        // Omit undefined record entries - an entry with an undefined key is equivalent to no entry
        if (value !== undefined) {
            const childSchema = (0, feature_libraries_1.getFieldSchema)((0, util_1.brand)(key), schema);
            const mappedChildTree = fieldDataToMapTrees(value, context, childSchema);
            fields.set((0, util_1.brand)(key), mappedChildTree);
        }
    }
    return {
        type,
        fields,
    };
}
function getType(data, context, typeSet) {
    const possibleTypes = (0, feature_libraries_1.getPossibleTypes)(context, typeSet, data);
    (0, core_utils_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], undefined)}.
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];
}
function getSchema(context, type) {
    return context.schema.nodeSchema.get(type) ?? (0, util_1.fail)("Requested type does not exist in schema.");
}
/**
 * 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 telemetry_utils_1.UsageError(message);
}
function checkInput(condition, message) {
    if (!condition) {
        invalidInput(typeof message === "string" ? message : message());
    }
}
//# sourceMappingURL=toMapTree.js.map