/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { assert } from "@fluidframework/core-utils/internal";
import { UsageError } from "@fluidframework/telemetry-utils/internal";
import { EmptyKey, } from "../core/index.js";
import { cursorForMapTreeField, cursorForMapTreeNode, isFluidHandle, isTreeValue, typeNameSymbol, valueSchemaAllows, } from "../feature-libraries/index.js";
import { brand, fail, isReadonlyArray } from "../util/index.js";
import { nullSchema } from "./leafNodeSchema.js";
import { FieldKind, NodeKind, normalizeAllowedTypes, normalizeFieldSchema, getStoredKey, } from "./schemaTypes.js";
/**
 * 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 allowedTypes - 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`.
 */
export function cursorFromNodeData(data, allowedTypes) {
    if (data === undefined) {
        return undefined;
    }
    const mappedContent = nodeDataToMapTree(data, normalizeAllowedTypes(allowedTypes));
    return cursorForMapTreeNode(mappedContent);
}
/**
 * Transforms an input {@link InsertableContent} tree to an array of {@link MapTree}s, and wraps the tree in a {@link CursorWithNode}.
 * @param data - The input tree to be converted.
 * @param schema - Schema of the field with which the input `data` is associated.
 */
export function cursorFromFieldData(data, schema) {
    const mappedContent = Array.isArray(data)
        ? arrayToMapTreeFields(data, schema.allowedTypeSet)
        : [nodeDataToMapTree(data, schema.allowedTypeSet)];
    return cursorForMapTreeField(mappedContent);
}
/**
 * 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 globalSchema - Schema for the whole tree for interperting `Any`.
 * @param allowedTypes - The set of types allowed by the parent context. Used to validate the input tree.
 */
