"use strict";
/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.UnsafeUnknownSchema = exports.markSchemaMostDerived = exports.evaluateLazySchema = exports.areFieldSchemaEqual = exports.areImplicitFieldSchemaEqual = exports.unannotateSchemaRecord = exports.unannotateImplicitAllowedTypes = exports.normalizeAnnotatedAllowedTypes = exports.normalizeToAnnotatedAllowedType = exports.normalizeAllowedTypes = exports.normalizeFieldSchema = exports.ObjectFieldSchema = exports.FieldSchemaAlpha = exports.FieldSchema = exports.createFieldSchema = exports.getDefaultProvider = exports.extractFieldProvider = exports.isConstant = exports.getExplicitStoredKey = exports.getStoredKey = exports.FieldKind = exports.isAnnotatedAllowedType = exports.isTreeNodeSchemaClass = void 0;
const internal_1 = require("@fluidframework/core-utils/internal");
const internal_2 = require("@fluidframework/telemetry-utils/internal");
const index_js_1 = require("../util/index.js");
const index_js_2 = require("./core/index.js");
const flexList_js_1 = require("./flexList.js");
const leafNodeSchema_js_1 = require("./leafNodeSchema.js");
const treeNodeValid_js_1 = require("./treeNodeValid.js");
/**
 * Returns true if the given schema is a {@link TreeNodeSchemaClass}, or otherwise false if it is a {@link TreeNodeSchemaNonClass}.
 * @internal
 */
function isTreeNodeSchemaClass(schema) {
    return schema.constructor !== undefined;
}
exports.isTreeNodeSchemaClass = isTreeNodeSchemaClass;
/**
 * Checks if the given allowed type is annotated with {@link AllowedTypeMetadata}.
 */
function isAnnotatedAllowedType(allowedType) {
    return "metadata" in allowedType && "type" in allowedType;
}
exports.isAnnotatedAllowedType = isAnnotatedAllowedType;
/**
 * Kind of a field on an {@link TreeObjectNode}.
 * @remarks
 * More kinds may be added over time, so do not assume this is an exhaustive set.
 * See {@link FieldSchema} for where these are used, and {@link SchemaFactory} for how to create schema which use them.
 * @public
 */
var FieldKind;
(function (FieldKind) {
    /**
     * A field which can be empty or filled.
     * @remarks
     * Allows 0 or one child.
     */
    FieldKind[FieldKind["Optional"] = 0] = "Optional";
    /**
     * A field which must always be filled.
     * @remarks
     * Only allows exactly one child.
     */
    FieldKind[FieldKind["Required"] = 1] = "Required";
    /**
     * A special readonly field used for node identifier strings.
     * @remarks
     * Only allows exactly one child.
     *
     * See {@link SchemaFactory.identifier} for more details.
     */
    FieldKind[FieldKind["Identifier"] = 2] = "Identifier";
})(FieldKind || (exports.FieldKind = FieldKind = {}));
/**
 * Maps from a property key to its corresponding {@link FieldProps.key | stored key} for the provided
 * {@link ImplicitFieldSchema | field schema}.
 *
 * @remarks
 * If an explicit stored key was specified in the schema, it will be used.
 * Otherwise, the stored key is the same as the property key.
 */
function getStoredKey(propertyKey, fieldSchema) {
    return (0, index_js_1.brand)(getExplicitStoredKey(fieldSchema) ?? propertyKey);
}
exports.getStoredKey = getStoredKey;
/**
 * Gets the {@link FieldProps.key | stored key} specified by the schema, if one was explicitly specified.
 * Otherwise, returns undefined.
 */
function getExplicitStoredKey(fieldSchema) {
    return fieldSchema instanceof FieldSchema ? fieldSchema.props?.key : undefined;
}
exports.getExplicitStoredKey = getExplicitStoredKey;
/**
 * Returns true if the given {@link FieldProvider} is a {@link ConstantFieldProvider}
 */
