"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.normalizeNewFieldContent = exports.applyFieldTypesFromContext = exports.applyTypesFromContext = exports.cursorsForTypedFieldData = exports.cursorsFromContextualData = exports.cursorForTypedData = exports.cursorForTypedTreeData = exports.cursorFromContextualData = exports.isContextuallyTypedNodeDataObject = exports.isArrayLike = exports.arrayLikeMarkerSymbol = exports.getPossibleTypes = exports.getAllowedTypes = exports.getFieldKind = exports.getFieldSchema = exports.getPrimaryField = exports.isTreeValue = exports.valueSymbol = exports.typeNameSymbol = exports.isPrimitive = exports.areCursors = void 0;
const core_utils_1 = require("@fluidframework/core-utils");
const util_1 = require("../util");
const core_1 = require("../core");
// TODO:
// This module currently is assuming use of default-field-kinds.
// The field kinds should instead come from a view schema registry thats provided somewhere.
const default_schema_1 = require("./default-schema");
const typed_schema_1 = require("./typed-schema");
const mapTreeCursor_1 = require("./mapTreeCursor");
const valueUtilities_1 = require("./valueUtilities");
const multiplicity_1 = require("./multiplicity");
/**
 * This library defines a tree data format that can infer its types from context.
 * It can only be used when the schema is known.
 * The format is optimized for ergonomics when the developer knows the schema,
 * and needs to declare or navigate trees.
 *
 * The format defined here is very tolerant to optimize for flexibility of expressing trees:
 * APIs exposing data in this format should likely further constrain what is allowed.
 * For example guarantee which fields and nodes should be inlined, and that types will be required everywhere.
 *
 * This is from Editable tree one which has been deleted and should no longer be used!
 */
/**
 * Check if NewFieldContent is made of {@link ITreeCursor}s.
 *
 * Useful when APIs want to take in tree data in multiple formats, including cursors.
 */
function areCursors(data) {
    if ((0, core_1.isCursor)(data)) {
        return true;
    }
    if (Array.isArray(data) && data.length >= 0 && (0, core_1.isCursor)(data[0])) {
        return true;
    }
    return false;
}
exports.areCursors = areCursors;
/**
 * @returns true iff `schema` trees should default to being viewed as just their value when possible.
 * @deprecated This definition of Primitive is from editable-tree-1 and should not be used.
 * @remarks
 * TODO:
 * This (like most things in this file) works with stored schema doing things that should be done with view schema.
 * This just replicates the old editable-tree policy: this entire file should get replaced with the new factory based approach which has access to view schema to align this with how LeafSchema work.
 * @alpha
 */
function isPrimitive(schema) {
    // TODO: use a separate `ITreeSchema` type, with metadata that determines if the type is primitive.
    // Since the above is not done yet, use use a heuristic:
    return (schema.leafValue !== undefined &&
        schema.objectNodeFields.size === 0 &&
        schema.mapFields === undefined);
}
exports.isPrimitive = isPrimitive;
/**
 * String which identifies this code.
 * Targeted at developers: can be used for symbol strings, or other developer targeted strings,
 * like error messages.
 */
const scope = "contextuallyTyped";
/**
 * A symbol for the name of the type of a tree in contexts where string keys are already in use for fields.
 * See {@link TreeNodeSchemaIdentifier}.
 * @alpha
 */
exports.typeNameSymbol = Symbol(`${scope}:typeName`);
/**
 * A symbol for the value of a tree node in contexts where string keys are already in use for fields.
 * @alpha
 */
exports.valueSymbol = Symbol(`${scope}:value`);
/**
 * Checks if a value is a {@link TreeValue}.
 */
function isTreeValue(nodeValue) {
    switch (typeof nodeValue) {
        case "string":
        case "number":
        case "boolean":
            return true;
        default:
            return nodeValue === null || (0, valueUtilities_1.isFluidHandle)(nodeValue);
    }
}
exports.isTreeValue = isTreeValue;
/**
 * @returns the key and the schema of the primary field out of the given tree schema.
 *
 * See note on {@link EmptyKey} for what is a primary field.
 * @alpha
 */
