/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { assert } from "@fluidframework/core-utils";
import { fail, isReadonlyArray } from "../util/index.js";
import { EmptyKey, isCursor, } from "../core/index.js";
// 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.
import { fieldKinds } from "./default-schema/index.js";
import { Any, FlexFieldNodeSchema, LeafNodeSchema, FlexMapNodeSchema, FlexObjectNodeSchema, allowedTypesSchemaSet, } from "./typed-schema/index.js";
import { cursorForMapTreeField, cursorForMapTreeNode, mapTreeFromCursor } from "./mapTreeCursor.js";
import { isFluidHandle, allowsValue } from "./valueUtilities.js";
import { Multiplicity } from "./multiplicity.js";
/**
 * 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.
 */
export function areCursors(data) {
    if (isCursor(data)) {
        return true;
    }
    if (Array.isArray(data) && data.length >= 0 && isCursor(data[0])) {
        return true;
    }
    return false;
}
/**
 * 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}.
 * @internal
 */
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.
 * @internal
 */
export const valueSymbol = Symbol(`${scope}:value`);
/**
 * Checks if a value is a {@link TreeValue}.
 */
export function isTreeValue(nodeValue) {
    switch (typeof nodeValue) {
        case "string":
        case "number":
        case "boolean":
            return true;
        default:
            return nodeValue === null || isFluidHandle(nodeValue);
    }
}
export 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 fieldKinds.get(fieldSchema.kind.identifier) ?? fail("missing field kind");
}
/**
 * @returns all allowed child types for `typeSet`.
 */
export function getAllowedTypes(schemaData, typeSet) {
    // TODO: Performance: avoid the `Any` case being frequent, possibly with caching in the caller of `getPossibleChildTypes`.
    return typeSet === Any ? new Set(schemaData.nodeSchema.values()) : typeSet;
}
/**
 * @returns all types, for which the data is schema-compatible.
 */
export function getPossibleTypes(context, typeSet, data) {
    // All types allowed by schema
    const allowedTypes = getAllowedTypes(context, typeSet);
    const possibleTypes = [];
    for (const allowed of allowedTypes) {
        if (shallowCompatibilityTest(allowed, data)) {
            possibleTypes.push(allowed);
        }
    }
    return possibleTypes;
}
/**
 * A symbol used to define a {@link MarkedArrayLike} interface.
 * @internal
 */
export const arrayLikeMarkerSymbol = Symbol("editable-tree:arrayLikeMarker");
/**
 * Checks the type of a `ContextuallyTypedNodeData`.
 */
export function isArrayLike(data) {
    if (typeof data !== "object" || data === null) {
        return false;
    }
    return (data[arrayLikeMarkerSymbol] ===
        true || Array.isArray(data));
}
/**
 * Checks the type of a `ContextuallyTypedNodeData`.
 * @internal
 */
export function isContextuallyTypedNodeDataObject(data) {
    return !(isTreeValue(data) || isArrayLike(data));
}
/**
 * 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(!areCursors(data), 0x6b1 /* cursors cannot be used as contextually typed data. */);
    assert(data !== undefined, 0x6b2 /* undefined cannot be used as contextually typed data. Use ContextuallyTypedFieldData. */);
    if (schema instanceof LeafNodeSchema) {
        // 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 (isTreeValue(data)) {
            return allowsValue(schema.leafValue, data);
        }
        if (data[valueSymbol] === undefined) {
            return false;
        }
    }
    if (isTreeValue(data)) {
        return false;
    }
    if (isArrayLike(data)) {
        if (schema instanceof FlexFieldNodeSchema) {
            const field = schema.getFieldSchema();
            return field.kind.multiplicity === Multiplicity.Sequence;
        }
        else {
            return false;
        }
    }
    if (data[typeNameSymbol] !== undefined) {
        return data[typeNameSymbol] === schema.name;
    }
    // For now, consider all not explicitly typed objects shallow compatible.
    // This will require explicit differentiation in polymorphic cases rather than automatic structural differentiation.
    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.
 * @internal
 */
export function cursorFromContextualData(context, typeSet, data) {
    const mapTree = applyTypesFromContext(context, typeSet, data);
    return cursorForMapTreeNode(mapTree);
}
/**
 * Strongly typed {@link cursorFromContextualData} for a TreeNodeSchema.
 * @returns a cursor in Nodes mode for a single node containing the provided data.
 * @internal
 */
