"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.applyFieldTypesFromContext = exports.applyTypesFromContext = exports.cursorsForTypedFieldData = exports.cursorsFromContextualData = exports.cursorForTypedData = exports.cursorForTypedTreeData = exports.cursorFromContextualData = exports.isContextuallyTypedNodeDataObject = exports.isWritableArrayLike = exports.isArrayLike = exports.arrayLikeMarkerSymbol = exports.getPossibleTypes = exports.getAllowedTypes = exports.getFieldKind = exports.getFieldSchema = exports.getPrimaryField = exports.allowsValue = exports.isPrimitiveValue = exports.valueSymbol = exports.typeNameSymbol = void 0;
const common_utils_1 = require("@fluidframework/common-utils");
const util_1 = require("../util");
const core_1 = require("../core");
// TODO:
// This module currently is assuming use of defaultFieldKinds.
// The field kinds should instead come from a view schema registry thats provided somewhere.
const defaultFieldKinds_1 = require("./defaultFieldKinds");
const modular_schema_1 = require("./modular-schema");
const mapTreeCursor_1 = require("./mapTreeCursor");
const editable_tree_1 = require("./editable-tree");
/**
 * 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.
 * See {@link EditableTree} for an example of this.
 */
/**
 * 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 TreeSchemaIdentifier}.
 * @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`);
/**
 * @alpha
 */
function isPrimitiveValue(nodeValue) {
    return nodeValue !== undefined && typeof nodeValue !== "object";
}
exports.isPrimitiveValue = isPrimitiveValue;
function allowsValue(schema, nodeValue) {
    switch (schema) {
        case core_1.ValueSchema.String:
            return typeof nodeValue === "string";
        case core_1.ValueSchema.Number:
            return typeof nodeValue === "number";
        case core_1.ValueSchema.Boolean:
            return typeof nodeValue === "boolean";
        case core_1.ValueSchema.Nothing:
            return typeof nodeValue === "undefined";
        case core_1.ValueSchema.Serializable:
            return true;
        default:
            (0, util_1.fail)("invalid value schema");
    }
}
exports.allowsValue = allowsValue;
/**
 * @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.localFields.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, schemaData, schema) {
    var _a;
    if ((0, core_1.isGlobalFieldKey)(field)) {
        return (0, core_1.lookupGlobalFieldSchema)(schemaData, (0, core_1.keyFromSymbol)(field));
    }
    (0, common_utils_1.assert)(schema !== undefined, 0x423 /* The field is a local field, a parent schema is required. */);
    return (_a = schema.localFields.get(field)) !== null && _a !== void 0 ? _a : schema.extraLocalFields;
}
exports.getFieldSchema = getFieldSchema;
function getFieldKind(fieldSchema) {
    var _a;
    // TODO:
    // This module currently is assuming use of defaultFieldKinds.
    // The field kinds should instead come from a view schema registry thats provided somewhere.
    return (_a = defaultFieldKinds_1.fieldKinds.get(fieldSchema.kind.identifier)) !== null && _a !== void 0 ? _a : (0, util_1.fail)("missing field kind");
}
exports.getFieldKind = getFieldKind;
/**
 * @returns all allowed child types for `typeSet`.
 */
function getAllowedTypes(schema, typeSet) {
    // TODO: Performance: avoid the `undefined` case being frequent, possibly with caching in the caller of `getPossibleChildTypes`.
    return typeSet !== null && typeSet !== void 0 ? typeSet : new Set(schema.treeSchema.keys());
}
exports.getAllowedTypes = getAllowedTypes;
/**
 * @returns all types, for which the data is schema-compatible.
 */
