"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.scoped = exports.structuralName = exports.SchemaFactory = exports.SchemaFactory_base = exports.defaultSchemaFactoryObjectOptions = exports.schemaFromValue = void 0;
const internal_1 = require("@fluidframework/core-utils/internal");
const internal_2 = require("@fluidframework/id-compressor/internal");
const internal_3 = require("@fluidframework/runtime-utils/internal");
const internal_4 = require("@fluidframework/telemetry-utils/internal");
const index_js_1 = require("../../util/index.js");
const index_js_2 = require("../core/index.js");
const fieldSchema_js_1 = require("../fieldSchema.js");
const leafNodeSchema_js_1 = require("../leafNodeSchema.js");
const index_js_3 = require("../node-kinds/index.js");
const unhydratedFlexTreeFromInsertable_js_1 = require("../unhydratedFlexTreeFromInsertable.js");
const schemaStatics_js_1 = require("./schemaStatics.js");
/**
 * Gets the leaf domain schema compatible with a given {@link TreeValue}.
 */
function schemaFromValue(value) {
    switch (typeof value) {
        case "boolean": {
            return leafNodeSchema_js_1.booleanSchema;
        }
        case "number": {
            return leafNodeSchema_js_1.numberSchema;
        }
        case "string": {
            return leafNodeSchema_js_1.stringSchema;
        }
        case "object": {
            if (value === null) {
                return leafNodeSchema_js_1.nullSchema;
            }
            (0, internal_1.assert)((0, internal_3.isFluidHandle)(value), 0x87e /* invalid TreeValue */);
            return leafNodeSchema_js_1.handleSchema;
        }
        default: {
            (0, internal_1.unreachableCase)(value);
        }
    }
}
exports.schemaFromValue = schemaFromValue;
/**
 * Default options for Object node schema creation.
 * @remarks Omits parameters that are not relevant for common use cases.
 */
exports.defaultSchemaFactoryObjectOptions = {
    allowUnknownOptionalFields: false,
};
// > = `${TScope extends undefined ? "" : `${TScope}.`}${TName}`;
const schemaStaticsPublic = schemaStatics_js_1.schemaStatics;
/**
 * Create a class with `Statics` as both static properties and member properties.
 * @privateRemarks
 * An attempt was made to let this take in a base class so it could be used again on SchemaFactoryAlpha.
 * This was unsuccessful, mostly due to issues with trying to manipulate constructor types.
 */
function classWithStatics(statics) {
    // eslint-disable-next-line @typescript-eslint/no-extraneous-class
    class WithStatics {
    }
    Object.assign(WithStatics.prototype, statics);
    Object.assign(WithStatics, statics);
    return WithStatics;
}
/**
 * Base class for SchemaFactory, exposes {@link SchemaStatics} as both static properties and member properties.
 * @remarks
 * Do not use this directly, use {@link SchemaFactory} instead.
 * @privateRemarks
 * Exported only as a workaround for {@link https://github.com/microsoft/TypeScript/issues/59550} and {@link https://github.com/microsoft/rushstack/issues/4429}.
 * @system @public
 */
