/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { assert } from "@fluidframework/common-utils";
import { brand, fail } from "../util";
import { EmptyKey, isGlobalFieldKey, keyFromSymbol, ValueSchema, lookupGlobalFieldSchema, lookupTreeSchema, symbolIsFieldKey, } from "../core";
// TODO:
// This module currently is assuming use of defaultFieldKinds.
// The field kinds should instead come from a view schema registry thats provided somewhere.
import { fieldKinds } from "./defaultFieldKinds";
import { Multiplicity, allowedTypesToTypeSet, } from "./modular-schema";
import { singleMapTreeCursor } from "./mapTreeCursor";
import { isPrimitive } from "./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
 */
export const 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
 */
export const valueSymbol = Symbol(`${scope}:value`);
/**
 * @alpha
 */
export function isPrimitiveValue(nodeValue) {
    return nodeValue !== undefined && typeof nodeValue !== "object";
}
export function allowsValue(schema, nodeValue) {
    switch (schema) {
        case ValueSchema.String:
            return typeof nodeValue === "string";
        case ValueSchema.Number:
            return typeof nodeValue === "number";
        case ValueSchema.Boolean:
            return typeof nodeValue === "boolean";
        case ValueSchema.Nothing:
            return typeof nodeValue === "undefined";
        case ValueSchema.Serializable:
            return true;
        default:
            fail("invalid value schema");
    }
}
/**
 * @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
 */
export function getPrimaryField(schema) {
    // TODO: have a better mechanism for this. See note on EmptyKey.
    const field = schema.localFields.get(EmptyKey);
    if (field === undefined) {
        return undefined;
    }
    return { key: EmptyKey, schema: field };
}
// TODO: this (and most things in this file) should use ViewSchema, and already have the full kind information.
export function getFieldSchema(field, schemaData, schema) {
    var _a;
    if (isGlobalFieldKey(field)) {
        return lookupGlobalFieldSchema(schemaData, keyFromSymbol(field));
    }
    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;
}
export 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 = fieldKinds.get(fieldSchema.kind.identifier)) !== null && _a !== void 0 ? _a : fail("missing field kind");
}
/**
 * @returns all allowed child types for `typeSet`.
 */
export 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());
}
/**
 * @returns all types, for which the data is schema-compatible.
 */
export 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;
}
/**
 * A symbol used to define a {@link MarkedArrayLike} interface.
 * @alpha
 */
export const arrayLikeMarkerSymbol = Symbol("editable-tree:arrayLikeMarker");
/**
 * Checks the type of a `ContextuallyTypedNodeData`.
 */
export function isArrayLike(data) {
    return isWritableArrayLike(data) || Array.isArray(data);
}
/**
 * Checks the type of a `ContextuallyTypedNodeData`.
 * @alpha
 */
export function isWritableArrayLike(data) {
    if (typeof data !== "object") {
        return false;
    }
    return (data[arrayLikeMarkerSymbol] ===
        true);
}
/**
 * Checks the type of a `ContextuallyTypedNodeData`.
 * @alpha
 */
export function isContextuallyTypedNodeDataObject(data) {
    return !(isPrimitiveValue(data) || isArrayLike(data) || data === null);
}
/**
 * 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 = lookupTreeSchema(schemaData, type);
    if (isPrimitiveValue(data)) {
        return isPrimitive(schema) && allowsValue(schema.value, data);
    }
    if (isArrayLike(data)) {
        const primary = getPrimaryField(schema);
        return (primary !== undefined &&
            getFieldKind(primary.schema).multiplicity === Multiplicity.Sequence);
    }
    if (data[typeNameSymbol] !== undefined) {
        return data[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 (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
 */
export function cursorFromContextualData(schemaData, typeSet, data) {
    const mapTree = applyTypesFromContext(schemaData, typeSet, data);
    return singleMapTreeCursor(mapTree);
}
/**
 * Strongly typed {@link cursorFromContextualData} for a TreeSchema
 * @alpha
 */
export function cursorForTypedTreeData(schemaData, schema, data) {
    return cursorFromContextualData(schemaData, new Set([schema.name]), data);
}
/**
 * Strongly typed {@link cursorFromContextualData} for AllowedTypes.
 * @alpha
 */
export function cursorForTypedData(schemaData, schema, data) {
    return cursorFromContextualData(schemaData, allowedTypesToTypeSet(schema), data);
}
/**
 * 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.
 */
export function cursorsFromContextualData(schemaData, field, data) {
    const mapTrees = applyFieldTypesFromContext(schemaData, field, data);
    return mapTrees.map(singleMapTreeCursor);
}
/**
 * Strongly typed {@link cursorsFromContextualData} for a FieldSchema
 * @alpha
 */
export function cursorsForTypedFieldData(schemaData, schema, data) {
    return cursorFromContextualData(schemaData, schema.types, data);
}
/**
 * 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.
 */
export function applyTypesFromContext(schemaData, typeSet, data) {
    const possibleTypes = getPossibleTypes(schemaData, typeSet, data);
    assert(possibleTypes.length !== 0, 0x4d4 /* data incompatible with all types allowed by the schema */);
    assert(possibleTypes.length === 1, 0x4d5 /* data compatible with more than one type allowed by the schema */);
    const type = possibleTypes[0];
    const schema = 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.
        assert(isPrimitive(schema), 0x5c3 /* Schema must be primitive when providing a primitive value */);
        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);
        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 = brand(key);
            const childSchema = getFieldSchema(childKey, schemaData, schema);
            return [
                childKey,
                applyFieldTypesFromContext(schemaData, childSchema, data[childKey]),
            ];
        }));
        const value = data[valueSymbol];
        assert(allowsValue(schema.value, value), 0x4d7 /* provided value not permitted by the schema */);
        return { value, type, fields };
    }
}
function fieldKeysFromData(data) {
    const keys = Reflect.ownKeys(data).filter((key) => typeof key === "string" || 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.
 */
export function applyFieldTypesFromContext(schemaData, field, data) {
    const multiplicity = getFieldKind(field).multiplicity;
    if (data === undefined) {
        assert(multiplicity === Multiplicity.Forbidden || multiplicity === Multiplicity.Optional, 0x4d8 /* `undefined` provided for a field that does not support `undefined` */);
        return [];
    }
    if (multiplicity === Multiplicity.Sequence) {
        assert(isArrayLike(data), 0x4d9 /* expected array for a sequence field */);
        const children = Array.from(data, (child) => applyTypesFromContext(schemaData, field.types, child));
        return children;
    }
    assert(multiplicity === Multiplicity.Value || multiplicity === Multiplicity.Optional, 0x4da /* single value provided for an unsupported field */);
    return [applyTypesFromContext(schemaData, field.types, data)];
}
//# sourceMappingURL=contextuallyTyped.js.map