/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { assert } from "@fluidframework/core-utils";
import { getOrCreate, isReadonlyArray } from "../util";
import { isFlexTreeNode, isLazy, markEager, } from "../feature-libraries";
import { leaf } from "../domains";
import { createNodeProxy, createRawNodeProxy, getClassSchema, getSequenceField, listPrototypeProperties, mapStaticDispatchMap,
// eslint-disable-next-line import/no-internal-modules
 } from "../simple-tree/proxies";
import { getFlexSchema, setFlexSchemaFromClassSchema } from "./toFlexSchema";
import { FieldKind, FieldSchema, NodeBase, NodeKind, } from "./schemaTypes";
/**
 * Instances of this class are schema for leaf nodes.
 * @remarks
 * Unlike other schema, leaf schema are class instances instead of classes themselves.
 * This is because the instance type (the tree node type) for leaves are not objects,
 * so those instances can't be instances of a schema based class.
 * @privateRemarks
 * This class refers to the underlying flex tree schema in its constructor, so this class can't be included in the package API.
 */
class LeafNodeSchema {
    create(data) {
        return data;
    }
    constructor(schema) {
        this.kind = NodeKind.Leaf;
        setFlexSchemaFromClassSchema(this, schema);
        this.identifier = schema.name;
        this.info = schema.info;
    }
}
/**
 * Wrapper around LeafNodeSchema's constructor that provides the return type that is desired in the package public API.
 */