function isConstant(fieldProvider) {
    return fieldProvider.length === 0;
}
exports.isConstant = isConstant;
function extractFieldProvider(input) {
    return input;
}
exports.extractFieldProvider = extractFieldProvider;
function getDefaultProvider(input) {
    return input;
}
exports.getDefaultProvider = getDefaultProvider;
function createFieldSchema(kind, annotatedTypes, props) {
    return createFieldSchemaPrivate(kind, annotatedTypes, props);
}
exports.createFieldSchema = createFieldSchema;
/**
 * Implementation for {@link createFieldSchema}
 */
let createFieldSchemaPrivate;
/**
 * 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.
 * Use {@link SchemaFactory} to create the FieldSchema instances, for example {@link SchemaStatics.optional}.
 * @privateRemarks
 * Public access to the constructor is removed to prevent creating expressible but unsupported (or not stable) configurations.
 * {@link createFieldSchema} can be used internally to create instances.
 *
 * @typeParam TCustomMetadata - Custom metadata properties to associate with the field.
 * See {@link FieldSchemaMetadata.custom}.
 *
 * @remarks
 * All implementations of this are actually {@link FieldSchemaAlpha} which exposes some additional alpha APIs.
 *
 * @sealed @public
 */
class FieldSchema {
    /**
     * What types of tree nodes are allowed in this field.
     * @remarks Counterpart to {@link FieldSchema.allowedTypes}, with any lazy definitions evaluated.
     */
    get allowedTypeSet() {
        return this.lazyTypes.value;
    }
    /**
     * {@inheritDoc FieldProps.metadata}
     */
    get metadata() {
        return this.props?.metadata ?? {};
    }
    /**
     * This class is `@sealed`: protected members like this constructor are for internal use only.
     * Use {@link SchemaFactory} to create the FieldSchema instances.
     */
    constructor(
    /**
     * The {@link https://en.wikipedia.org/wiki/Kind_(type_theory) | kind } of this field.
     * Determines the multiplicity, viewing and editing APIs as well as the merge resolution policy.
     */
    kind, 
    /**
     * What types of tree nodes are allowed in this field.
     */
    allowedTypes, 
    /**
     * Optional properties associated with the field.
     */
    props) {
        this.kind = kind;
        this.allowedTypes = allowedTypes;
        this.props = props;
        if (!(this instanceof FieldSchemaAlpha)) {
            throw new internal_2.UsageError("FieldSchema is @sealed: sub-classing is not allowed.");
        }
        this.lazyTypes = new internal_1.Lazy(() => normalizeAllowedTypes(this.allowedTypes));
        // TODO: optional fields should (by default) get a default provider that returns undefined, removing the need to special case them here:
        this.requiresValue =
            this.props?.defaultProvider === undefined && this.kind !== FieldKind.Optional;
    }
}
exports.FieldSchema = FieldSchema;
/**
 * {@link FieldSchema} including alpha APIs (currently {@link SimpleFieldSchema}).
 * @remarks
 * This class will go away once the alpha APIs are stable and implemented by {@link FieldSchema}.
 * @sealed @alpha
 */
class FieldSchemaAlpha extends FieldSchema {
    /**
     * Persisted metadata for this field schema.
     */
    get persistedMetadata() {
        return this.propsAlpha?.persistedMetadata;
    }
    constructor(kind, types, annotatedAllowedTypes, props) {
        super(kind, types, props);
        this.annotatedAllowedTypes = annotatedAllowedTypes;
        this.allowedTypesMetadata = (0, index_js_2.isAnnotatedAllowedTypes)(annotatedAllowedTypes)
            ? annotatedAllowedTypes.metadata
            : {};
        this.lazyAnnotatedTypes = new internal_1.Lazy(() => normalizeAnnotatedAllowedTypes(annotatedAllowedTypes));
        this.lazyIdentifiers = new internal_1.Lazy(() => new Set([...this.allowedTypeSet].map((t) => t.identifier)));
        this.propsAlpha = props;
    }
    get allowedTypesIdentifiers() {
        return this.lazyIdentifiers.value;
    }
    /**
     * What types of tree nodes are allowed in this field and their annotations.
     * @remarks Counterpart to {@link FieldSchemaAlpha.annotatedAllowedTypes}, with any lazy definitions evaluated.
     */
    get annotatedAllowedTypesNormalized() {
        return this.lazyAnnotatedTypes.value;
    }
}
exports.FieldSchemaAlpha = FieldSchemaAlpha;
(() => {
    createFieldSchemaPrivate = (kind, annotatedAllowedTypes, props) => new FieldSchemaAlpha(kind, unannotateImplicitAllowedTypes(annotatedAllowedTypes), annotatedAllowedTypes, props);
})();
/**
 * {@link FieldSchemaAlpha} including {@link SimpleObjectFieldSchema}.
 */