function getPrimaryField(schema) {
    // TODO: have a better mechanism for this. See note on EmptyKey.
    const field = schema.objectNodeFields.get(core_1.EmptyKey);
    if (field === undefined) {
        return undefined;
    }
    return { key: core_1.EmptyKey, schema: field };
}
exports.getPrimaryField = getPrimaryField;
// TODO: this (and most things in this file) should use ViewSchema, and already have the full kind information.
function getFieldSchema(field, schema) {
    return schema.objectNodeFields.get(field) ?? schema.mapFields ?? typed_schema_1.TreeFieldSchema.empty;
}
exports.getFieldSchema = getFieldSchema;
function getFieldKind(fieldSchema) {
    // TODO:
    // This module currently is assuming use of defaultFieldKinds.
    // The field kinds should instead come from a view schema registry thats provided somewhere.
    return default_schema_1.fieldKinds.get(fieldSchema.kind.identifier) ?? (0, util_1.fail)("missing field kind");
}
exports.getFieldKind = getFieldKind;
/**
 * @returns all allowed child types for `typeSet`.
 */
function getAllowedTypes(schemaData, typeSet) {
    // TODO: Performance: avoid the `undefined` case being frequent, possibly with caching in the caller of `getPossibleChildTypes`.
    return typeSet ?? new Set(schemaData.nodeSchema.keys());
}
exports.getAllowedTypes = getAllowedTypes;
/**
 * @returns all types, for which the data is schema-compatible.
 */
function getPossibleTypes(context, typeSet, data) {
    // All types allowed by schema
    const allowedTypes = getAllowedTypes(context.schema, typeSet);
    const possibleTypes = [];
    for (const allowed of allowedTypes) {
        if (shallowCompatibilityTest(context.schema, allowed, data)) {
            possibleTypes.push(allowed);
        }
    }
    return possibleTypes;
}
exports.getPossibleTypes = getPossibleTypes;
/**
 * A symbol used to define a {@link MarkedArrayLike} interface.
 * @alpha
 */
exports.arrayLikeMarkerSymbol = Symbol("editable-tree:arrayLikeMarker");
/**
 * Checks the type of a `ContextuallyTypedNodeData`.
 */
function isArrayLike(data) {
    if (typeof data !== "object" || data === null) {
        return false;
    }
    return (data[exports.arrayLikeMarkerSymbol] ===
        true || Array.isArray(data));
}
exports.isArrayLike = isArrayLike;
/**
 * Checks the type of a `ContextuallyTypedNodeData`.
 * @alpha
 */
function isContextuallyTypedNodeDataObject(data) {
    return !(isTreeValue(data) || isArrayLike(data));
}
exports.isContextuallyTypedNodeDataObject = isContextuallyTypedNodeDataObject;
/**
 * 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(schemaData, type, data) {
    (0, core_utils_1.assert)(!areCursors(data), 0x6b1 /* cursors cannot be used as contextually typed data. */);
    (0, core_utils_1.assert)(data !== undefined, 0x6b2 /* undefined cannot be used as contextually typed data. Use ContextuallyTypedFieldData. */);
    const schema = schemaData.nodeSchema.get(type) ?? (0, util_1.fail)("requested type does not exist in schema");
    if (isTreeValue(data)) {
        return (0, valueUtilities_1.allowsValue)(schema.leafValue, data);
    }
    // TODO: once this is using view schema, replace with schemaIsLeaf
    if (schema.leafValue !== undefined) {
        // Reject objects with no value from being leaf nodes.
        // Note that if allowing IFluidHandles without wrapping them in a leaf node object,
        // this (or the above isPrimitiveValue) would have to change.
        if (data[exports.valueSymbol] === undefined) {
            return false;
        }
    }
    if (isArrayLike(data)) {
        const primary = getPrimaryField(schema);
        return (primary !== undefined &&
            getFieldKind(primary.schema).multiplicity === multiplicity_1.Multiplicity.Sequence);
    }
    if (data instanceof Map) {
        return schema.mapFields !== undefined;
    }
    if (data[exports.typeNameSymbol] !== undefined) {
        return data[exports.typeNameSymbol] === type;
    }
    // For now, consider all not explicitly typed objects shallow compatible.
    // This will require explicit differentiation in polymorphic cases rather than automatic structural differentiation.
    // Special case primitive schema to not be compatible with data with fields.
    if (isPrimitive(schema)) {
        if (fieldKeysFromData(data).length > 0) {
            return false;
        }
    }
    return true;
}
/**
 * Construct a tree from ContextuallyTypedNodeData.
 *
 * TODO: this should probably be refactored into a `try` function which either returns a Cursor or a SchemaError with a path to the error.
 * @returns a cursor in Nodes mode for a single node containing the provided data.
 * @alpha
 */
