/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { assert, Lazy } from "@fluidframework/core-utils/internal";
import { EmptyKey, LeafNodeStoredSchema, MapNodeStoredSchema, ObjectNodeStoredSchema, } from "../../core/index.js";
import { compareSets, mapIterable, oneFromSet, } from "../../util/index.js";
import { FieldKinds } from "../default-schema/index.js";
import { objectToMapTyped } from "./typeUtils.js";
{
}
/**
 * T must extend TreeSchemaSpecification.
 * This can not be enforced using TypeScript since doing so breaks recursive type support.
 * See note on SchemaBuilder.fieldRecursive.
 * @internal
 */
export class TreeNodeSchemaBase {
    constructor(builder, name, info, stored) {
        this.builder = builder;
        this.name = name;
        this.info = info;
        this.stored = stored;
    }
}
/**
 * @internal
 */
export class FlexMapNodeSchema extends TreeNodeSchemaBase {
    get mapFields() {
        return this.info;
    }
    static create(builder, name, specification) {
        return new FlexMapNodeSchema(builder, name, specification, new MapNodeStoredSchema(specification.stored));
    }
    getFieldSchema(field) {
        return this.info;
    }
}
/**
 * @internal
 */
export class LeafNodeSchema extends TreeNodeSchemaBase {
    get leafValue() {
        return this.info;
    }
    static create(builder, name, specification) {
        return new LeafNodeSchema(builder, name, specification, new LeafNodeStoredSchema(specification));
    }
    getFieldSchema(field) {
        return FlexFieldSchema.empty;
    }
}
/**
 * @internal
 */
export class FlexObjectNodeSchema extends TreeNodeSchemaBase {
    static create(builder, name, specification) {
        const objectNodeFieldsObject = normalizeStructFields(specification);
        const objectNodeFields = objectToMapTyped(objectNodeFieldsObject);
        return new FlexObjectNodeSchema(builder, name, specification, objectNodeFieldsObject, objectNodeFields);
    }
    constructor(builder, name, info, objectNodeFieldsObject, 
    // Allows reading fields through the normal map.
    // Stricter typing caused Specification to no longer be covariant, so has been removed.
    objectNodeFields) {
        const fields = mapIterable(objectNodeFields, ([k, v]) => [k, v.stored]);
        super(builder, name, info, new ObjectNodeStoredSchema(new Map(fields)));
        this.objectNodeFieldsObject = objectNodeFieldsObject;
        this.objectNodeFields = objectNodeFields;
    }
    getFieldSchema(field) {
        return this.objectNodeFields.get(field) ?? FlexFieldSchema.empty;
    }
}
/**
 * TODO: remove or replace (or subclass) this with more specific types, like "List".
 * @internal
 */
export class FlexFieldNodeSchema extends TreeNodeSchemaBase {
    static create(builder, name, specification) {
        return new FlexFieldNodeSchema(builder, name, specification);
    }
    constructor(builder, name, info) {
        const objectNodeFields = new Map([[EmptyKey, info.stored]]);
        super(builder, name, info, new ObjectNodeStoredSchema(objectNodeFields));
    }
    getFieldSchema(field) {
        return (field ?? EmptyKey) === EmptyKey
            ? this.info
            : FlexFieldSchema.empty;
    }
}
function normalizeStructFields(fields) {
    const out = {};
    // eslint-disable-next-line no-restricted-syntax
    for (const key in fields) {
        if (Object.prototype.hasOwnProperty.call(fields, key)) {
            const element = fields[key];
            out[key] = normalizeField(element);
        }
    }
    return out;
}
function normalizeField(t) {
    if (t === undefined) {
        return FlexFieldSchema.empty;
    }
    assert(t instanceof FlexFieldSchema, 0x6ae /* invalid TreeFieldSchema */);
    return t;
}
/**
 * Allow any node (as long as it meets the schema for its own type).
 * @internal
 */
export const Any = "Any";
/**
 * Checks if an {@link FlexAllowedTypes} is {@link (Any:type)}.
 * @internal
 */
export function allowedTypesIsAny(t) {
    return t.length === 1 && t[0] === Any;
}
/**
 * All policy for a specific field,
 * including functionality that does not have to be kept consistent across versions or deterministic.
 *
 * This can include policy for how to use this schema for "view" purposes, and well as how to expose editing APIs.
 *
 * @remarks
 * `Types` here must extend `AllowedTypes`, but this cannot be enforced with an "extends" clause: see {@link Unenforced} for details.
 *
 * @typeParam TKind - The kind of field.
 * @typeParam TTypes - The types allowed by the field.
 *
 * @sealed
 * @internal
 */
