/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { ErasedTypeImplementation, } from "@fluidframework/core-interfaces/internal";
import { Lazy } from "@fluidframework/core-utils/internal";
import { UsageError } from "@fluidframework/telemetry-utils/internal";
import { getOrCreate, isReadonlyArray, } from "../../util/index.js";
import { isLazy } from "./flexList.js";
import { NodeKind, } from "./treeNodeSchema.js";
import { schemaAsTreeNodeValid } from "./treeNodeValid.js";
/**
 * Checks if the input is an {@link AnnotatedAllowedTypes}.
 */
export function isAnnotatedAllowedTypes(allowedTypes) {
    checkForUninitializedSchema(allowedTypes);
    return allowedTypes instanceof AnnotatedAllowedTypesInternal;
}
/**
 * The implementation of {@link AnnotatedAllowedTypes}. Also implements {@link AllowedTypesFull}.
 * @remarks
 * Due to TypeScript limitations, this class cannot directly state it implements {@link AllowedTypesFull}.
 * As a workaround for that, the static `create` method returns the intersection type.
 */
export class AnnotatedAllowedTypesInternal extends ErasedTypeImplementation {
    constructor(types, metadata = {}) {
        super();
        this.types = types;
        this.metadata = metadata;
        this.unannotatedTypes = types.map((type) => type.type);
        // Since the array has been copied, mutations to it will not be handled correctly so freeze it.
        // Support for such mutations could be added at a later date by making more things lazy.
        Object.freeze(this.types);
        Object.freeze(this.unannotatedTypes);
        this.isLazy = false;
        for (const type of this.unannotatedTypes) {
            if (isLazy(type)) {
                this.isLazy = true;
            }
            else {
                markSchemaMostDerived(type);
            }
        }
        const proxy = AnnotatedAllowedTypesInternal.proxy(this);
        this.lazyEvaluate = new Lazy(() => {
            const annotated = this.isLazy
                ? AnnotatedAllowedTypesInternal.create(this.types.map((type) => ({
                    ...type,
                    type: evaluateLazySchema(type.type),
                })), this.metadata)
                : proxy;
            return {
                annotated,
                set: new Set(annotated.unannotatedTypes),
                identifiers: new Set(annotated.unannotatedTypes.map((t) => t.identifier)),
            };
        });
        return proxy;
    }
    evaluate() {
        return this.lazyEvaluate.value.annotated;
    }
    evaluateSet() {
        return this.lazyEvaluate.value.set;
    }
    evaluateIdentifiers() {
        return this.lazyEvaluate.value.identifiers;
    }
    /**
     * Get the {@link SimpleAllowedTypeAttributes} version of the allowed types set.
     */
    static evaluateSimpleAllowedTypes(annotatedAllowedTypes) {
        const simpleAllowedTypes = new Map();
        for (const type of annotatedAllowedTypes.evaluate().types) {
            simpleAllowedTypes.set(type.type.identifier, {
                isStaged: type.metadata.stagedSchemaUpgrade ?? false,
            });
        }
        return simpleAllowedTypes;
    }
    static [Symbol.hasInstance](value) {
        return ErasedTypeImplementation[Symbol.hasInstance].call(this, value);
    }
    static narrow(value) {
        if (!ErasedTypeImplementation[Symbol.hasInstance].call(this, value)) {
            throw new TypeError("Invalid AnnotatedAllowedTypes instance");
        }
    }
    static proxy(result) {
        const proxy = new Proxy(result, {
            set: () => {
                throw new UsageError("AnnotatedAllowedTypes is immutable");
            },
            get: (target, property, receiver) => {
                // Hide common array editing methods.
                if (property === "push" || property === "pop") {
                    return undefined;
                }
                // Forward array lookup and array methods to the unannotated types array.
                if (property in target.unannotatedTypes) {
                    return Reflect.get(target.unannotatedTypes, property);
                }
                // Forward anything else to target.
                return Reflect.get(target, property);
            },
            has: (target, property) => {
                if (property in target.unannotatedTypes) {
                    return true;
                }
                // Forward anything else to target.
                return property in target;
            },
            ownKeys: (target) => {
                const targetKeys = Reflect.ownKeys(target);
                const unannotatedTypesKeys = Reflect.ownKeys(target.unannotatedTypes);
                return [...unannotatedTypesKeys, ...targetKeys];
            },
            getOwnPropertyDescriptor: (target, property) => {
                if (Object.prototype.hasOwnProperty.call(target.unannotatedTypes, property)) {
                    const inner = Object.getOwnPropertyDescriptor(target.unannotatedTypes, property);
                    return {
                        ...inner,
                        // Since these properties are not on the target, make them non-configurable to confirm with proxy invariants.
                        configurable: true,
                        writable: false,
                    };
                }
                else {
                    const inner = Object.getOwnPropertyDescriptor(target, property);
                    return {
                        ...inner,
                        writable: false,
                        // Allow only array entries to be enumerable.
                        enumerable: false,
                    };
                }
            },
        });
        return proxy;
    }
    static create(types, metadata = {}) {
        const result = new AnnotatedAllowedTypesInternal(types, metadata);
        return result;
    }
    static createUnannotated(types, metadata = {}) {
        Object.freeze(types);
        const annotatedTypes = types.map(normalizeToAnnotatedAllowedType);
        const result = AnnotatedAllowedTypesInternal.create(annotatedTypes, metadata);
        return result;
    }
    static createMixed(types, metadata = {}) {
        Object.freeze(types);
        const annotatedTypes = types.map(normalizeToAnnotatedAllowedType);
        const result = AnnotatedAllowedTypesInternal.create(annotatedTypes, metadata);
        return result;
    }
}
/**
 * Checks if the given allowed type is annotated with {@link AllowedTypeMetadata}.
 */
