/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { assert, unreachableCase } from "@fluidframework/core-utils/internal";
import { isFlexTreeNode, isLazy, } from "../feature-libraries/index.js";
import { getOrCreate, isReadonlyArray, } from "../util/index.js";
import { booleanSchema, handleSchema, nullSchema, numberSchema, stringSchema, } from "./leafNodeSchema.js";
import { FieldKind, createFieldSchema, getDefaultProvider, } from "./schemaTypes.js";
import { arraySchema } from "./arrayNode.js";
import { isFluidHandle } from "@fluidframework/runtime-utils/internal";
import { objectSchema, } from "./objectNode.js";
import { mapSchema } from "./mapNode.js";
import { createFieldSchemaUnsafe } from "./schemaFactoryRecursive.js";
/**
 * Gets the leaf domain schema compatible with a given {@link TreeValue}.
 */
export function schemaFromValue(value) {
    switch (typeof value) {
        case "boolean":
            return booleanSchema;
        case "number":
            return numberSchema;
        case "string":
            return stringSchema;
        case "object": {
            if (value === null) {
                return nullSchema;
            }
            assert(isFluidHandle(value), 0x87e /* invalid TreeValue */);
            return handleSchema;
        }
        default:
            unreachableCase(value);
    }
}
// > = `${TScope extends undefined ? "" : `${TScope}.`}${TName}`;
// TODO:
// SchemaFactory.array references should link to the correct overloads, however the syntax for this does not seems to work currently for methods unless the they are not qualified with the class.
// API-Extractor requires such links to be qualified with the class, so it can't work.
// Since linking the overload set as a whole also doesn't work, these have been made non-links for now.
/**
 * Creates various types of {@link TreeNodeSchema|schema} for {@link TreeNode}s.
 *
 * @typeParam TScope - Scope added as a prefix to the name of every schema produced by this factory.
 * @typeParam TName - Type of names used to identify each schema produced in this factory.
 * Typically this is just `string` but it is also possible to use `string` or `number` based enums if you prefer to identify your types that way.
 *
 * @remarks
 * All schema produced by this factory get a {@link TreeNodeSchemaCore.identifier|unique identifier} by combining the {@link SchemaFactory.scope} with the schema's `Name`.
 * The `Name` part may be explicitly provided as a parameter, or inferred as a structural combination of the provided types.
 * The APIs which use this second approach, structural naming, also deduplicate all equivalent calls.
 * Therefor two calls to `array(allowedTypes)` with the same allowedTypes will return the same {@link TreeNodeSchema} instance.
 * On the other hand, two calls to `array(name, allowedTypes)` will always return different {@link TreeNodeSchema} instances
 * and it is an error to use both in the same tree (since their identifiers are not unique).
 *
 * Note:
 * POJO stands for Plain Old JavaScript Object.
 * This means an object that works like a `{}` style object literal.
 * In this case it means the prototype is `Object.prototype` and acts like a set of key value pairs (data, not methods).
 * The usage below generalizes this to include array and map like objects as well.
 *
 * There are two ways to use these APIs:
 * |                     | Customizable | POJO Emulation |
 * | ------------------- | ------------ |--------------- |
 * | Declaration         | `class X extends schemaFactory.object("x", {}) {}` | `const X = schemaFactory.object("x", {}); type X = NodeFromSchema<typeof X>; `
 * | Allows adding "local" (non-persisted) members | Yes. Members (including methods) can be added to class.        | No. Attempting to set non-field members will error. |
 * | Prototype | The user defined class | `Object.prototype`, `Map.prototype` or `Array.prototype` depending on node kind |
 * | Structurally named Schema | Not Supported | Supported |
 * | Explicitly named Objects | Supported | Supported |
 * | Explicitly named Maps and Arrays | Supported: Both declaration approaches can be used | Not Supported |
 * | node.js assert.deepEqual | Compares like class instances: equal to other nodes of the same type with the same content, including custom local fields. | Compares like plain objects: equal to plain JavaScript objects with the same fields, and other nodes with the same fields, even if the types are different. |
 * | IntelliSense | Shows and links to user defined class by name: `X` | Shows internal type generation logic: `object & TreeNode & ObjectFromSchemaRecord<{}> & WithType<"test.x">` |
 * | Recursion | Supported with special declaration patterns. | Unsupported: Generated d.ts files replace recursive references with `any`, breaking use of recursive schema across compilation boundaries |
 *
 * Note that while "POJO Emulation" nodes act a lot like POJO objects, they are not true POJO objects:
 *
 * - Adding new arbitrary fields will error, as well some cases of invalid edits.
 *
 * - They are implemented using proxies.
 *
 * - They have state that is not exposed via enumerable own properties, including a {@link TreeNodeSchema}.
 * This makes libraries like node.js `assert.deepEqual` fail to detect differences in type.
 *
 * - Assigning members has side effects (in this case editing the persisted/shared tree).
 *
 * - Not all operations implied by the prototype will work correctly: stick to the APIs explicitly declared in the TypeScript types.
 *
 * @privateRemarks
 * It's perfectly possible to make `POJO Emulation` mode (or even just hiding the prototype) selectable even when using the custom user class declaration syntax.
 * When doing this, it's still possible to make `instanceof` perform correctly.
 * Allowing (or banning) custom/out-of-schema properties on the class is also possible in both modes: it could be orthogonal.
 * Also for consistency, if keeping the current approach to detecting `POJO Emulation` mode it might make sense to make explicitly named Maps and Arrays do the detection the same as how object does it.
 *
 * @sealed @public
 */