export class FlexFieldSchema {
    /**
     * Constructs a TreeFieldSchema.
     * @privateRemarks
     * Alias for the constructor, but with extends clause for the `Types` parameter that {@link FlexFieldSchema} can not have (due to recursive type issues).
     */
    static create(kind, allowedTypes) {
        return new FlexFieldSchema(kind, allowedTypes);
    }
    /**
     * Constructs a TreeFieldSchema, but missing the extends clause which breaks most recursive types.
     * @remarks
     * `Types` here must extend `AllowedTypes`, but this cannot be enforced with an "extends" clause: see {@link Unenforced} for details.
     * Prefer {@link FlexFieldSchema.create} when possible.
     */
    static createUnsafe(kind, allowedTypes) {
        return new FlexFieldSchema(kind, allowedTypes);
    }
    /**
     * @param kind - The {@link https://en.wikipedia.org/wiki/Kind_(type_theory) | kind} of this field.
     * Determine the multiplicity, viewing and editing APIs as well as the merge resolution policy.
     * @param allowedTypes - What types of tree nodes are allowed in this field.
     */
    constructor(kind, allowedTypes) {
        this.kind = kind;
        this.allowedTypes = allowedTypes;
        // Since this class can't have the desired extends clause, do some extra runtime validation:
        assert(Array.isArray(allowedTypes), 0x7bc /* Invalid allowedTypes */);
        for (const allowedType of allowedTypes) {
            if (allowedType === Any) {
                assert(allowedTypes.length === 1, 0x7bd /* Invalid Any in allowedTypes */);
            }
            else if (typeof allowedType !== "function") {
                assert(allowedType instanceof TreeNodeSchemaBase, 0x7be /* Invalid entry in allowedTypes */);
            }
        }
        const lazy = new Lazy(() => {
            const input = this.allowedTypes;
            const schema = allowedTypesSchemaSet(input);
            return {
                names: allowedTypesToTypeSet(input),
                schema,
                monomorphicChildType: schema !== Any ? oneFromSet(schema) : undefined,
            };
        });
        this.lazyTypes = lazy;
        this.stored = {
            kind: this.kind.identifier,
            get types() {
                return lazy.value.names;
            },
        };
    }
    /**
     * Types which are allowed in this field (by {@link TreeNodeSchemaIdentifier}), in a format optimized for stored schema.
     * This is the same set of types in {@link FlexFieldSchema.allowedTypes}, just in a different format.
     */
    get types() {
        return this.lazyTypes.value.names;
    }
    /**
     * Types which are allowed in this field.
     * This is the same set of types in {@link FlexFieldSchema.allowedTypes}, just as a set with laziness removed.
     * @privateRemarks
     * TODO:
     * 3 ways to access the allowed types are now exposed.
     * Reducing this and/or renaming the more friendly options to take the shorter name (`types`)
     * would be a good idea.
     */
    get allowedTypeSet() {
        return this.lazyTypes.value.schema;
    }
    /**
     * If exactly one type of child is allowed in this field, it is provided here.
     * @remarks
     * Some code paths (like unboxing and compressed tree encoding) special case schema with exactly one allowed type.
     * This field allows for simple and optimized handling of this case.
     */
    get monomorphicChildType() {
        return this.lazyTypes.value.monomorphicChildType;
    }
    /**
     * Compare this schema to another.
     *
     * @returns true iff the schema are identical.
     */
    equals(other) {
        if (other.kind !== this.kind) {
            return false;
        }
        if (other.types === undefined) {
            return this.types === undefined;
        }
        if (this.types === undefined) {
            return false;
        }
        return compareSets({
            a: this.types,
            b: other.types,
            aExtra: () => false,
            bExtra: () => false,
        });
    }
}
/**
 * Schema for a field which must always be empty.
 */
FlexFieldSchema.empty = FlexFieldSchema.create(FieldKinds.forbidden, []);
/**
 * Convert {@link FlexAllowedTypes} to {@link TreeTypeSet}.
 * @internal
 */
export function allowedTypesSchemaSet(t) {
    if (allowedTypesIsAny(t)) {
        return Any;
    }
    const list = t.map((value) => {
        if (typeof value === "function") {
            return value();
        }
        return value;
    });
    return new Set(list);
}
/**
 * Convert {@link FlexAllowedTypes} to {@link TreeTypeSet}.
 * @internal
 */
export function allowedTypesToTypeSet(t) {
    const list = allowedTypesSchemaSet(t);
    if (list === Any) {
        return undefined;
    }
    const names = Array.from(list, (type) => {
        assert(type instanceof TreeNodeSchemaBase, 0x7bf /* invalid allowed type */);
        return type.name;
    });
    return new Set(names);
}
/**
 * Converts a {@link FlexTreeSchema} into a {@link TreeStoredSchema}.
 */
export function intoStoredSchema(treeSchema) {
    return {
        rootFieldSchema: treeSchema.rootFieldSchema.stored,
        ...intoStoredSchemaCollection(treeSchema),
    };
}
/**
 * Converts a {@link SchemaCollection} into a {@link StoredSchemaCollection}.
 */
export function intoStoredSchemaCollection(treeSchema) {
    return {
        nodeSchema: new Map(mapIterable(treeSchema.nodeSchema.entries(), ([k, v]) => [k, v.stored])),
    };
}
// These schema type narrowing functions are preferred over `instanceof` due to being easier to migrate to class based schema.
/**
 * Checks if a {@link FlexTreeNodeSchema} is a {@link FlexMapNodeSchema}.
 * @internal
 */
export function schemaIsMap(schema) {
    return schema instanceof FlexMapNodeSchema;
}
/**
 * Checks if a {@link FlexTreeNodeSchema} is a {@link LeafNodeSchema}.
 * @internal
 */
export function schemaIsLeaf(schema) {
    return schema instanceof LeafNodeSchema;
}
/**
 * Checks if a {@link FlexTreeNodeSchema} is a {@link FlexFieldNodeSchema}.
 * @internal
 */
export function schemaIsFieldNode(schema) {
    return schema instanceof FlexFieldNodeSchema;
}
/**
 * Checks if a {@link FlexTreeNodeSchema} is a {@link FlexObjectNodeSchema}.
 * @internal
 */
export function schemaIsObjectNode(schema) {
    return schema instanceof FlexObjectNodeSchema;
}
//# sourceMappingURL=typedTreeSchema.js.map