function cursorFromContextualData(context, typeSet, data) {
    const mapTree = applyTypesFromContext(context, typeSet, data);
    return (0, mapTreeCursor_1.cursorForMapTreeNode)(mapTree);
}
exports.cursorFromContextualData = cursorFromContextualData;
/**
 * Strongly typed {@link cursorFromContextualData} for a TreeNodeSchema.
 * @returns a cursor in Nodes mode for a single node containing the provided data.
 * @alpha
 */
function cursorForTypedTreeData(context, schema, data) {
    return cursorFromContextualData(context, new Set([schema.name]), data);
}
exports.cursorForTypedTreeData = cursorForTypedTreeData;
/**
 * Strongly typed {@link cursorFromContextualData} for AllowedTypes.
 * @returns a cursor in Nodes mode for a single node containing the provided data.
 * @alpha
 */
function cursorForTypedData(context, schema, data) {
    return cursorFromContextualData(context, (0, typed_schema_1.allowedTypesToTypeSet)(schema), data);
}
exports.cursorForTypedData = cursorForTypedData;
/**
 * Construct a tree from ContextuallyTypedNodeData.
 *
 * TODO: this should probably be refactored into a `try` function which either returns a Cursor or a SchemaError with a path to the error.
 * TODO: migrate APIs which take arrays of cursors to take cursors in fields mode.
 */
function cursorsFromContextualData(context, field, data) {
    const mapTrees = applyFieldTypesFromContext(context, field, data);
    return mapTrees.map(mapTreeCursor_1.cursorForMapTreeNode);
}
exports.cursorsFromContextualData = cursorsFromContextualData;
/**
 * Strongly typed {@link cursorsFromContextualData} for a TreeFieldSchema
 * @alpha
 */
function cursorsForTypedFieldData(context, schema, data) {
    return cursorsFromContextualData(context, schema, data);
}
exports.cursorsForTypedFieldData = cursorsForTypedFieldData;
/**
 * Construct a MapTree from ContextuallyTypedNodeData.
 *
 * TODO: this should probably be refactored into a `try` function which either returns a MapTree or a SchemaError with a path to the error.
 * TODO: test suite.
 *
 * @remarks
 * This version is only exported as a more testable entry point than `cursorFromContextualData` which keeps the use of `MapTree` as an implementation detail.
 * This should not be reexported from the parent module.
 */