exports.SchemaFactory_base = classWithStatics(schemaStaticsPublic);
// 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
 * For details related to inputting data constrained by schema (including via assignment), and how non-exact schema types are handled in general refer to {@link Input}.
 * For information about recursive schema support, see methods postfixed with "recursive" and {@link ValidateRecursiveSchema}.
 * To apply schema defined with this factory to a tree, see {@link TreeViewConfiguration} and {@link ViewableTree.viewWith}.
 * See the {@link https://fluidframework.com/docs/data-structures/tree/schema-evolution | documentation on schema evolution} for how to handle changes to schema over time.
 *
 * 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.
 * Therefore 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).
 *
 * For "customizable" schema (those which can be subclassed to customize them, see details below) some additional rules must be followed:
 *
 * 1. Only a single {@link TreeNodeSchema|schema} can be used from the class hierarchy deriving from the base class produced by this factory.
 * It is legal to subclass the returned class, and even subclass that class,
 * but only a single class from that class hierarchy can ever be instantiated or passed to any API as a {@link TreeNodeSchema|schema}.
 * These base classes can be used with `instanceof`, but not with schema based APIs like `Tree.is`.
 *
 * 2. If overriding the constructor, the constructor must accept the same argument as the base constructor `super` and forward it to `super` unchanged.
 *
 * 3. Properties for fields defined in the schema should not be overridden.
 *
 * 4. Additional static members added to schema should pick relatively unique keys to reduce the risk of colliding with implementation details what are not exposed in the API.
 *
 * 5. If exporting the schema from a package which uses API-Extractor, export the base class and derived class separately to work around {@link https://github.com/microsoft/rushstack/issues/4429 | a known limitation}.
 *
 * 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 Approach:
 *
 * 1. Declaration: `class X extends schemaFactory.object("x", {}) {}`
 *
 * 2. Allows adding "local" (non-persisted) members: Yes. Members (including methods) can be added to the class.
 *
 * 3. Prototype: The user-defined class.
 *
 * 4. Structurally named Schema: Not Supported.
 *
 * 5. Explicitly named Objects: Supported.
 *
 * 6. Explicitly named Maps and Arrays: Supported: Both declaration approaches can be used.
 *
 * 7. Node.js `assert.deepEqual`: Compares like class instances: equal to other nodes of the same type with the same content, including custom local fields.
 *
 * 8. IntelliSense: Shows and links to user-defined class by name: `X`.
 *
 * 9. Recursion: Supported with special declaration patterns.
 *
 * POJO Emulation Approach:
 *
 * 1. Declaration: `const X = schemaFactory.object("x", {}); type X = NodeFromSchema<typeof X>;`
 *
 * 2. Allows adding "local" (non-persisted) members: No. Attempting to set non-field members will result in an error.
 *
 * 3. Prototype: `Object.prototype`, `Map.prototype`, or `Array.prototype` depending on node kind.
 *
 * 4. Structurally named Schema: Supported.
 *
 * 5. Explicitly named Objects: Supported.
 *
 * 6. Explicitly named Maps and Arrays: Not Supported.
 *
 * 7. Node.js `assert.deepEqual`: 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.
 *
 * 8. IntelliSense: Shows internal type generation logic: `object & TreeNode & ObjectFromSchemaRecord<{}> & WithType<"test.x">`.
 *
 * 9. Recursion: Unsupported: {@link https://github.com/microsoft/TypeScript/issues/55832 | Generated `.d.ts` files replace recursive references with `any`},
 * breaking the 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.
 *
 * Note: the comparison between the customizable and POJO modes is not done in a table because TSDoc does not currently have support for embedded markdown.
 *
 * @see {@link SchemaFactoryAlpha}
 *
 * @sealed @public
 */
class SchemaFactory extends exports.SchemaFactory_base {
    /**
     * Construct a SchemaFactory with a given {@link SchemaFactory.scope|scope}.
     * @remarks
     * There are no restrictions on mixing schema from different schema factories.
     * Typically each library will create one or more SchemaFactories and use them to define its schema.
     */
    constructor(
    /**
     * Prefix appended to the identifiers of all {@link TreeNodeSchema} produced by this builder.
     *
     * @remarks
     * Generally each independently developed library
     * (possibly a package, but could also be part of a package or multiple packages developed together)
     * should get its own unique `scope`.
     * Then each schema in the library get a name which is unique within the library.
     * The scope and name are joined (with a period) to form the {@link TreeNodeSchemaCore.identifier|schema identifier}.
     * Following this pattern allows a single application to depend on multiple libraries which define their own schema, and use them together in a single tree without risk of collisions.
     * If a library logically contains sub-libraries with their own schema, they can be given a scope nested inside the parent scope, such as "ParentScope.ChildScope".
     *
     * To avoid collisions between the scopes of libraries
     * it is recommended that the libraries use {@link https://en.wikipedia.org/wiki/Reverse_domain_name_notation | Reverse domain name notation} or a UUIDv4 for their scope.
     * If this pattern is followed, application can safely use third party libraries without risk of the schema in them colliding.
     *
     * You may opt out of using a scope by passing `undefined`, but note that this increases the risk of collisions.
     *
     * @example
     * Fluid Framework follows this pattern, placing the schema for the built in leaf types in the `com.fluidframework.leaf` scope.
     * If Fluid Framework publishes more schema in the future, they would be under some other `com.fluidframework` scope.
     * This ensures that any schema defined by any other library will not conflict with Fluid Framework's schema
     * as long as the library uses the recommended patterns for how to scope its schema..
     *
     * @example
     * A library could generate a random UUIDv4, like `242c4397-49ed-47e6-8dd0-d5c3bc31778b` and use that as the scope.
     * Note: do not use this UUID: a new one must be randomly generated when needed to ensure collision resistance.
     * ```typescript
     * const factory = new SchemaFactory("242c4397-49ed-47e6-8dd0-d5c3bc31778b");
     * ```
     */
    scope) {
        super();
        this.scope = scope;
        /**
         * TODO:
         * If users of this generate the same name because two different schema with the same identifier were used,
         * the second use can get a cache hit, and reference the wrong schema.
         * Such usage should probably return a distinct type or error but currently does not.
         * The use of markSchemaMostDerived in structuralName at least ensure an error in the case where the collision is from two types extending the same schema factor class.
         */
        this.structuralTypes = new Map();
    }
    /**
     * Define a {@link TreeNodeSchemaClass} 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 (0, index_js_3.objectSchema)(scoped(this, name), fields, true, {
            ...exports.defaultSchemaFactoryObjectOptions,
        });
    }
    /**
     * {@link SchemaFactory.map} implementation.
     *
     * @privateRemarks
     * This should return `TreeNodeSchemaBoth`, however TypeScript gives an error if one of the overloads implicitly up-casts the return type of the implementation.
     * This seems like a TypeScript bug getting variance backwards for overload return types since it's erroring when the relation between the overload
     * and the implementation is type safe, and forcing an unsafe typing instead.
     */
    map(nameOrAllowedTypes, allowedTypes, options) {
        if (allowedTypes === undefined) {
            const types = nameOrAllowedTypes;
            const fullName = structuralName("Map", types);
            (0, internal_1.debugAssert)(() => options === undefined || "No options for structural types");
            return this.getStructuralType(fullName, types, () => this.namedMap(fullName, nameOrAllowedTypes, false, true, {}));
        }
        // To actually have type safety, assign to the type this method should return before implicitly up-casting when returning.
        const out = this.namedMap(nameOrAllowedTypes, allowedTypes, true, true, options ?? {});
        return out;
    }
    /**
     * 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, options) {
        return (0, index_js_3.mapSchema)(scoped(this, name), allowedTypes, implicitlyConstructable, 
        // The current policy is customizable nodes don't get fake prototypes.
        !customizable, options ?? {});
    }
    /**
     * {@link SchemaFactory.array} implementation.
     *
     * @privateRemarks
     * This should return TreeNodeSchemaBoth: see note on "map" implementation for details.
     */
    array(nameOrAllowedTypes, allowedTypes, options) {
        if (allowedTypes === undefined) {
            const types = nameOrAllowedTypes;
            const fullName = structuralName("Array", types);
            (0, internal_1.debugAssert)(() => options === undefined || "No options for structural types");
            return this.getStructuralType(fullName, types, () => this.namedArray(fullName, nameOrAllowedTypes, false, true, {}));
        }
        const out = this.namedArray(nameOrAllowedTypes, allowedTypes, true, true, options ?? {});
        return out;
    }
    /**
     * Retrieves or creates a structural {@link TreeNodeSchema} with the specified name and types.
     *
     * @param fullName - The name for the structural schema.
     * @param types - The input schema(s) used to define the structural schema.
     * @param builder - A function that builds the schema if it does not already exist.
     * @returns The structural {@link TreeNodeSchema} associated with the given name and types.
     * @throws `UsageError` if a schema structurally named schema with the same name is cached in `structuralTypes` but had different input types.
     */
    getStructuralType(fullName, types, builder) {
        const structural = (0, index_js_1.getOrCreate)(this.structuralTypes, fullName, builder);
        const inputTypes = new Set((0, index_js_2.normalizeAllowedTypes)(types));
        const outputTypes = new Set((0, index_js_2.normalizeAllowedTypes)(structural.info));
        // If our cached value had a different set of types then were requested, the user must have caused a collision.
        const same = (0, index_js_1.compareSets)({ a: inputTypes, b: outputTypes });
        if (!same) {
            throw new internal_4.UsageError(`Structurally named schema collision: two schema named "${fullName}" were defined with different input schema.`);
        }
        return structural;
    }
    /**
     * 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, options) {
        return (0, index_js_3.arraySchema)(scoped(this, name), allowedTypes, implicitlyConstructable, customizable, options);
    }
    /**
     * A special readonly field which holds an identifier string for an object node.
     * @remarks
     * The value of this field, a "node identifier", is a string which identifies a node (or nodes) among all 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 specified.
     * When an identifier is not provided, the SharedTree will generate an identifier for the node automatically.
     * The identifier generated by the SharedTree has the following properties:
     *
     * - It is a UUID which will not collide with other generated UUIDs.
     *
     * - It is compressed to a space-efficient representation when stored in the document.
     * Reading the identifier before inserting the node into a tree prevents the identifier from being stored in its compressed form,
     * resulting in a larger storage footprint.
     *
     * - A compressed form of the identifier can be accessed at runtime via the {@link TreeNodeApi.shortId|Tree.shortId()} API.
     *
     * 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, if the user requires it to be unique, it is up to them to ensure uniqueness.
     * 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 {@link TreeNodeApi.shortId|Tree.shortId()} API).
     */
    get identifier() {
        const defaultIdentifierProvider = (0, fieldSchema_js_1.getDefaultProvider)((context) => {
            const id = context === "UseGlobalContext"
                ? globalIdentifierAllocator.decompress(globalIdentifierAllocator.generateCompressedId())
                : context.nodeKeyManager.stabilizeNodeIdentifier(context.nodeKeyManager.generateLocalNodeIdentifier());
            return [(0, unhydratedFlexTreeFromInsertable_js_1.unhydratedFlexTreeFromInsertable)(id, this.string)];
        });
        return (0, fieldSchema_js_1.createFieldSchema)(fieldSchema_js_1.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.
     */
    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.
     * See also {@link FixRecursiveArraySchema} for additional information specific to recursive arrays schema exports.
     */
    // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
    arrayRecursive(name, allowedTypes) {
        const RecursiveArray = this.namedArray(name, allowedTypes, true, false, {});
        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) {
        const MapSchema = this.namedMap(name, allowedTypes, true, 
        // Setting this (implicitlyConstructable) to true seems to work ok currently, but not for other node kinds.
        // Supporting this could be fragile and might break other future changes, so it's being kept as false for now.
        false, {});
        return MapSchema;
    }
}
exports.SchemaFactory = SchemaFactory;
function structuralName(collectionName, allowedTypes) {
    let inner;
    if ((0, index_js_1.isReadonlyArray)(allowedTypes)) {
        const names = allowedTypes.map((t) => {
            // Ensure that lazy types (functions) don't slip through here.
            (0, internal_1.assert)(!(0, index_js_2.isLazy)(t), 0x83d /* invalid type provided */);
            (0, index_js_2.markSchemaMostDerived)(t);
            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 were needed so that a single type name "Any" would not collide with the "any" case which used to exist.
        inner = JSON.stringify(names);
    }
    else {
        return structuralName(collectionName, [allowedTypes]);
    }
    return `${collectionName}<${inner}>`;
}
exports.structuralName = structuralName;
function scoped(factory, name) {
    return (factory.scope === undefined ? `${name}` : `${factory.scope}.${name}`);
}
exports.scoped = scoped;
/**
 * Used to allocate default identifiers for unhydrated nodes when no context is available.
 * @remarks
 * The identifiers allocated by this will never be compressed to Short Ids.
 * Using this is only better than creating fully random V4 UUIDs because it reduces the entropy making it possible for things like text compression to work slightly better.
 */
const globalIdentifierAllocator = (0, internal_2.createIdCompressor)();
//# sourceMappingURL=schemaFactory.js.map