function getPossibleTypes(schemaData, typeSet, data) {
    // All types allowed by schema
    const allowedTypes = getAllowedTypes(schemaData, typeSet);
    const possibleTypes = [];
    for (const allowed of allowedTypes) {
        if (shallowCompatibilityTest(schemaData, 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) {
    return isWritableArrayLike(data) || Array.isArray(data);
}
exports.isArrayLike = isArrayLike;
/**
 * Checks the type of a `ContextuallyTypedNodeData`.
 * @alpha
 */
function isWritableArrayLike(data) {
    if (typeof data !== "object") {
        return false;
    }
    return (data[exports.arrayLikeMarkerSymbol] ===
        true);
}
exports.isWritableArrayLike = isWritableArrayLike;
/**
 * Checks the type of a `ContextuallyTypedNodeData`.
 * @alpha
 */
function isContextuallyTypedNodeDataObject(data) {
    return !(isPrimitiveValue(data) || isArrayLike(data) || data === null);
}
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) {
    const schema = (0, core_1.lookupTreeSchema)(schemaData, type);
    if (isPrimitiveValue(data)) {
        return (0, editable_tree_1.isPrimitive)(schema) && allowsValue(schema.value, data);
    }
    if (isArrayLike(data)) {
        const primary = getPrimaryField(schema);
        return (primary !== undefined &&
            getFieldKind(primary.schema).multiplicity === modular_schema_1.Multiplicity.Sequence);
    }
    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 local fields.
    if ((0, editable_tree_1.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.
 * @alpha
 */
function cursorFromContextualData(schemaData, typeSet, data) {
    const mapTree = applyTypesFromContext(schemaData, typeSet, data);
    return (0, mapTreeCursor_1.singleMapTreeCursor)(mapTree);
}
exports.cursorFromContextualData = cursorFromContextualData;
/**
 * Strongly typed {@link cursorFromContextualData} for a TreeSchema
 * @alpha
 */
function cursorForTypedTreeData(schemaData, schema, data) {
    return cursorFromContextualData(schemaData, new Set([schema.name]), data);
}
exports.cursorForTypedTreeData = cursorForTypedTreeData;
/**
 * Strongly typed {@link cursorFromContextualData} for AllowedTypes.
 * @alpha
 */
function cursorForTypedData(schemaData, schema, data) {
    return cursorFromContextualData(schemaData, (0, modular_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(schemaData, field, data) {
    const mapTrees = applyFieldTypesFromContext(schemaData, field, data);
    return mapTrees.map(mapTreeCursor_1.singleMapTreeCursor);
}
exports.cursorsFromContextualData = cursorsFromContextualData;
/**
 * Strongly typed {@link cursorsFromContextualData} for a FieldSchema
 * @alpha
 */
function cursorsForTypedFieldData(schemaData, schema, data) {
    return cursorFromContextualData(schemaData, schema.types, 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(schemaData, typeSet, data) {
    const possibleTypes = getPossibleTypes(schemaData, typeSet, data);
    (0, common_utils_1.assert)(possibleTypes.length !== 0, 0x4d4 /* data incompatible with all types allowed by the schema */);
    (0, common_utils_1.assert)(possibleTypes.length === 1, 0x4d5 /* data compatible with more than one type allowed by the schema */);
    const type = possibleTypes[0];
    const schema = (0, core_1.lookupTreeSchema)(schemaData, type);
    if (isPrimitiveValue(data)) {
        // This check avoids returning an out of schema node
        // in the case where schema permits the value, but has required fields.
        (0, common_utils_1.assert)((0, editable_tree_1.isPrimitive)(schema), 0x5c3 /* Schema must be primitive when providing a primitive value */);
        (0, common_utils_1.assert)(allowsValue(schema.value, data), 0x4d3 /* unsupported schema for provided primitive */);
        return { value: data, type, fields: new Map() };
    }
    else if (isArrayLike(data)) {
        const primary = getPrimaryField(schema);
        (0, common_utils_1.assert)(primary !== undefined, 0x4d6 /* array data reported comparable with the schema without a primary field */);
        const children = applyFieldTypesFromContext(schemaData, primary.schema, data);
        return { value: undefined, type, fields: new Map([[primary.key, children]]) };
    }
    else {
        const fields = new Map(fieldKeysFromData(data).map((key) => {
            const childKey = (0, util_1.brand)(key);
            const childSchema = getFieldSchema(childKey, schemaData, schema);
            return [
                childKey,
                applyFieldTypesFromContext(schemaData, childSchema, data[childKey]),
            ];
        }));
        const value = data[exports.valueSymbol];
        (0, common_utils_1.assert)(allowsValue(schema.value, value), 0x4d7 /* provided value not permitted by the schema */);
        return { value, type, fields };
    }
}
exports.applyTypesFromContext = applyTypesFromContext;
function fieldKeysFromData(data) {
    const keys = Reflect.ownKeys(data).filter((key) => typeof key === "string" || (0, core_1.symbolIsFieldKey)(key));
    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(schemaData, field, data) {
    const multiplicity = getFieldKind(field).multiplicity;
    if (data === undefined) {
        (0, common_utils_1.assert)(multiplicity === modular_schema_1.Multiplicity.Forbidden || multiplicity === modular_schema_1.Multiplicity.Optional, 0x4d8 /* `undefined` provided for a field that does not support `undefined` */);
        return [];
    }
    if (multiplicity === modular_schema_1.Multiplicity.Sequence) {
        (0, common_utils_1.assert)(isArrayLike(data), 0x4d9 /* expected array for a sequence field */);
        const children = Array.from(data, (child) => applyTypesFromContext(schemaData, field.types, child));
        return children;
    }
    (0, common_utils_1.assert)(multiplicity === modular_schema_1.Multiplicity.Value || multiplicity === modular_schema_1.Multiplicity.Optional, 0x4da /* single value provided for an unsupported field */);
    return [applyTypesFromContext(schemaData, field.types, data)];
}
exports.applyFieldTypesFromContext = applyFieldTypesFromContext;
//# sourceMappingURL=contextuallyTyped.js.map