export class SchemaFactory {
    /**
     * @param scope - Prefix appended to the identifiers of all {@link TreeNodeSchema} produced by this builder.
     * Use of [Reverse domain name notation](https://en.wikipedia.org/wiki/Reverse_domain_name_notation) or a UUIDv4 is recommended to avoid collisions.
     * You may opt out of using a scope by passing `undefined`, but note that this increases the risk of collisions.
     */
    constructor(scope) {
        this.scope = scope;
        this.structuralTypes = new Map();
        /**
         * {@link TreeNodeSchema} for holding a JavaScript `string`.
         *
         * @remarks
         * Strings containing unpaired UTF-16 surrogate pair code units may not be handled correctly.
         *
         * These limitations come from the use of UTF-8 encoding of the strings, which requires them to be valid unicode.
         * JavaScript does not make this requirement for its strings so not all possible JavaScript strings are supported.
         * @privateRemarks
         * TODO:
         * We should be much more clear about what happens if you use problematic values.
         * We should validate and/or normalize them when inserting content.
         */
        this.string = stringSchema;
        /**
         * {@link TreeNodeSchema} for holding a JavaScript `number`.
         *
         * @remarks
         * The number is a [double-precision 64-bit binary format IEEE 754](https://en.wikipedia.org/wiki/Double-precision_floating-point_format) value, however there are some exceptions:
         * - `NaN`, and the infinities are converted to `null` (and may therefore only be used where `null` is allowed by the schema).
         * - `-0` may be converted to `0` in some cases.
         *
         * These limitations match the limitations of JSON.
         * @privateRemarks
         * TODO:
         * We should be much more clear about what happens if you use problematic values.
         * We should validate and/or normalize them when inserting content.
         */
        this.number = numberSchema;
        /**
         * {@link TreeNodeSchema} for holding a boolean.
         */
        this.boolean = booleanSchema;
        /**
         * {@link TreeNodeSchema} for JavaScript `null`.
         *
         * @remarks
         * There are good [reasons to avoid using null](https://www.npmjs.com/package/%40rushstack/eslint-plugin#rushstackno-new-null) in JavaScript, however sometimes it is desired.
         * This {@link TreeNodeSchema} node provides the option to include nulls in trees when desired.
         * Unless directly inter-operating with existing data using null, consider other approaches, like wrapping the value in an optional field, or using a more specifically named empty object node.
         */
        this.null = nullSchema;
        /**
         * {@link TreeNodeSchema} for holding an {@link @fluidframework/core-interfaces#(IFluidHandle:interface)}.
         */
        this.handle = handleSchema;
    }
    scoped(name) {
        return (this.scope === undefined ? `${name}` : `${this.scope}.${name}`);
    }
    /**
     * Define a {@link TreeNodeSchema} for a {@link TreeObjectNode}.
     *
     * @param name - Unique identifier for this schema within this factory's scope.
     * @param fields - Schema for fields of the object node's schema. Defines what children can be placed under each key.
     */
    object(name, fields) {
        return objectSchema(this.scoped(name), fields, true);
    }
    map(nameOrAllowedTypes, allowedTypes) {
        if (allowedTypes === undefined) {
            const types = nameOrAllowedTypes;
            const fullName = structuralName("Map", types);
            return getOrCreate(this.structuralTypes, fullName, () => this.namedMap(fullName, nameOrAllowedTypes, false, true));
        }
        return this.namedMap(nameOrAllowedTypes, allowedTypes, true, true);
    }
    /**
     * Define a {@link TreeNodeSchema} for a {@link (TreeMapNode:interface)}.
     *
     * @param name - Unique identifier for this schema within this factory's scope.
     */
    namedMap(name, allowedTypes, customizable, implicitlyConstructable) {
        return mapSchema(this.scoped(name), allowedTypes, implicitlyConstructable, 
        // The current policy is customizable nodes don't get fake prototypes.
        !customizable);
    }
    array(nameOrAllowedTypes, allowedTypes) {
        if (allowedTypes === undefined) {
            const types = nameOrAllowedTypes;
            const fullName = structuralName("Array", types);
            return getOrCreate(this.structuralTypes, fullName, () => this.namedArray(fullName, nameOrAllowedTypes, false, true));
        }
        return this.namedArray(nameOrAllowedTypes, allowedTypes, true, true);
    }
    /**
     * Define a {@link TreeNodeSchema} for a {@link (TreeArrayNode:interface)}.
     *
     * @param name - Unique identifier for this schema within this factory's scope.
     *
     * @remarks
     * This is not intended to be used directly, use the overload of `array` which takes a name instead.
     * This is only public to work around a compiler limitation.
     */
    namedArray(name, allowedTypes, customizable, implicitlyConstructable) {
        return arraySchema(this.scoped(name), allowedTypes, implicitlyConstructable, customizable);
    }
    /**
     * Make a field optional instead of the default, which is required.
     *
     * @param t - The types allowed under the field.
     * @param props - Optional properties to associate with the field.
     */
    optional(t, props) {
        const defaultOptionalProvider = getDefaultProvider(() => {
            return undefined;
        });
        return createFieldSchema(FieldKind.Optional, t, {
            defaultProvider: defaultOptionalProvider,
            ...props,
        });
    }
    /**
     * Make a field explicitly required.
     *
     * @param t - The types allowed under the field.
     * @param props - Optional properties to associate with the field.
     *
     * @remarks
     * Fields are required by default, but this API can be used to make the required nature explicit in the schema,
     * and allows associating custom {@link FieldProps | properties} with the field.
     */
    required(t, props) {
        return createFieldSchema(FieldKind.Required, t, props);
    }
    /**
     * {@link SchemaFactory.optional} except tweaked to work better for recursive types.
     * Use with {@link ValidateRecursiveSchema} for improved type safety.
     * @remarks
     * This version of {@link SchemaFactory.optional} has fewer type constraints to work around TypeScript limitations, see {@link Unenforced}.
     * See {@link ValidateRecursiveSchema} for additional information about using recursive schema.
     */
    optionalRecursive(t, props) {
        return createFieldSchemaUnsafe(FieldKind.Optional, t, props);
    }
    /**
     * {@link SchemaFactory.required} except tweaked to work better for recursive types.
     * Use with {@link ValidateRecursiveSchema} for improved type safety.
     * @remarks
     * This version of {@link SchemaFactory.required} has fewer type constraints to work around TypeScript limitations, see {@link Unenforced}.
     * See {@link ValidateRecursiveSchema} for additional information about using recursive schema.
     */
    requiredRecursive(t, props) {
        return createFieldSchemaUnsafe(FieldKind.Required, t, props);
    }
    /**
     * A special field which holds a unique identifier for an object node.
     * @remarks
     * The value of this field, a "node identifier", uniquely identifies a node among all other nodes in the tree.
     * Node identifiers are strings, and can therefore be used as lookup keys in maps or written to a database.
     * When the node is constructed, the identifier field does not need to be populated.
     * The SharedTree will provide an identifier for the node automatically.
     * An identifier provided automatically by the SharedTree has the following properties:
     * - It is a UUID.
     * - It is compressed to a space-efficient representation when stored in the document.
     * - A compressed form of the identifier can be accessed at runtime via the `Tree.shortId()` API.
     * - It will error if read (and will not be present in the object's iterable properties) before the node has been inserted into the tree.
     *
     * However, a user may alternatively supply their own string as the identifier if desired (for example, if importing identifiers from another system).
     * In that case, it is up to the user to ensure that the identifier is unique within the current tree - no other node should have the same identifier at the same time.
     * If the identifier is not unique, it may be read, but may cause libraries or features which operate over node identifiers to misbehave.
     * User-supplied identifiers may be read immediately, even before insertion into the tree.
     *
     * A node may have more than one identifier field (though note that this precludes the use of the `Tree.shortId()` API).
     */
    get identifier() {
        const defaultIdentifierProvider = getDefaultProvider((nodeKeyManager) => {
            return nodeKeyManager.stabilizeNodeKey(nodeKeyManager.generateLocalNodeKey());
        });
        return createFieldSchema(FieldKind.Identifier, this.string, {
            defaultProvider: defaultIdentifierProvider,
        });
    }
    /**
     * {@link SchemaFactory.object} except tweaked to work better for recursive types.
     * Use with {@link ValidateRecursiveSchema} for improved type safety.
     * @remarks
     * This version of {@link SchemaFactory.object} has fewer type constraints to work around TypeScript limitations, see {@link Unenforced}.
     * See {@link ValidateRecursiveSchema} for additional information about using recursive schema.
     *
     * Additionally `ImplicitlyConstructable` is disabled (forcing use of constructor) to avoid
     * `error TS2589: Type instantiation is excessively deep and possibly infinite.`
     * which otherwise gets reported at sometimes incorrect source locations that vary based on incremental builds.
     */
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
    objectRecursive(name, t) {
        return this.object(name, t);
    }
    /**
     * `SchemaFactory.array` except tweaked to work better for recursive types.
     * Use with {@link ValidateRecursiveSchema} for improved type safety.
     * @remarks
     * This version of `SchemaFactory.array` uses the same workarounds as {@link SchemaFactory.objectRecursive}.
     * See {@link ValidateRecursiveSchema} for additional information about using recursive schema.
     */
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
    arrayRecursive(name, allowedTypes) {
        class RecursiveArray extends this.namedArray(name, allowedTypes, true, false) {
            constructor(data) {
                if (isFlexTreeNode(data)) {
                    // TODO: use something other than `any`
                    // eslint-disable-next-line @typescript-eslint/no-explicit-any
                    super(data);
                }
                else {
                    super(data);
                }
            }
        }
        return RecursiveArray;
    }
    /**
     * `SchemaFactory.map` except tweaked to work better for recursive types.
     * Use with {@link ValidateRecursiveSchema} for improved type safety.
     * @remarks
     * This version of `SchemaFactory.map` uses the same workarounds as {@link SchemaFactory.objectRecursive}.
     * See {@link ValidateRecursiveSchema} for additional information about using recursive schema.
     */
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
    mapRecursive(name, allowedTypes) {
        class MapSchema extends this.namedMap(name, allowedTypes, true, false) {
            constructor(data) {
                if (isFlexTreeNode(data)) {
                    // TODO: use something other than `any`
                    // eslint-disable-next-line @typescript-eslint/no-explicit-any
                    super(data);
                }
                else {
                    super(new Map(data));
                }
            }
        }
        return MapSchema;
    }
}
export function structuralName(collectionName, allowedTypes) {
    let inner;
    if (!isReadonlyArray(allowedTypes)) {
        return structuralName(collectionName, [allowedTypes]);
    }
    else {
        const names = allowedTypes.map((t) => {
            // Ensure that lazy types (functions) don't slip through here.
            assert(!isLazy(t), 0x83d /* invalid type provided */);
            return t.identifier;
        });
        // Ensure name is order independent
        names.sort();
        // Ensure name can't have collisions by quoting and escaping any quotes in the names of types.
        // Using JSON is a simple way to accomplish this.
        // The outer `[]` around the result are also needed so that a single type name "Any" would not collide with the "any" case above.
        inner = JSON.stringify(names);
    }
    return `${collectionName}<${inner}>`;
}
//# sourceMappingURL=schemaFactory.js.map