class ObjectFieldSchema extends FieldSchemaAlpha {
    constructor(kind, allowedTypes, annotatedTypes, props) {
        super(kind, allowedTypes, annotatedTypes, props);
        this.storedKey = props.key;
    }
}
exports.ObjectFieldSchema = ObjectFieldSchema;
/**
 * Normalizes a {@link ImplicitFieldSchema} or {@link ImplicitAnnotatedFieldSchema} to a {@link FieldSchema}.
 */
function normalizeFieldSchema(schema) {
    return schema instanceof FieldSchema
        ? schema
        : createFieldSchema(FieldKind.Required, schema);
}
exports.normalizeFieldSchema = normalizeFieldSchema;
/**
 * Normalizes a {@link ImplicitAllowedTypes} to a set of {@link TreeNodeSchema}s, by eagerly evaluating any
 * lazy schema declarations.
 *
 * @remarks Note: this must only be called after all required schemas have been declared, otherwise evaluation of
 * recursive schemas may fail.
 *
 * @internal
 */
function normalizeAllowedTypes(types) {
    const normalized = new Set();
    if ((0, index_js_1.isReadonlyArray)(types)) {
        // Types array must not be modified after it is normalized since that would result in the user of the normalized data having wrong (out of date) content.
        Object.freeze(types);
        for (const lazyType of types) {
            normalized.add(evaluateLazySchema(lazyType));
        }
    }
    else {
        normalized.add(evaluateLazySchema(types));
    }
    return normalized;
}
exports.normalizeAllowedTypes = normalizeAllowedTypes;
/**
 * Normalizes an allowed type to an {@link AnnotatedAllowedType}, by adding empty annotations if they don't already exist.
 */
function normalizeToAnnotatedAllowedType(type) {
    return isAnnotatedAllowedType(type)
        ? type
        : {
            metadata: {},
            type,
        };
}
exports.normalizeToAnnotatedAllowedType = normalizeToAnnotatedAllowedType;
/**
 * Normalizes a {@link ImplicitAnnotatedAllowedTypes} to a set of {@link AnnotatedAllowedSchema}s, by eagerly evaluating any
 * lazy schema declarations and adding empty metadata if it doesn't already exist.
 *
 * @remarks Note: this must only be called after all required schemas have been declared, otherwise evaluation of
 * recursive schemas may fail.
 */
function normalizeAnnotatedAllowedTypes(types) {
    const typesWithoutAnnotation = (0, index_js_2.isAnnotatedAllowedTypes)(types) ? types.types : types;
    const annotatedTypes = [];
    if ((0, index_js_1.isReadonlyArray)(typesWithoutAnnotation)) {
        for (const annotatedType of typesWithoutAnnotation) {
            if (isAnnotatedAllowedType(annotatedType)) {
                annotatedTypes.push({
                    type: evaluateLazySchema(annotatedType.type),
                    metadata: annotatedType.metadata,
                });
            }
            else {
                annotatedTypes.push({ type: evaluateLazySchema(annotatedType), metadata: {} });
            }
        }
    }
    else {
        if (isAnnotatedAllowedType(typesWithoutAnnotation)) {
            annotatedTypes.push({
                type: evaluateLazySchema(typesWithoutAnnotation.type),
                metadata: typesWithoutAnnotation.metadata,
            });
        }
        else {
            annotatedTypes.push({ type: evaluateLazySchema(typesWithoutAnnotation), metadata: {} });
        }
    }
    return {
        metadata: (0, index_js_2.isAnnotatedAllowedTypes)(types) ? types.metadata : {},
        types: annotatedTypes,
    };
}
exports.normalizeAnnotatedAllowedTypes = normalizeAnnotatedAllowedTypes;
/**
 * Converts an {@link ImplicitAnnotatedAllowedTypes} to an {@link ImplicitAllowedTypes}s, by removing
 * any annotations.
 * @remarks
 * This does not evaluate any lazy schemas.
 */