export function cursorForTypedTreeData(context, schema, data) {
    return cursorFromContextualData(context, new Set([schema]), data);
}
/**
 * Strongly typed {@link cursorFromContextualData} for AllowedTypes.
 * @returns a cursor in Nodes mode for a single node containing the provided data.
 * @internal
 */
export function cursorForTypedData(context, schema, data) {
    return cursorFromContextualData(context, allowedTypesSchemaSet(schema), data);
}
/**
 * Construct a tree from ContextuallyTypedNodeData.
 * Returns a cursor in Field mode.
 *
 * TODO: this should probably be refactored into a `try` function which either returns a Cursor or a SchemaError with a path to the error.
 */
export function cursorsFromContextualData(context, field, data) {
    const mapTrees = applyFieldTypesFromContext(context, field, data);
    return cursorForMapTreeField(mapTrees);
}
/**
 * Strongly typed {@link cursorsFromContextualData} for a TreeFieldSchema
 * @internal
 */
export function cursorsForTypedFieldData(context, schema, data) {
    return cursorsFromContextualData(context, schema, 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(context, typeSet, data) {
    const possibleTypes = getPossibleTypes(context.schema, 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 schema = possibleTypes[0];
    if (schema instanceof LeafNodeSchema) {
        const value = isTreeValue(data)
            ? data
            : data[valueSymbol];
        assert(allowsValue(schema.leafValue, value), 0x4d3 /* unsupported schema for provided primitive */);
        return { value, type: schema.name, fields: new Map() };
    }
    assert(!isTreeValue(data), 0x880 /* leaf value for non leaf */);
    if (schema instanceof FlexFieldNodeSchema) {
        if (isArrayLike(data)) {
            const children = applyFieldTypesFromContext(context, schema.getFieldSchema(), data);
            return {
                value: undefined,
                type: schema.name,
                fields: new Map(children.length > 0 ? [[EmptyKey, children]] : []),
            };
        }
    }
    assert(!isArrayLike(data), 0x881 /* array for non field node */);
    if (schema instanceof FlexMapNodeSchema ||
        schema instanceof FlexObjectNodeSchema ||
        schema instanceof FlexFieldNodeSchema) {
        const fields = new Map();
        for (const key of fieldKeysFromData(data)) {
            assert(!fields.has(key), 0x6b3 /* Keys should not be duplicated */);
            const childSchema = schema.getFieldSchema(key);
            const children = applyFieldTypesFromContext(context, childSchema, data[key]);
            if (children.length > 0) {
                fields.set(key, children);
            }
        }
        if (schema instanceof FlexObjectNodeSchema) {
            for (const key of schema.objectNodeFields.keys()) {
                if (data[key] === undefined) {
                    setFieldForKey(key, context, schema, fields);
                }
            }
        }
        const value = data[valueSymbol];
        assert(value === undefined, 0x4d7 /* provided value not permitted by the schema */);
        return { value, type: schema.name, fields };
    }
    else {
        fail("unexpected node kind");
    }
}
function setFieldForKey(key, context, schema, fields) {
    const requiredFieldSchema = schema.getFieldSchema(key);
    const multiplicity = getFieldKind(requiredFieldSchema).multiplicity;
    if (multiplicity === 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.
 */
export function applyFieldTypesFromContext(context, 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(context, field.allowedTypeSet, child));
        return children;
    }
    assert(multiplicity === Multiplicity.Single || multiplicity === Multiplicity.Optional, 0x4da /* single value provided for an unsupported field */);
    return [applyTypesFromContext(context, field.allowedTypeSet, data)];
}
/**
 * Convert NewFieldContent into ITreeCursorSynchronous.
 * The returned cursor will be in Field mode.
 */
export function normalizeNewFieldContent(context, schema, content) {
    if (areCursors(content)) {
        if (isReadonlyArray(content)) {
            assert(getFieldKind(schema).multiplicity === Multiplicity.Sequence || content.length === 1, 0x6b8 /* non-sequence fields can not be provided content that is multiple cursors */);
            // TODO: is there a better way to get a field cursor from an array of node cursors?
            const mapTrees = content.map((c) => mapTreeFromCursor(c));
            return cursorForMapTreeField(mapTrees);
        }
        if (content.mode === 1 /* CursorLocationType.Fields */) {
            return content;
        }
        // TODO: is there a better way to get a field cursor from a node cursor?
        return cursorForMapTreeField([mapTreeFromCursor(content)]);
    }
    return cursorsFromContextualData(context, schema, content);
}
//# sourceMappingURL=contextuallyTyped.js.map