function makeLeaf(schema) {
    return new LeafNodeSchema(schema);
}
// Leaf schema shared between all SchemaFactory instances.
const stringSchema = makeLeaf(leaf.string);
const numberSchema = makeLeaf(leaf.number);
const booleanSchema = makeLeaf(leaf.boolean);
const nullSchema = makeLeaf(leaf.null);
const handleSchema = makeLeaf(leaf.handle);
/**
 * Builds schema libraries, and the schema within them.
 *
 * @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.
 *
 * @sealed @beta
 */
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.
     */
    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}.
         */
        this.handle = handleSchema;
    }
    scoped(name) {
        return `${this.scope}.${name}`;
    }
    /**
     * Construct a class that provides the common parts all TreeNodeSchemaClass share.
     * More specific schema extend this class.
     */
    nodeSchema(name, kind, t) {
        const identifier = this.scoped(name);
        class schema extends NodeBase {
            /**
             * This constructor only does validation of the input, and should be passed the argument from the derived type unchanged.
             * It is up to the derived type to actually do something with this value.
             */
            constructor(input) {
                super();
                // Currently this just does validation. All other logic is in the subclass.
                if (isFlexTreeNode(input)) {
                    assert(getClassSchema(input.schema) === this.constructor, 0x83b /* building node with wrong schema */);
                }
                // TODO: make this a better user facing error, and explain how to copy explicitly.
                assert(!(input instanceof NodeBase), 0x83c /* Existing nodes cannot be used as new content to insert. They must either be moved or explicitly copied */);
            }
        }
        schema.identifier = identifier;
        schema.kind = kind;
        schema.info = t;
        // Class objects are functions (callable), so we need a strong way to distinguish between `schema` and `() => schema` when used as a `LazyItem`.
        markEager(schema);
        return schema;
    }
    /**
     * Define a {@link TreeNodeSchema} for an object node.
     *
     * @param name - Unique identifier for this schema within this factory's scope.
     */
    object(name, t) {
        const allowAdditionalProperties = true;
        class schema extends this.nodeSchema(name, NodeKind.Object, t) {
            constructor(input) {
                super(input);
                if (isFlexTreeNode(input)) {
                    return createNodeProxy(input, allowAdditionalProperties, this);
                }
                else {
                    const flexSchema = getFlexSchema(this.constructor);
                    return createRawNodeProxy(flexSchema, input, allowAdditionalProperties, this);
                }
            }
        }
        return schema;
    }
    map(nameOrAllowedTypes, allowedTypes) {
        if (allowedTypes === undefined) {
            const types = nameOrAllowedTypes;
            const fullName = structuralName("Map", types);
            return getOrCreate(this.structuralTypes, fullName, () => this.namedMap(fullName, nameOrAllowedTypes, false));
        }
        return this.namedMap(nameOrAllowedTypes, allowedTypes, true);
    }
    namedMap(name, allowedTypes, customizable) {
        class schema extends this.nodeSchema(name, NodeKind.Map, allowedTypes) {
            constructor(input) {
                super(input);
                if (isFlexTreeNode(input)) {
                    return createNodeProxy(input, customizable, customizable ? this : undefined);
                }
                else {
                    const flexSchema = getFlexSchema(this.constructor);
                    return createRawNodeProxy(flexSchema, input, customizable, customizable ? this : undefined);
                }
            }
        }
        // Setup map functionality
        Object.defineProperties(schema.prototype, mapStaticDispatchMap);
        return schema;
    }
    list(nameOrAllowedTypes, allowedTypes) {
        if (allowedTypes === undefined) {
            const types = nameOrAllowedTypes;
            const fullName = structuralName("List", types);
            return getOrCreate(this.structuralTypes, fullName, () => this.namedList(fullName, nameOrAllowedTypes, false));
        }
        return this.namedList(nameOrAllowedTypes, allowedTypes, true);
    }
    /**
     * Define a {@link TreeNodeSchema} for a {@link (TreeListNode:interface)}.
     *
     * @param name - Unique identifier for this schema within this factory's scope.
     */
    namedList(name, allowedTypes, customizable) {
        // This class returns a proxy from its constructor to handle numeric indexing.
        // Alternatively it could extend a normal class which gets tons of numeric properties added.
        class schema extends this.nodeSchema(name, NodeKind.List, allowedTypes) {
            get length() {
                return getSequenceField(this).length;
            }
            constructor(input) {
                super(input);
                if (isFlexTreeNode(input)) {
                    return createNodeProxy(input, customizable, customizable ? this : undefined);
                }
                else {
                    const flexSchema = getFlexSchema(this.constructor);
                    return createRawNodeProxy(flexSchema, [...input], customizable, customizable ? this : undefined);
                }
            }
        }
        // Setup list functionality
        Object.defineProperties(schema.prototype, listPrototypeProperties);
        return schema;
    }
    /**
     * Make a field optional instead of the default which is required.
     */
    optional(t) {
        return new FieldSchema(FieldKind.Optional, t);
    }
    /**
     * Function which can be used for its compile time side-effects to tweak the evaluation order of recursive types to make them compile.
     * @remarks
     * Some related information in https://github.com/microsoft/TypeScript/issues/55758.
     *
     * Also be aware that code which relies on this tends to break VSCode's IntelliSense every time anything related to that code (even comments) is edited.
     * The command `TypeScript: Restart TS Server` should fix it.
     * Sometimes this does not work: closing all open files except the schema before running the command can help.
     * Real compile errors (for example elsewhere in the file) can also cause the IntelliSense to not work correctly ever after `TypeScript: Restart TS Server`.
     * Intellisense has also shown problems when schema files with recursive types are part of a cyclic file dependency. Splitting the schema into its own file with minimal dependencies can help with this.
     *
     * @example
     * ```typescript
     * const factory = new SchemaFactory("example");
     * const recursiveReference = () => RecursiveObject;
     * factory.fixRecursiveReference(recursiveReference);
     * export class RecursiveObject extends factory.object("exampleObject", {
     * 	recursive: [recursiveReference],
     * }) {}
     * ```
     */
    fixRecursiveReference(...types) { }
}
// TODO: unify this with logic in getOrCreateNodeProxy
export function createTree(schema, data) {
    if (typeof schema === "function") {
        return new schema(data);
    }
    return schema.create(data);
}
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