function unannotateImplicitAllowedTypes(types) {
    return ((0, index_js_2.isAnnotatedAllowedTypes)(types)
        ? types.types.map((allowedType) => isAnnotatedAllowedType(allowedType) ? allowedType.type : allowedType)
        : (0, index_js_1.isReadonlyArray)(types)
            ? types.map((allowedType) => isAnnotatedAllowedType(allowedType) ? allowedType.type : allowedType)
            : isAnnotatedAllowedType(types)
                ? types.type
                : types);
}
exports.unannotateImplicitAllowedTypes = unannotateImplicitAllowedTypes;
/**
 * Removes annotations from a schema record.
 */
function unannotateSchemaRecord(schemaRecord) {
    return Object.fromEntries(Object.entries(schemaRecord).map(([key, schema]) => [
        key,
        schema instanceof FieldSchema ? schema : unannotateImplicitAllowedTypes(schema),
    ]));
}
exports.unannotateSchemaRecord = unannotateSchemaRecord;
/**
 * Returns true if the given {@link ImplicitFieldSchema} are equivalent, otherwise false.
 * @remarks Two ImplicitFieldSchema are considered equivalent if all of the following are true:
 * 1. They have the same {@link FieldKind | kinds}.
 * 2. They have {@link areFieldPropsEqual | equivalent FieldProps}.
 * 3. They have the same exact set of allowed types. The allowed types must be (respectively) reference equal.
 *
 * For example, comparing an ImplicitFieldSchema that is a {@link TreeNodeSchema} to an ImplicitFieldSchema that is a {@link FieldSchema}
 * will return true if they are the same kind, the FieldSchema has exactly one allowed type (the TreeNodeSchema), and they have equivalent FieldProps.
 */
function areImplicitFieldSchemaEqual(a, b) {
    return areFieldSchemaEqual(normalizeFieldSchema(a), normalizeFieldSchema(b));
}
exports.areImplicitFieldSchemaEqual = areImplicitFieldSchemaEqual;
/**
 * Returns true if the given {@link FieldSchema} are equivalent, otherwise false.
 * @remarks Two FieldSchema are considered equivalent if all of the following are true:
 * 1. They have the same {@link FieldKind | kinds}.
 * 2. They have {@link areFieldPropsEqual | equivalent FieldProps}.
 * 3. They have the same exact set of allowed types. The allowed types must be reference equal.
 */
function areFieldSchemaEqual(a, b) {
    if (a === b) {
        return true;
    }
    if (a.kind !== b.kind) {
        return false;
    }
    if (!areFieldPropsEqual(a.props, b.props)) {
        return false;
    }
    return (0, index_js_1.compareSets)({ a: a.allowedTypeSet, b: b.allowedTypeSet });
}
exports.areFieldSchemaEqual = areFieldSchemaEqual;
/**
 * Returns true if the given {@link FieldProps} are equivalent, otherwise false.
 * @remarks FieldProps are considered equivalent if their keys and default providers are reference equal, and their metadata are {@link areMetadataEqual | equivalent}.
 */
function areFieldPropsEqual(a, b) {
    if (a === b) {
        return true;
    }
    if (a?.key !== b?.key || a?.defaultProvider !== b?.defaultProvider) {
        return false;
    }
    if (!areMetadataEqual(a?.metadata, b?.metadata)) {
        return false;
    }
    return true;
}
/**
 * Returns true if the given {@link FieldSchemaMetadata} are equivalent, otherwise false.
 * @remarks FieldSchemaMetadata are considered equivalent if their custom data and descriptions are (respectively) reference equal.
 */