function applyTypesFromContext(context, typeSet, data) {
    const possibleTypes = getPossibleTypes(context, typeSet, data);
    (0, core_utils_1.assert)(possibleTypes.length !== 0, 0x4d4 /* data incompatible with all types allowed by the schema */);
    (0, core_utils_1.assert)(possibleTypes.length === 1, 0x4d5 /* data compatible with more than one type allowed by the schema */);
    const type = possibleTypes[0];
    const schema = context.schema.nodeSchema.get(type) ?? (0, util_1.fail)("requested type does not exist in schema");
    if (isTreeValue(data)) {
        (0, core_utils_1.assert)((0, valueUtilities_1.allowsValue)(schema.leafValue, data), 0x4d3 /* unsupported schema for provided primitive */);
        return { value: data, type, fields: new Map() };
    }
    else if (isArrayLike(data)) {
        const primary = getPrimaryField(schema);
        (0, core_utils_1.assert)(primary !== undefined, 0x4d6 /* array data reported comparable with the schema without a primary field */);
        const children = applyFieldTypesFromContext(context, primary.schema, data);
        return {
            value: undefined,
            type,
            fields: new Map(children.length > 0 ? [[primary.key, children]] : []),
        };
    }
    else if (data instanceof Map) {
        const fields = new Map();
        for (const [key, value] of data) {
            (0, core_utils_1.assert)(!fields.has(key), 0x7f0 /* Keys should not be duplicated */);
            const childSchema = getFieldSchema(key, schema);
            const children = applyFieldTypesFromContext(context, childSchema, value);
            if (children.length > 0) {
                fields.set(key, children);
            }
        }
        return {
            value: undefined,
            type,
            fields,
        };
    }
    else {
        const fields = new Map();
        for (const key of fieldKeysFromData(data)) {
            (0, core_utils_1.assert)(!fields.has(key), 0x6b3 /* Keys should not be duplicated */);
            const childSchema = getFieldSchema(key, schema);
            const children = applyFieldTypesFromContext(context, childSchema, data[key]);
            if (children.length > 0) {
                fields.set(key, children);
            }
        }
        for (const key of schema.objectNodeFields.keys()) {
            if (data[key] === undefined) {
                setFieldForKey(key, context, schema, fields);
            }
        }
        const value = data[exports.valueSymbol];
        (0, core_utils_1.assert)((0, valueUtilities_1.allowsValue)(schema.leafValue, value), 0x4d7 /* provided value not permitted by the schema */);
        return { value, type, fields };
    }
}
exports.applyTypesFromContext = applyTypesFromContext;
function setFieldForKey(key, context, schema, fields) {
    const requiredFieldSchema = getFieldSchema(key, schema);
    const multiplicity = getFieldKind(requiredFieldSchema).multiplicity;
    if (multiplicity === multiplicity_1.Multiplicity.Single && context.fieldSource !== undefined) {
        const fieldGenerator = context.fieldSource(key, requiredFieldSchema);
        if (fieldGenerator !== undefined) {
            const children = fieldGenerator();
            fields.set(key, children);
        }
    }
}
function fieldKeysFromData(data) {
    const keys = Reflect.ownKeys(data).filter((key) => typeof key === "string");
    return keys;
}
/**
 * Construct a MapTree from ContextuallyTypedNodeData.
 *
 * TODO: this should probably be refactored into a `try` function which either returns a MapTree or a SchemaError with a path to the error.
 * TODO: test suite.
 *
 * @remarks
 * This version is only exported as a more testable entry point than `cursorsFromContextualData` which keeps the use of `MapTree` as an implementation detail.
 * This should not be reexported from the parent module.
 */
function applyFieldTypesFromContext(context, field, data) {
    const multiplicity = getFieldKind(field).multiplicity;
    if (data === undefined) {
        (0, core_utils_1.assert)(multiplicity === multiplicity_1.Multiplicity.Forbidden || multiplicity === multiplicity_1.Multiplicity.Optional, 0x4d8 /* `undefined` provided for a field that does not support `undefined` */);
        return [];
    }
    if (multiplicity === multiplicity_1.Multiplicity.Sequence) {
        (0, core_utils_1.assert)(isArrayLike(data), 0x4d9 /* expected array for a sequence field */);
        const children = Array.from(data, (child) => applyTypesFromContext(context, field.types, child));
        return children;
    }
    (0, core_utils_1.assert)(multiplicity === multiplicity_1.Multiplicity.Single || multiplicity === multiplicity_1.Multiplicity.Optional, 0x4da /* single value provided for an unsupported field */);
    return [applyTypesFromContext(context, field.types, data)];
}
exports.applyFieldTypesFromContext = applyFieldTypesFromContext;
/**
 * Convert NewFieldContent into ITreeCursor array.
 */
function normalizeNewFieldContent(context, schema, content) {
    if (areCursors(content)) {
        if (getFieldKind(schema).multiplicity === multiplicity_1.Multiplicity.Sequence) {
            (0, core_utils_1.assert)((0, util_1.isReadonlyArray)(content), 0x6b7 /* sequence fields require array content */);
            return content;
        }
        else {
            if ((0, util_1.isReadonlyArray)(content)) {
                (0, core_utils_1.assert)(content.length === 1, 0x6b8 /* non-sequence fields can not be provided content that is multiple cursors */);
                return content;
            }
            return [content];
        }
    }
    return cursorsFromContextualData(context, schema, content);
}
exports.normalizeNewFieldContent = normalizeNewFieldContent;
//# sourceMappingURL=contextuallyTyped.js.map