"use strict";
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
 * @module Schema
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.MetaDataRegistry = exports.ClassRegistry = exports.EntityJsClassMap = void 0;
const core_bentley_1 = require("@itwin/core-bentley");
const core_common_1 = require("@itwin/core-common");
const Entity_1 = require("./Entity");
const Schema_1 = require("./Schema");
const EntityReferences_1 = require("./EntityReferences");
const assert = require("assert");
const Symbols_1 = require("./internal/Symbols");
const isGeneratedClassTag = Symbol("isGeneratedClassTag");
/** Maintains the mapping between the name of a BIS [ECClass]($ecschema-metadata) (in "schema:class" format) and the JavaScript [[Entity]] class that implements it.
 * @public
 */
class EntityJsClassMap {
    _classMap = new Map();
    /** @internal */
    has(classFullName) {
        return this._classMap.has(classFullName.toLowerCase());
    }
    /** @internal */
    get(classFullName) {
        return this._classMap.get(classFullName.toLowerCase());
    }
    /** @internal */
    set(classFullName, entityClass) {
        this._classMap.set(classFullName.toLowerCase(), entityClass);
    }
    /** @internal */
    delete(classFullName) {
        return this._classMap.delete(classFullName.toLowerCase());
    }
    /** @internal */
    clear() {
        this._classMap.clear();
    }
    /** @internal */
    [Symbol.iterator]() {
        return this._classMap[Symbol.iterator]();
    }
    /**
     * Registers a single `entityClass` defined in the specified `schema`.
     * This method registers the class globally. To register a class for a specific iModel, use [[IModelDb.jsClassMap]].
     *
     * @param entityClass - The JavaScript class that implements the BIS [ECClass](@itwin/core-common) to be registered.
     * @param schema - The schema that contains the `entityClass`.
     *
     * @throws Error if the class is already registered.
     *
     * @public
     */
    register(entityClass, schema) {
        const key = (`${schema.schemaName}:${entityClass.className}`).toLowerCase();
        if (this.has(key)) {
            const errMsg = `Class ${key} is already registered. Make sure static className member is correct on JavaScript class ${entityClass.name}`;
            core_bentley_1.Logger.logError("core-frontend.classRegistry", errMsg);
            throw new Error(errMsg);
        }
        entityClass.schema = schema;
        this.set(key, entityClass);
    }
}
exports.EntityJsClassMap = EntityJsClassMap;
/** Maintains the mapping between the name of a BIS [ECClass]($ecschema-metadata) (in "schema:class" format) and the JavaScript [[Entity]] class that implements it.
 * Applications or modules that supply their own Entity subclasses should use [[registerModule]] or [[register]] at startup
 * to establish their mappings.
 *
 * When creating custom Entity subclasses for registration, you should:
 * - Override the `className` property to match your ECClass name:
 *   ```typescript
 *   public static override get className() { return "TestElement"; }
 *   ```
 * - Do NOT override `schemaName` or `schema` - these will be wired up automatically during registration
 *
 * @public
 */
