/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { assert } from "@fluidframework/core-utils";
import { Any, FieldKinds, normalizeField, SchemaBuilderBase, FieldNodeSchema, TreeNodeSchemaBase, } from "../feature-libraries";
import { addFactory } from "../simple-tree";
import { getOrCreate } from "../util";
import { leaf } from "./leafDomain";
/**
 * Builds schema libraries, and the schema within them.
 *
 * @remarks
 * Fields, when inferred from {@link ImplicitFieldSchema}, default to the `Required` {@link FieldKind} (except for in Maps, which default to `Optional`).
 * Implicitly includes `leaf` schema library by default.
 *
 * This type has some built in defaults which impact compatibility.
 * This includes which {@link FieldKind}s it uses.
 * To ensure that these defaults can be updated without compatibility issues,
 * this class is versioned: the number in its name indicates its compatibility,
 * and if its defaults are changed to ones that would not be compatible with a version of the application using the previous versions,
 * this number will be updated to make it impossible for an app to implicitly do a compatibility breaking change by updating this package.
 * Major package version updates are allowed to break API compatibility, but must not break content compatibility unless a corresponding code change is made in the app to opt in.
 *
 * @privateRemarks
 * TODO: Maybe rename to DefaultSchemaBuilder1 because of the versioning implications above.
 * Same applies to SchemaBuilder.
 * TODO: figure out a way to link `leaf` above without breaking API Extractor.
 * @sealed @alpha
 */
export class SchemaBuilder extends SchemaBuilderBase {
    constructor(options) {
        super(FieldKinds.required, {
            ...options,
            libraries: [...(options.libraries ?? []), leaf.library],
        });
        this.structuralTypes = new Map();
        /**
         * Define a schema for an {@link FieldKinds.optional|optional field}.
         * @remarks
         * Shorthand or passing `FieldKinds.optional` to {@link TreeFieldSchema.create}.
         *
         * Since this creates a {@link TreeFieldSchema} (and not a {@link TreeNodeSchema}), the resulting schema is structurally typed, and not impacted by the {@link SchemaBuilderBase.scope}:
         * therefore this method is the same as the static version.
         */
        this.optional = SchemaBuilder.optional;
        /**
         * Define a schema for a {@link FieldKinds.required|required field}.
         * @remarks
         * Shorthand or passing `FieldKinds.required` to {@link TreeFieldSchema.create}.
         * Note that `FieldKinds.required` is the current default field kind, so APIs accepting {@link ImplicitFieldSchema}
         * can be passed the `allowedTypes` and will implicitly wrap it up in a {@link FieldKinds.required|required field}.
         *
         * Since this creates a {@link TreeFieldSchema} (and not a {@link TreeNodeSchema}), the resulting schema is structurally typed, and not impacted by the {@link SchemaBuilderBase.scope}:
         * therefore this method is the same as the static version.
         */
        this.required = SchemaBuilder.required;
        /**
         * Define a schema for a {@link FieldKinds.sequence|sequence field}.
         * @remarks
         * Shorthand or passing `FieldKinds.sequence` to {@link TreeFieldSchema.create}.
         *
         * Since this creates a {@link TreeFieldSchema} (and not a {@link TreeNodeSchema}), the resulting schema is structurally typed, and not impacted by the {@link SchemaBuilderBase.scope}:
         * therefore this method is the same as the static version.
         */
        this.sequence = SchemaBuilder.sequence;
        /**
         * {@link leaf.number}
         */
        this.number = leaf.number;
        /**
         * {@link leaf.boolean}
         */
        this.boolean = leaf.boolean;
        /**
         * {@link leaf.string}
         */
        this.string = leaf.string;
        /**
         * {@link leaf.handle}
         */
        this.handle = leaf.handle;
        /**
         * {@link leaf.null}
         */
        this.null = leaf.null;
    }
    object(name, t) {
        const schema = super.object(name, t);
        return addFactory(schema);
    }
    objectRecursive(name, t) {
        return this.object(name, t);
    }
    list(nameOrAllowedTypes, allowedTypes) {
        if (allowedTypes === undefined) {
            const types = nameOrAllowedTypes;
            const fullName = structuralName("List", types);
            return getOrCreate(this.structuralTypes, fullName, () => this.namedList(fullName, nameOrAllowedTypes));
        }
        return this.namedList(nameOrAllowedTypes, allowedTypes);
    }
    /**
     * Define (and add to this library) a {@link FieldNodeSchema} for a {@link (TreeListNode:interface)}.
     *
     * The name must be unique among all TreeNodeSchema in the the document schema.
     *
     * @privateRemarks
     * TODO: If A custom "List" API is added as a subtype of {@link FieldNode}, this would opt into that.
     */
    namedList(name, allowedTypes) {
        const schema = FieldNodeSchema.create(this, this.scoped(name), this.sequence(allowedTypes));
        this.addNodeSchema(schema);
        return schema;
    }
    map(nameOrAllowedTypes, allowedTypes) {
        if (allowedTypes === undefined) {
            const types = nameOrAllowedTypes;
            const fullName = structuralName("Map", types);
            return getOrCreate(this.structuralTypes, fullName, () => super.map(fullName, normalizeField(nameOrAllowedTypes, FieldKinds.optional)));
        }
        return super.map(nameOrAllowedTypes, normalizeField(allowedTypes, FieldKinds.optional));
    }
    /**
     * 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 (or the "recursive" SchemaBuilder methods 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: the exact cause has not been confirmed but if you have the file open multiple times (for example in both sides of a window split into two columns): closing the extra copy may help.
     * Focusing the file with the errors before running `TypeScript: Restart TS Server` can also 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`.
     */
    fixRecursiveReference(...types) { }
}
/**
 * Define a schema for an {@link FieldKinds.optional|optional field}.
 * @remarks
 * Shorthand or passing `FieldKinds.optional` to {@link TreeFieldSchema.create}.
 *
 * This method is also available as an instance method on {@link SchemaBuilder}.
 */
