"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.unhydratedFlexTreeFromInsertable = void 0;
const internal_1 = require("@fluidframework/core-utils/internal");
const internal_2 = require("@fluidframework/telemetry-utils/internal");
const internal_3 = require("@fluidframework/runtime-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("./core/index.js");
// Required to prevent the introduction of new circular dependencies
// TODO: Having the schema provide their own policy functions for compatibility which
// unhydratedFlexTreeFromInsertable invokes instead of manually handling each kind would remove this bad
// dependency, and reduce coupling.
// eslint-disable-next-line import/no-internal-modules
const objectNodeTypes_js_1 = require("./node-kinds/object/objectNodeTypes.js");
// eslint-disable-next-line import/no-internal-modules
const unhydratedFlexTree_js_1 = require("./core/unhydratedFlexTree.js");
const toStoredSchema_js_1 = require("./toStoredSchema.js");
const createContext_js_1 = require("./createContext.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 an {@link UnhydratedFlexTreeNode}.
 * @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`
 *
 * For fields with a default value, the field may be omitted.
 * If `context` is not provided, defaults which require a context will be left empty which can be out of schema.
 *
 * @param allowedTypes - The set of types allowed by the parent context. Used to validate the input tree.
 * @remarks
 * The resulting tree will be populated with any defaults from {@link FieldProvider}s in the schema.
 *
 * Often throws UsageErrors for invalid data, but may miss some cases.
 *
 * Output should comply with the provided view schema, but this is not explicitly validated:
 * validation against stored schema (to guard against document corruption) is done elsewhere.
 */
function unhydratedFlexTreeFromInsertable(data, allowedTypes) {
    const normalizedFieldSchema = (0, schemaTypes_js_1.normalizeFieldSchema)(allowedTypes);
    if (data === undefined) {
        // TODO: this code-path should support defaults
        if (normalizedFieldSchema.kind !== schemaTypes_js_1.FieldKind.Optional) {
            throw new internal_2.UsageError("Got undefined for non-optional field.");
        }
        return undefined;
    }
    const flexTree = unhydratedFlexTreeFromInsertableNode(data, normalizedFieldSchema.allowedTypeSet);
    return flexTree;
}
exports.unhydratedFlexTreeFromInsertable = unhydratedFlexTreeFromInsertable;
/**
 * Copy content from `data` into a UnhydratedFlexTreeNode.
 */
function unhydratedFlexTreeFromInsertableNode(data, allowedTypes) {
    if ((0, index_js_4.isTreeNode)(data)) {
        const kernel = (0, index_js_4.getKernel)(data);
        const inner = kernel.getInnerNodeIfUnhydrated();
        if (inner === undefined) {
            // 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");
        }
        else {
            if (!allowedTypes.has(kernel.schema)) {
                throw new internal_2.UsageError("Invalid schema for this context.");
            }
            return inner;
        }
    }
    const schema = getType(data, allowedTypes);
    let result;
    switch (schema.kind) {
        case index_js_4.NodeKind.Leaf:
            result = leafToFlexContent(data, schema, allowedTypes);
            break;
        case index_js_4.NodeKind.Array:
            result = arrayToFlexContent(data, schema);
            break;
        case index_js_4.NodeKind.Map:
            result = mapToFlexContent(data, schema);
            break;
        case index_js_4.NodeKind.Object:
            result = objectToFlexContent(data, schema);
            break;
        default:
            (0, internal_1.unreachableCase)(schema.kind);
    }
    return new index_js_4.UnhydratedFlexTreeNode(...result, (0, createContext_js_1.getUnhydratedContext)(schema));
}
/**
 * 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 leafToFlexContent(data, schema, allowedTypes) {
    (0, internal_1.assert)(schema.kind === index_js_4.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),
        },
        new Map(),
    ];
}
/**
 * Checks an incoming {@link TreeLeafValue} to ensure it is compatible with its requirements.
 * 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.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 internal_2.UsageError(`Received unsupported numeric value: ${value}.`);
                }
            }
            else {
                return value;
            }
        }
        case "string":
        // TODO:
        // This should detect invalid strings. Something like @stdlib/regexp-utf16-unpaired-surrogate could be used to do this.
        // See SchemaFactory.string for details.
        case "boolean":
            return value;
        case "object": {
            if (value === null || (0, internal_3.isFluidHandle)(value)) {
                return value;
            }
        }
        default:
            throw new internal_2.UsageError(`Received unsupported leaf value: ${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.
 */
function arrayChildToFlexTree(child, allowedTypes) {
    // 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}.`);
        }
    }
    return unhydratedFlexTreeFromInsertableNode(childWithFallback, allowedTypes);
}
/**
 * Transforms data under an Array schema.
 * @param data - The tree data to be transformed. Must be an iterable.
 * @param schema - The schema associated with the value.
 */
function arrayToFlexContent(data, schema) {
    (0, internal_1.assert)(schema.kind === index_js_4.NodeKind.Array, 0x922 /* Expected an array schema. */);
    if (!(typeof data === "object" && data !== null && Symbol.iterator in 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 = Array.from(data, (child) => arrayChildToFlexTree(child, allowedChildTypes));
    const context = (0, createContext_js_1.getUnhydratedContext)(schema).flexContext;
    // Array nodes have a single `EmptyKey` field:
    const fieldsEntries = mappedData.length === 0
        ? []
        : [
            [
                index_js_1.EmptyKey,
                new index_js_4.UnhydratedSequenceField(context, index_js_2.FieldKinds.sequence.identifier, index_js_1.EmptyKey, mappedData),
            ],
        ];
    return [
        {
            type: (0, index_js_3.brand)(schema.identifier),
        },
        new Map(fieldsEntries),
    ];
}
/**
 * Transforms data under a Map schema.
 * @param data - The tree data to be transformed. Must be an iterable.
 * @param schema - The schema associated with the value.
 */
function mapToFlexContent(data, schema) {
    (0, internal_1.assert)(schema.kind === index_js_4.NodeKind.Map, 0x923 /* Expected a Map schema. */);
    if (!(typeof data === "object" && data !== null)) {
        throw new internal_2.UsageError(`Input data is incompatible with Map schema: ${data}`);
    }
    const allowedChildTypes = (0, schemaTypes_js_1.normalizeAllowedTypes)(schema.info);
    const fieldsIterator = (Symbol.iterator in data
        ? // Support iterables of key value pairs (including Map objects)
            data
        : // Support record objects for JSON style Map data
            Object.entries(data));
    const context = (0, createContext_js_1.getUnhydratedContext)(schema).flexContext;
    const transformedFields = new Map();
    for (const item of fieldsIterator) {
        if (!(0, index_js_3.isReadonlyArray)(item) || item.length !== 2 || typeof item[0] !== "string") {
            throw new internal_2.UsageError(`Input data is incompatible with map entry: ${item}`);
        }
        const [key, value] = item;
        (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 child = unhydratedFlexTreeFromInsertableNode(value, allowedChildTypes);
            const field = (0, unhydratedFlexTree_js_1.createField)(context, index_js_2.FieldKinds.optional.identifier, (0, index_js_3.brand)(key), [child]);
            transformedFields.set((0, index_js_3.brand)(key), field);
        }
    }
    return [
        {
            type: (0, index_js_3.brand)(schema.identifier),
        },
        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 objectToFlexContent(data, schema) {
    (0, internal_1.assert)((0, objectNodeTypes_js_1.isObjectNodeSchema)(schema), 0x924 /* Expected an Object schema. */);
    if (typeof data !== "object" ||
        data === null ||
        Symbol.iterator in data ||
        (0, internal_3.isFluidHandle)(data)) {
        throw new internal_2.UsageError(`Input data is incompatible with Object schema: ${data}`);
    }
    const fields = new Map();
    const context = (0, createContext_js_1.getUnhydratedContext)(schema).flexContext;
    for (const [key, fieldInfo] of schema.flexKeyMap) {
        const value = getFieldProperty(data, key);
        let children;
        if (value === undefined) {
            const defaultProvider = fieldInfo.schema.props?.defaultProvider ??
                (0, internal_1.fail)(0xbb1 /* missing field has no default provider */);
            const fieldProvider = (0, schemaTypes_js_1.extractFieldProvider)(defaultProvider);
            children = (0, schemaTypes_js_1.isConstant)(fieldProvider) ? fieldProvider() : fieldProvider;
        }
        else {
            children = [
                unhydratedFlexTreeFromInsertableNode(value, fieldInfo.schema.allowedTypeSet),
            ];
        }
        const kind = toStoredSchema_js_1.convertFieldKind.get(fieldInfo.schema.kind) ?? (0, internal_1.fail)(0xbb2 /* Invalid field kind */);
        fields.set(fieldInfo.storedKey, (0, unhydratedFlexTree_js_1.createField)(context, kind.identifier, fieldInfo.storedKey, children));
    }
    return [{ type: (0, index_js_3.brand)(schema.identifier) }, fields];
}
/**
 * Check {@link FactoryContentObject} for a property which could be store a field.
 *
 * @returns If the property exists, return its value. Otherwise, returns undefined.
 * @remarks
 * The currently policy is to only consider own properties.
 * See {@link InsertableObjectFromSchemaRecord} for where this policy is documented in the public API.
 *
 * Explicit undefined values are treated the same as missing properties to allow explicit use of undefined with defaulted identifiers.
 *
 * @privateRemarks
 * If we ever want to have an optional field which defaults to something other than undefined, this will need changes.
 * It would need to adjusting the handling of explicit undefined in contexts where undefined is allowed, and a default provider also exists.
 */
function getFieldProperty(data, key) {
    // This policy only allows own properties.
    if (Object.hasOwnProperty.call(data, key)) {
        return data[key];
    }
    return undefined;
}
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))}.`);
    }
    if (!(0, index_js_3.hasSingle)(possibleTypes)) {
        throw new internal_2.UsageError(`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];
}
/**
 * Returns all types for which the data is schema-compatible.
 */