class ClassRegistry {
    static _globalClassMap = new EntityJsClassMap();
    /** @internal */
    static isNotFoundError(err) { return (err instanceof core_common_1.IModelError) && (err.errorNumber === core_bentley_1.IModelStatus.NotFound); }
    /** @internal */
    static makeMetaDataNotFoundError(className) { return new core_common_1.IModelError(core_bentley_1.IModelStatus.NotFound, `metadata not found for ${className}`); }
    /** Register a single `entityClass` defined in the specified `schema`.
     * @see [[registerModule]] to register multiple classes.
     * @public
     */
    static register(entityClass, schema) {
        this._globalClassMap.register(entityClass, schema);
    }
    /** Generate a proxy Schema for a domain that has not been registered. */
    static generateProxySchema(domain, iModel) {
        const hasBehavior = iModel.withPreparedSqliteStatement(`
      SELECT NULL FROM [ec_CustomAttribute] [c]
        JOIN [ec_schema] [s] ON [s].[Id] = [c].[ContainerId]
        JOIN [ec_class] [e] ON [e].[Id] = [c].[ClassId]
        JOIN [ec_schema] [b] ON [e].[SchemaId] = [b].[Id]
      WHERE [c].[ContainerType] = 1 AND [s].[Name] = ? AND [b].[Name] || '.' || [e].[name] = ?`, (stmt) => {
            stmt.bindString(1, domain);
            stmt.bindString(2, "BisCore.SchemaHasBehavior");
            return stmt.step() === core_bentley_1.DbResult.BE_SQLITE_ROW;
        });
        const schemaClass = class extends Schema_1.Schema {
            static get schemaName() { return domain; }
            static get missingRequiredBehavior() { return hasBehavior; }
        };
        iModel.schemaMap.registerSchema(schemaClass); // register the class before we return it.
        return schemaClass;
    }
    /** First, finds the root BisCore entity class for an entity, by traversing base classes and mixin targets (AppliesTo).
     * Then, gets its metadata and returns that.
     * @param iModel - iModel containing the metadata for this type
     * @param ecTypeQualifier - a full name of an ECEntityClass to find the root of
     * @returns the qualified full name of an ECEntityClass
     * @internal public for testing only
     */
    static getRootEntity(iModel, ecTypeQualifier) {
        const [classSchema, className] = ecTypeQualifier.split(".");
        const schemaItemJson = iModel[Symbols_1._nativeDb].getSchemaItem(classSchema, className);
        if (schemaItemJson.error)
            throw new core_common_1.IModelError(schemaItemJson.error.status, `failed to get schema item '${ecTypeQualifier}'`);
        assert(undefined !== schemaItemJson.result);
        const schemaItem = JSON.parse(schemaItemJson.result);
        if (!("appliesTo" in schemaItem) && schemaItem.baseClass === undefined) {
            return ecTypeQualifier;
        }
        // typescript doesn't understand that the inverse of the above condition is
        // ("appliesTo" in rootclassMetaData || rootClassMetaData.baseClass !== undefined)
        const parentItemQualifier = schemaItem.appliesTo ?? schemaItem.baseClass;
        return this.getRootEntity(iModel, parentItemQualifier);
    }
    /** Generate a JavaScript class from Entity metadata.
     * @param entityMetaData The Entity metadata that defines the class
     */
    // eslint-disable-next-line @typescript-eslint/no-deprecated
    static generateClassForEntity(entityMetaData, iModel) {
        const name = entityMetaData.ecclass.split(":");
        const domainName = name[0];
        const className = name[1];
        if (0 === entityMetaData.baseClasses.length) // metadata must contain a superclass
            throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadArg, `class ${entityMetaData.ecclass} has no superclass`);
        // make sure schema exists
        let schema = iModel.schemaMap.get(domainName) ?? Schema_1.Schemas.getRegisteredSchema(domainName);
        if (undefined === schema)
            schema = this.generateProxySchema(domainName, iModel); // no schema found, create it too
        const superClassFullName = entityMetaData.baseClasses[0].toLowerCase();
        const superclass = iModel.jsClassMap.get(superClassFullName) ?? this._globalClassMap.get(superClassFullName);
        if (undefined === superclass)
            throw new core_common_1.IModelError(core_bentley_1.IModelStatus.NotFound, `cannot find superclass for class ${entityMetaData.ecclass}`);
        // user defined class hierarchies may skip a class in the hierarchy, and therefore their JS base class cannot
        // be used to tell if there are any generated classes in the hierarchy
        let generatedClassHasNonGeneratedNonCoreAncestor = false;
        let currentSuperclass = superclass;
        const MAX_ITERS = 1000;
        for (let i = 0; i < MAX_ITERS; ++i) {
            if (currentSuperclass.schema.schemaName === "BisCore")
                break;
            if (!currentSuperclass.isGeneratedClass) {
                generatedClassHasNonGeneratedNonCoreAncestor = true;
                break;
            }
            // eslint-disable-next-line @typescript-eslint/no-deprecated
            const superclassMetaData = iModel.classMetaDataRegistry.find(currentSuperclass.classFullName);
            if (superclassMetaData === undefined)
                throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadSchema, `could not find the metadata for class '${currentSuperclass.name}', class metadata should be loaded by now`);
            const maybeNextSuperclass = this.getClass(superclassMetaData.baseClasses[0], iModel);
            if (maybeNextSuperclass === undefined)
                throw new core_common_1.IModelError(core_bentley_1.IModelStatus.BadSchema, `could not find the base class of '${currentSuperclass.name}', all generated classes must have a base class`);
            currentSuperclass = maybeNextSuperclass;
        }
        const generatedClass = class extends superclass {
            static get className() { return className; }
            static [isGeneratedClassTag] = true;
            static get isGeneratedClass() { return this.hasOwnProperty(isGeneratedClassTag); }
        };
        // the above creates an anonymous class. For help debugging, set the "constructor.name" property to be the same as the bisClassName.
        Object.defineProperty(generatedClass, "name", { get: () => className }); // this is the (only) way to change that readonly property.
        // a class only gets an automatic `collectReferenceIds` implementation if:
        // - it is not in the `BisCore` schema
        // - there are no ancestors with manually registered JS implementations, (excluding BisCore base classes)
        if (!generatedClassHasNonGeneratedNonCoreAncestor) {
            const navigationProps = Object.entries(entityMetaData.properties)
                .filter(([_name, prop]) => prop.isNavigation)
                // eslint-disable-next-line @typescript-eslint/no-shadow
                .map(([name, prop]) => {
                assert(prop.relationshipClass);
                const maybeMetaData = iModel[Symbols_1._nativeDb].getSchemaItem(...prop.relationshipClass.split(":"));
                assert(maybeMetaData.result !== undefined, "The nav props relationship metadata was not found");
                const relMetaData = JSON.parse(maybeMetaData.result);
                const rootClassMetaData = ClassRegistry.getRootEntity(iModel, relMetaData.target.constraintClasses[0]);
                // root class must be in BisCore so should be loaded since biscore classes will never get this
                // generated implementation
                const normalizeClassName = (clsName) => clsName.replace(".", ":");
                const rootClass = ClassRegistry.findRegisteredClass(normalizeClassName(rootClassMetaData));
                assert(rootClass, `The root class for ${prop.relationshipClass} was not in BisCore.`);
                return { name, concreteEntityType: EntityReferences_1.EntityReferences.typeFromClass(rootClass) };
            });
            Object.defineProperty(generatedClass.prototype, "collectReferenceIds", {
                value(referenceIds) {
                    // eslint-disable-next-line @typescript-eslint/dot-notation
                    const superImpl = superclass.prototype["collectReferenceIds"];
                    superImpl.call(this, referenceIds);
                    for (const navProp of navigationProps) {
                        const relatedElem = this[navProp.name]; // cast to any since subclass can have any extensions
                        if (!relatedElem || !core_bentley_1.Id64.isValid(relatedElem.id))
                            continue;
                        const referenceId = EntityReferences_1.EntityReferences.fromEntityType(relatedElem.id, navProp.concreteEntityType);
                        referenceIds.add(referenceId);
                    }
                },
                // defaults for methods on a prototype (required for sinon to stub out methods on tests)
                writable: true,
                configurable: true,
            });
        }
        // if the schema is a proxy for a domain with behavior, throw exceptions for all protected operations
        if (schema.missingRequiredBehavior) {
            const throwError = () => {
                throw new core_common_1.IModelError(core_bentley_1.IModelStatus.WrongHandler, `Schema [${domainName}] not registered, but is marked with SchemaHasBehavior`);
            };
            superclass.protectedOperations.forEach((operation) => generatedClass[operation] = throwError);
        }
        iModel.jsClassMap.register(generatedClass, schema); // register it before returning
        return generatedClass;
    }
    /** Register all of the classes found in the given module that derive from [[Entity]].
     * [[register]] will be invoked for each subclass of `Entity` exported by `moduleObj`.
     * @param moduleObj The module to search for subclasses of Entity
     * @param schema The schema that contains all of the [ECClass]($ecschema-metadata)es exported by `moduleObj`.
     */
    static registerModule(moduleObj, schema) {
        for (const thisMember in moduleObj) { // eslint-disable-line guard-for-in
            const thisClass = moduleObj[thisMember];
            if (thisClass.prototype instanceof Entity_1.Entity)
                this.register(thisClass, schema);
        }
    }
    /**
     * This function fetches the specified Entity from the imodel, generates a JavaScript class for it, and registers the generated
     * class. This function also ensures that all of the base classes of the Entity exist and are registered.
     */
    static generateClass(classFullName, iModel) {
        // eslint-disable-next-line @typescript-eslint/no-deprecated
        const metadata = iModel.classMetaDataRegistry.find(classFullName);
        if (metadata === undefined || metadata.ecclass === undefined)
            throw this.makeMetaDataNotFoundError(classFullName);
        // Make sure we have all base classes registered.
        if (metadata.baseClasses && (0 !== metadata.baseClasses.length))
            this.getClass(metadata.baseClasses[0], iModel);
        // Now we can generate the class from the classDef.
        return this.generateClassForEntity(metadata, iModel);
    }
    /** Find a registered class by classFullName.
     * @param classFullName class to find
     * @param iModel The IModel that contains the class definitions
     * @returns The Entity class or undefined
     */
    static findRegisteredClass(classFullName) {
        return this._globalClassMap.get(classFullName.toLowerCase());
    }
    /** Get the Entity class for the specified Entity className.
     * @param classFullName The full BIS class name of the Entity
     * @param iModel The IModel that contains the class definitions
     * @returns The Entity class
     */
    static getClass(classFullName, iModel) {
        const key = classFullName.toLowerCase();
        return iModel.jsClassMap.get(key) ?? this._globalClassMap.get(key) ?? this.generateClass(key, iModel);
    }
    /** Unregister a class, by name, if one is already registered.
     * This function is not normally needed, but is useful for cases where a generated *proxy* class needs to be replaced by the *real* class.
     * @param classFullName Name of the class to unregister
     * @return true if the class was unregistered
     * @internal
     */
    static unregisterClass(classFullName) { return this._globalClassMap.delete(classFullName.toLowerCase()); }
    /** Unregister all classes from a schema.
     * This function is not normally needed, but is useful for cases where a generated *proxy* schema needs to be replaced by the *real* schema.
     * @param schema Name of the schema to unregister
     * @internal
     */
    static unregisterClassesFrom(schema) {
        for (const entry of Array.from(this._globalClassMap)) {
            if (entry[1].schema === schema)
                this.unregisterClass(entry[0]);
        }
    }
}
exports.ClassRegistry = ClassRegistry;
/**
 * A cache that records the mapping between class names and class metadata.
 * @see [[IModelDb.classMetaDataRegistry]] to access the registry for a specific iModel.
 * @internal
 * @deprecated in 5.0 - will not be removed until after 2026-06-13. Please use `schemaContext` from the `iModel` instead.
 *
 * @example
 * @
 * Current Usage:
 * ```ts
 * const metaData: EntityMetaData | undefined = iModel.classMetaDataRegistry.find("SchemaName:ClassName");
 * ```
 *
 * Replacement:
 * ```ts
 * const entityMetaData: EntityClass | undefined = iModel.schemaContext.getSchemaItemSync("SchemaName.ClassName", EntityClass);
 * const relationshipMetaData: RelationshipClass | undefined = iModel.schemaContext.getSchemaItemSync("SchemaName", "ClassName", RelationshipClass);
 * ```
 */
class MetaDataRegistry {
    // eslint-disable-next-line @typescript-eslint/no-deprecated
    _registry = new Map();
    _classIdToName = new Map();
    /** Get the specified Entity metadata */
    // eslint-disable-next-line @typescript-eslint/no-deprecated
    find(classFullName) {
        return this._registry.get(classFullName.toLowerCase());
    }
    // eslint-disable-next-line @typescript-eslint/no-deprecated
    findByClassId(classId) {
        const name = this._classIdToName.get(classId);
        return undefined !== name ? this.find(name) : undefined;
    }
    /** Add metadata to the cache */
    // eslint-disable-next-line @typescript-eslint/no-deprecated
    add(classFullName, metaData) {
        const name = classFullName.toLowerCase();
        this._registry.set(name, metaData);
        this._classIdToName.set(metaData.classId, name);
    }
}
exports.MetaDataRegistry = MetaDataRegistry;
//# sourceMappingURL=ClassRegistry.js.map