SchemaBuilder.optional = fieldHelper(FieldKinds.optional);
/**
 * Define a schema for a {@link FieldKinds.required|required field}.
 * @remarks
 * Shorthand or passing `FieldKinds.required` to {@link TreeFieldSchema.create}.
 *
 * This method is also available as an instance method on {@link SchemaBuilder}.
 */
SchemaBuilder.required = fieldHelper(FieldKinds.required);
/**
 * Define a schema for a {@link FieldKinds.sequence|sequence field}.
 * @remarks
 * Shorthand or passing `FieldKinds.sequence` to {@link TreeFieldSchema.create}.
 *
 * This method is also available as an instance method on {@link SchemaBuilder}
 */
SchemaBuilder.sequence = fieldHelper(FieldKinds.sequence);
/**
 * Returns a wrapper around SchemaBuilder.field for a specific FieldKind.
 */
function fieldHelper(kind) {
    return (allowedTypes) => SchemaBuilder.field(kind, allowedTypes);
}
export function structuralName(collectionName, allowedTypes) {
    let inner;
    if (allowedTypes === Any) {
        inner = "Any";
    }
    else if (allowedTypes instanceof TreeNodeSchemaBase) {
        return structuralName(collectionName, [allowedTypes]);
    }
    else {
        assert(Array.isArray(allowedTypes), 0x7c7 /* Types should be an array */);
        const names = allowedTypes.map((t) => {
            // Ensure that lazy types (functions) don't slip through here.
            assert(t instanceof TreeNodeSchemaBase, 0x7c8 /* invalid type provided */);
            // Adding `as string` here would silence the linter, but make this code less type safe (since if this became not a string, it would still build).
            // Thus we explicitly check that this satisfies string.
            // This would best be done by appending `satisfies string`, but the linter also rejects this.
            // Therefor introducing a variable to do the same thing as `satisfies string` but such that the linter can understand:
            const name = t.name;
            // Just incase the compiler and linter really are onto something and this might sometimes not be a string, validate it:
            assert(typeof name === "string", 0x7c9 /* Name should be a string */);
            return name;
        });
        // 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=schemaBuilder.js.map