function getPossibleTypes(allowedTypes, data) {
    let best = CompatibilityLevel.None;
    const possibleTypes = [];
    for (const schema of allowedTypes) {
        const level = shallowCompatibilityTest(schema, data);
        if (level > best) {
            possibleTypes.length = 0;
            best = level;
        }
        if (best === level) {
            possibleTypes.push(schema);
        }
    }
    return best === CompatibilityLevel.None ? [] : possibleTypes;
}
exports.getPossibleTypes = getPossibleTypes;
/**
 * Indicates a compatibility level for inferring a schema to apply to insertable data.
 * @remarks
 * Only the highest compatibility options are used.
 * This approach allows adding new possible matching at a new lower compatibility level as a non breaking change,
 * since that way they can't make a case that was compatible before ambiguous now.
 */
var CompatibilityLevel;
(function (CompatibilityLevel) {
    /**
     * Not compatible. Constructor typing indicates incompatibility.
     */
    CompatibilityLevel[CompatibilityLevel["None"] = 0] = "None";
    /**
     * Additional compatibility cases added in Fluid Framework 2.2.
     */
    CompatibilityLevel[CompatibilityLevel["Low"] = 1] = "Low";
    /**
     * Compatible in Fluid Framework 2.0.
     */
    CompatibilityLevel[CompatibilityLevel["Normal"] = 2] = "Normal";
})(CompatibilityLevel || (CompatibilityLevel = {}));
/**
 * 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 FactoryContent. */);
    if ((0, index_js_2.isTreeValue)(data)) {
        return allowsValue(schema, data) ? CompatibilityLevel.Normal : CompatibilityLevel.None;
    }
    if (schema.kind === index_js_4.NodeKind.Leaf) {
        return CompatibilityLevel.None;
    }
    // Typing (of schema based constructors and thus implicit node construction)
    // allows iterables for constructing maps and arrays.
    // Some users of this API may have unions of maps and arrays,
    // and rely on Arrays ending up as array nodes and maps as Map nodes,
    // despite both being iterable and thus compatible with both.
    // This uses a priority based system where an array would be parsed as an array when unioned with a map,
    // but if in a map only context, could still be used as a map.
    if (data instanceof Map) {
        switch (schema.kind) {
            case index_js_4.NodeKind.Map:
                return CompatibilityLevel.Normal;
            case index_js_4.NodeKind.Array:
                // Maps are iterable, so type checking does allow constructing an ArrayNode from a map if the array's type is an array that includes the key and value types of the map.
                return CompatibilityLevel.Low;
            default:
                return CompatibilityLevel.None;
        }
    }
    if ((0, index_js_3.isReadonlyArray)(data)) {
        switch (schema.kind) {
            case index_js_4.NodeKind.Array:
                return CompatibilityLevel.Normal;
            case index_js_4.NodeKind.Map:
                // Arrays are iterable, so type checking does allow constructing an array from a MapNode from an if the array's type is key values pairs for the map.
                return CompatibilityLevel.Low;
            default:
                return CompatibilityLevel.None;
        }
    }
    const mapOrArray = schema.kind === index_js_4.NodeKind.Array || schema.kind === index_js_4.NodeKind.Map;
    if (Symbol.iterator in data) {
        return mapOrArray ? CompatibilityLevel.Normal : CompatibilityLevel.None;
    }
    // At this point, it is assumed data is a record-like object since all the other cases have been eliminated.
    if (schema.kind === index_js_4.NodeKind.Array) {
        return CompatibilityLevel.None;
    }
    if (schema.kind === index_js_4.NodeKind.Map) {
        // When not unioned with an ObjectNode, allow objects to be used to create maps.
        return CompatibilityLevel.Low;
    }
    (0, internal_1.assert)((0, objectNodeTypes_js_1.isObjectNodeSchema)(schema), 0x9e6 /* unexpected schema kind */);
    // 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 schema.fields) {
        if (fieldSchema.requiresValue) {
            if (getFieldProperty(data, fieldKey) === undefined) {
                return CompatibilityLevel.None;
            }
        }
    }
    return CompatibilityLevel.Normal;
}
function allowsValue(schema, value) {
    if (schema.kind === index_js_4.NodeKind.Leaf) {
        return (0, index_js_2.valueSchemaAllows)(schema.info, value);
    }
    return false;
}
//# sourceMappingURL=unhydratedFlexTreeFromInsertable.js.map