function areMetadataEqual(a, b) {
    if (a === b) {
        return true;
    }
    return (Object.is(a?.custom, b?.custom) &&
        a?.description === b?.description &&
        arePersistedMetadataEqual(a?.persistedMetadata, b?.persistedMetadata));
}
/**
 * Returns true if the given persisted metadata fields are equivalent, otherwise false.
 * @remarks
 * Currently only handles shallow equality in the case where the keys are in the same order. This is acceptable for current use cases.
 */
function arePersistedMetadataEqual(a, b) {
    if (Object.is(a, b)) {
        return true;
    }
    if (a === undefined || b === undefined) {
        return false;
    }
    // Note that the key order matters. If `a` and `b` have the same content but the keys are in a different order,
    // this method will return false.
    const aStringified = JSON.stringify(a);
    const bStringified = JSON.stringify(b);
    return aStringified === bStringified;
}
const cachedLazyItem = new WeakMap();
/**
 * Returns the schema referenced by the {@link LazyItem}.
 * @remarks
 * Caches results to handle {@link LazyItem}s which compute their resulting schema.
 * @alpha
 */
function evaluateLazySchema(value) {
    const evaluatedSchema = (0, flexList_js_1.isLazy)(value)
        ? (0, index_js_1.getOrCreate)(cachedLazyItem, value, value)
        : value;
    if (evaluatedSchema === undefined) {
        throw new internal_2.UsageError(`Encountered an undefined schema. This could indicate that some referenced schema has not yet been instantiated.`);
    }
    markSchemaMostDerived(evaluatedSchema);
    return evaluatedSchema;
}
exports.evaluateLazySchema = evaluateLazySchema;
/**
 * Indicates that the provided schema is the "most derived" version in its class hierarchy.
 *
 * @param oneTimeInitialize - If true this runs {@link TreeNodeValid.oneTimeInitialize} which does even more initialization and validation.
 * `oneTimeInitialize` can't safely be run until all transitively referenced schema are defined, so which cases can safely use it are more limited.
 * When legal for the caller to set this to true, it is preferred, but it is often not safe due to possible forward references.
 * @remarks
 * See {@link MostDerivedData} and {@link SchemaFactory} for details on what a "most derived" schema is and why it matters.
 *
 * This is a helper for invoking {@link TreeNodeValid.markMostDerived} for {@link TreeNodeSchema}.
 *
 * Calling this helps with error messages about invalid schema usage (See {@link SchemaFactory} for the rules, some of which this helps validate).
 * Typically this should be called for each schema as early as practical to improve error reporting for invalid usages of schema
 * (using two different schema derived from the same {@link SchemaFactory} produced base class).
 *
 * Note that construction of actual {@link TreeNode} instances or use of a schema transitively in a {@link TreeViewConfiguration} already do this,
 * so any calls to this that is unconditionally after that point for the given schema is not needed.
 * Instead most usages of this should be from those cases, and from miscellaneous cases where a schema is passed into an public API where theoretically someone could accidentally
 * pass in a base class of a schema instead of the most derived one.
 */
function markSchemaMostDerived(schema, oneTimeInitialize = false) {
    if (schema instanceof leafNodeSchema_js_1.LeafNodeSchema) {
        return;
    }
    if (!(0, index_js_2.inPrototypeChain)(schema, treeNodeValid_js_1.TreeNodeValid)) {
        // Use JSON.stringify to quote and escape identifier string.
        throw new internal_2.UsageError(`Schema for ${JSON.stringify(schema.identifier)} does not extend a SchemaFactory generated class. This is invalid.`);
    }
    const schemaValid = schema;
    if (oneTimeInitialize) {
        schemaValid.oneTimeInitialize();
    }
    else {
        schemaValid.markMostDerived();
    }
}
exports.markSchemaMostDerived = markSchemaMostDerived;
/**
 * {@inheritdoc (UnsafeUnknownSchema:type)}
 * @alpha
 */
exports.UnsafeUnknownSchema = Symbol("UnsafeUnknownSchema");
//# sourceMappingURL=schemaTypes.js.map