export function nodeDataToMapTree(data, allowedTypes) {
    assert(data !== undefined, 0x846 /* Cannot map undefined tree. */);
    if (data === null) {
        return valueToMapTree(data, allowedTypes);
    }
    switch (typeof data) {
        case "number":
        case "string":
        case "boolean":
            return valueToMapTree(data, allowedTypes);
        default: {
            if (isFluidHandle(data)) {
                return valueToMapTree(data, allowedTypes);
            }
            else if (Array.isArray(data)) {
                return arrayToMapTree(data, allowedTypes);
            }
            else if (data instanceof Map) {
                return mapToMapTree(data, allowedTypes);
            }
            else {
                // Assume record-like object
                return objectToMapTree(data, allowedTypes);
            }
        }
    }
}
function valueToMapTree(value, allowedTypes) {
    const mappedValue = mapValueWithFallbacks(value, allowedTypes);
    const schema = getType(mappedValue, allowedTypes);
    assert(schema.kind === NodeKind.Leaf && allowsValue(schema, mappedValue), 0x84a /* Unsupported schema for provided primitive. */);
    return {
        value: mappedValue,
        type: brand(schema.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(nullSchema)) {
                    return null;
                }
                else {
                    throw new TypeError(`Received unsupported numeric value: ${value}.`);
                }
            }
            else {
                return value;
            }
        }
        default:
            return value;
    }
}
function arrayToMapTreeFields(data, allowedTypes) {
    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(nullSchema)) {
                childWithFallback = null;
            }
            else {
                throw new TypeError(`Received unsupported array entry value: ${child}.`);
            }
        }
        const mappedChild = nodeDataToMapTree(childWithFallback, allowedTypes);
        mappedData.push(mappedChild);
    }
    return mappedData;
}
function arrayToMapTree(data, allowedTypes) {
    const schema = getType(data, allowedTypes);
    if (schema.kind !== NodeKind.Array) {
        throw new UsageError(`Provided array input is incompatible with schema "${schema.identifier}".`);
    }
    const childSchema = normalizeAllowedTypes(schema.info);
    const mappedData = arrayToMapTreeFields(data, childSchema);
    // Array node children are represented as a single field entry denoted with `EmptyKey`
    const fieldsEntries = mappedData.length === 0 ? [] : [[EmptyKey, mappedData]];
    const fields = new Map(fieldsEntries);
    return {
        type: brand(schema.identifier),
        fields,
    };
}
function mapToMapTree(data, allowedTypes) {
    const schema = getType(data, allowedTypes);
    if (schema.kind !== NodeKind.Map) {
        throw new UsageError(`Provided map input is incompatible with schema "${schema.identifier}".`);
    }
    const childSchema = normalizeAllowedTypes(schema.info);
    const fields = new Map();
    for (const [key, value] of data) {
        assert(!fields.has(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, childSchema);
            fields.set(brand(key), [mappedField]);
        }
    }
    return {
        type: brand(schema.identifier),
        fields,
    };
}
function objectToMapTree(data, allowedTypes) {
    const schema = getType(data, allowedTypes);
    if (schema.kind !== NodeKind.Object) {
        throw new UsageError(`Provided object input is incompatible with schema "${schema.identifier}".`);
    }
    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 viewKey of keys) {
        const fieldValue = data[viewKey];
        // Omit undefined record entries - an entry with an undefined key is equivalent to no entry
        if (fieldValue !== undefined) {
            const fieldSchema = getObjectFieldSchema(schema, viewKey);
            const mappedChildTree = nodeDataToMapTree(fieldValue, fieldSchema.allowedTypeSet);
            const flexKey = brand(getStoredKey(viewKey, fieldSchema));
            // Note: SchemaFactory validates this at schema creation time, with a user-friendly error.
            // So we don't expect to hit this, and if we do it is likely an internal bug.
            assert(!fields.has(flexKey), "Keys must not be duplicated");
            fields.set(flexKey, [mappedChildTree]);
        }
    }
    return {
        type: brand(schema.identifier),
        fields,
    };
}
function getObjectFieldSchema(schema, key) {
    assert(schema.kind === NodeKind.Object, "Expected an object schema.");
    const fields = schema.info;
    if (fields[key] === undefined) {
        fail(`Field "${key}" not found in schema "${schema.identifier}".`);
    }
    else {
        return normalizeFieldSchema(fields[key]);
    }
}
function getType(data, allowedTypes) {
    const possibleTypes = getPossibleTypes(allowedTypes, data);
    if (possibleTypes.length === 0) {
        throw new 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))}.`);
    }
    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 UsageError(message);
}
function checkInput(condition, message) {
    if (!condition) {
        invalidInput(typeof message === "string" ? message : message());
    }
}
/**
 * @returns all types for which the data is schema-compatible.
 */
export function getPossibleTypes(allowedTypes, data) {
    const possibleTypes = [];
    for (const schema of allowedTypes) {
        if (shallowCompatibilityTest(schema, data)) {
            possibleTypes.push(schema);
        }
    }
    return possibleTypes;
}
/**
 * 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) {
    assert(data !== undefined, 0x889 /* undefined cannot be used as contextually typed data. Use ContextuallyTypedFieldData. */);
    if (isTreeValue(data)) {
        return allowsValue(schema, data);
    }
    if (schema.kind === NodeKind.Leaf) {
        return false;
    }
    if (typeNameSymbol in data) {
        return data[typeNameSymbol] === schema.identifier;
    }
    if (isReadonlyArray(data)) {
        return schema.kind === NodeKind.Array;
    }
    if (schema.kind === NodeKind.Array) {
        return false;
    }
    if (data instanceof Map) {
        return schema.kind === NodeKind.Map;
    }
    if (schema.kind === NodeKind.Map) {
        return false;
    }
    // Assume record-like object
    if (schema.kind !== 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 = normalizeFieldSchema(fieldSchema);
        if (data[fieldKey] === undefined && normalizedFieldSchema.kind === FieldKind.Required) {
            return false;
        }
    }
    return true;
}
function allowsValue(schema, value) {
    if (schema.kind === NodeKind.Leaf) {
        return valueSchemaAllows(schema.info, value);
    }
    return false;
}
//# sourceMappingURL=toMapTree.js.map