export function isAnnotatedAllowedType(allowedType) {
    checkForUninitializedSchema(allowedType);
    // Class based schema, and lazy schema references report type "function": filtering them out with typeof makes narrowing based on members mostly safe
    return typeof allowedType === "object" && "metadata" in allowedType && "type" in allowedType;
}
/**
 * Package internal {@link SchemaUpgrade} construction API.
 */
export let createSchemaUpgrade;
/**
 * Unique token used to upgrade schemas and determine if a particular upgrade has been completed.
 * @remarks
 * Create using {@link SchemaStaticsBeta.staged}.
 * @privateRemarks
 * TODO:#38722 implement runtime schema upgrades.
 * Until then, the class purely behaves mostly as a placeholder.
 * TODO: Consider allowing users to store a name for the upgrade to use in error messages.
 * @sealed @beta
 */
export class SchemaUpgrade {
    constructor() { }
}
(() => {
    createSchemaUpgrade = () => new SchemaUpgrade();
})();
/**
 * Normalizes an {@link ImplicitAllowedTypes} to an {@link AllowedTypesFull}.
 * @alpha
 */
export function normalizeAllowedTypes(types) {
    return normalizeAllowedTypesInternal(types);
}
/**
 * Normalizes an allowed type to an {@link AnnotatedAllowedType}, by adding empty annotations if they don't already exist.
 */
export function normalizeToAnnotatedAllowedType(type) {
    return isAnnotatedAllowedType(type)
        ? type
        : {
            metadata: {},
            type,
        };
}
/**
 * See note inside {@link normalizeAllowedTypesInternal}.
 */
const cachedNormalize = new WeakMap();
/**
 * Normalizes allowed types to an {@link AllowedTypesFullInternal}.
 */
export function normalizeAllowedTypesInternal(type) {
    if (isAnnotatedAllowedTypes(type)) {
        return type;
    }
    // This caching accomplishes two things:
    // 1. It avoids redundant computations for the same input.
    // 2. It provides a stable object identity for the output in case the input is normalized twice and other systems (such as unhydrated contexts) are cached based on the object identity of the output.
    // It is this second case which is the more important since creating the AnnotatedAllowedTypesInternal is rather cheap.
    // Adding this cache improved the performance of the "large recursive union" test (which mostly just constructs a TreeConfiguration) by ~5 times.
    // This cache is strictly a performance optimization: it is not required for correctness.
    return getOrCreate(cachedNormalize, type, () => {
        const inputArray = isReadonlyArray(type) ? type : [type];
        Object.freeze(inputArray);
        const annotatedTypes = inputArray.map(normalizeToAnnotatedAllowedType);
        return AnnotatedAllowedTypesInternal.create(annotatedTypes);
    });
}
/**
 * Normalizes an {@link ImplicitAllowedTypes} to an {@link AllowedTypesFullInternalEvaluated} 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.
 */
export function normalizeAndEvaluateAnnotatedAllowedTypes(types) {
    return normalizeAllowedTypesInternal(types).evaluate();
}
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
 */
export function evaluateLazySchema(value) {
    const evaluatedSchema = isLazy(value)
        ? getOrCreate(cachedLazyItem, value, value)
        : value;
    checkForUninitializedSchema(evaluatedSchema);
    markSchemaMostDerived(evaluatedSchema);
    return evaluatedSchema;
}
/**
 * Throws a UsageError if the provided schema is undefined, most likely due to being used before it was initialized.
 */
export function checkForUninitializedSchema(schema) {
    if (schema === undefined) {
        throw new UsageError(`Encountered an undefined schema. This could indicate that some referenced schema has not yet been instantiated. Consider using a lazy schema reference (like "() => schema") or delaying the evaluation of the lazy reference if one is already being used.`);
    }
}
/**
 * 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.
 */
export function markSchemaMostDerived(schema, oneTimeInitialize = false) {
    // Leaf schema are not classes, and thus do not need to be marked as most derived.
    if (schema.kind === NodeKind.Leaf) {
        return;
    }
    const schemaValid = schemaAsTreeNodeValid(schema);
    if (oneTimeInitialize) {
        schemaValid.oneTimeInitialize();
    }
    else {
        schemaValid.markMostDerived();
    }
}
//# sourceMappingURL=allowedTypes.js.map