/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
 * @module ECDb
 */
import { assert, DbResult, Logger, OpenMode } from "@itwin/core-bentley";
import { ECSqlReader, IModelError } from "@itwin/core-common";
import { BackendLoggerCategory } from "./BackendLoggerCategory";
import { ConcurrentQuery } from "./ConcurrentQuery";
import { ECSqlStatement, ECSqlWriteStatement } from "./ECSqlStatement";
import { IModelNative } from "./internal/NativePlatform";
import { SqliteStatement, StatementCache } from "./SqliteStatement";
import { _nativeDb } from "./internal/Symbols";
const loggerCategory = BackendLoggerCategory.ECDb;
/** Modes for how to open [ECDb]($backend) files.
 * @public
 */
export var ECDbOpenMode;
(function (ECDbOpenMode) {
    ECDbOpenMode[ECDbOpenMode["Readonly"] = 0] = "Readonly";
    ECDbOpenMode[ECDbOpenMode["ReadWrite"] = 1] = "ReadWrite";
    /** Opens the file read-write and upgrades the file if necessary to the latest file format version. */
    ECDbOpenMode[ECDbOpenMode["FileUpgrade"] = 2] = "FileUpgrade";
})(ECDbOpenMode || (ECDbOpenMode = {}));
/** An ECDb file
 * @public
 */
export class ECDb {
    _nativeDb;
    // eslint-disable-next-line @typescript-eslint/no-deprecated
    _statementCache = new StatementCache();
    _sqliteStatementCache = new StatementCache();
    /** only for tests
     * @internal
     */
    resetSqliteCache(size) {
        this._sqliteStatementCache.clear();
        this._sqliteStatementCache = new StatementCache(size);
    }
    constructor() {
        this._nativeDb = new IModelNative.platform.ECDb();
    }
    /** Call this function when finished with this ECDb object. This releases the native resources held by the
     *  ECDb object.
     */
    [Symbol.dispose]() {
        if (!this._nativeDb)
            return;
        this.closeDb();
        this._nativeDb.dispose();
        this._nativeDb = undefined;
    }
    /**
     * Attach an iModel file to this connection and load and register its schemas.
     * @note There are some reserve tablespace names that cannot be used. They are 'main', 'schema_sync_db', 'ecchange' & 'temp'
     * @param fileName IModel file name
     * @param alias identifier for the attached file. This identifer is used to access schema from the attached file. e.g. if alias is 'abc' then schema can be accessed using 'abc.MySchema.MyClass'
     */
    attachDb(fileName, alias) {
        if (alias.toLowerCase() === "main" || alias.toLowerCase() === "schema_sync_db" || alias.toLowerCase() === "ecchange" || alias.toLowerCase() === "temp") {
            throw new IModelError(DbResult.BE_SQLITE_ERROR, "Reserved tablespace name cannot be used");
        }
        this[_nativeDb].attachDb(fileName, alias);
    }
    /**
     * Detach the attached file from this connection. The attached file is closed and its schemas are unregistered.
     * @note There are some reserve tablespace names that cannot be used. They are 'main', 'schema_sync_db', 'ecchange' & 'temp'
     * @param alias identifer that was used in the call to [[attachDb]]
     */
    detachDb(alias) {
        if (alias.toLowerCase() === "main" || alias.toLowerCase() === "schema_sync_db" || alias.toLowerCase() === "ecchange" || alias.toLowerCase() === "temp") {
            throw new IModelError(DbResult.BE_SQLITE_ERROR, "Reserved tablespace name cannot be used");
        }
        this.clearStatementCache();
        this[_nativeDb].detachDb(alias);
    }
    /** @deprecated in 5.0 - will not be removed until after 2026-06-13. Use [Symbol.dispose] instead. */
    dispose() {
        this[Symbol.dispose]();
    }
    /** Create an ECDb
     * @param pathName The path to the ECDb file to create.
     * @throws [IModelError]($common) if the operation failed.
     */
    createDb(pathName) {
        const status = this[_nativeDb].createDb(pathName);
        if (status !== DbResult.BE_SQLITE_OK)
            throw new IModelError(status, "Failed to created ECDb");
    }
    /** Open the ECDb.
     * @param pathName The path to the ECDb file to open
     * @param openMode Open mode
     * @throws [IModelError]($common) if the operation failed.
     */
    openDb(pathName, openMode = ECDbOpenMode.Readonly) {
        const nativeOpenMode = openMode === ECDbOpenMode.Readonly ? OpenMode.Readonly : OpenMode.ReadWrite;
        const tryUpgrade = openMode === ECDbOpenMode.FileUpgrade;
        const status = this[_nativeDb].openDb(pathName, nativeOpenMode, tryUpgrade);
        if (status !== DbResult.BE_SQLITE_OK)
            throw new IModelError(status, "Failed to open ECDb");
    }
    /** Returns true if the ECDb is open */
    get isOpen() { return this[_nativeDb].isOpen(); }
    /** Close the Db after saving any uncommitted changes.
     * @throws [IModelError]($common) if the database is not open.
     */
    closeDb() {
        this._statementCache.clear();
        this._sqliteStatementCache.clear();
        this[_nativeDb].closeDb();
    }
    /** @internal use to test statement caching */
    clearStatementCache() {
        this._statementCache.clear();
    }
    /** @internal use to test statement caching */
    getCachedStatementCount() {
        return this._statementCache.size;
    }
    /** Commit the outermost transaction, writing changes to the file. Then, restart the transaction.
     * @param changesetName The name of the operation that generated these changes.
     * @throws [IModelError]($common) if the database is not open or if the operation failed.
     */
    saveChanges(changesetName) {
        const status = this[_nativeDb].saveChanges(changesetName);
        if (status !== DbResult.BE_SQLITE_OK)
            throw new IModelError(status, "Failed to save changes");
    }
    /** Abandon (cancel) the outermost transaction, discarding all changes since last save. Then, restart the transaction.
     * @throws [IModelError]($common) if the database is not open or if the operation failed.
     */
    abandonChanges() {
        const status = this[_nativeDb].abandonChanges();
        if (status !== DbResult.BE_SQLITE_OK)
            throw new IModelError(status, "Failed to abandon changes");
    }
    /** Import a schema.
     *
     * If the import was successful, the database is automatically saved to disk.
     * @param pathName Path to ECSchema XML file to import.
     * @throws [IModelError]($common) if the database is not open or if the operation failed.
     */
    importSchema(pathName) {
        const status = this[_nativeDb].importSchema(pathName);
        if (status !== DbResult.BE_SQLITE_OK) {
            Logger.logError(loggerCategory, `Failed to import schema from '${pathName}'.`);
            throw new IModelError(status, `Failed to import schema from '${pathName}'.`);
        }
    }
    /** Removes unused schemas from the database.
     *
     * If the removal was successful, the database is automatically saved to disk.
     * @param schemaNames Array of schema names to drop
     * @throws [IModelError]($common) if the database if the operation failed.
     * @alpha
     */
    dropSchemas(schemaNames) {
        if (schemaNames.length === 0)
            return;
        if (this[_nativeDb].schemaSyncEnabled())
            throw new IModelError(DbResult.BE_SQLITE_ERROR, "Cannot drop schemas when schema sync is enabled");
        try {
            this[_nativeDb].dropSchemas(schemaNames);
            this.saveChanges('dropped unused schemas');
        }
        catch (error) {
            Logger.logError(loggerCategory, `Failed to drop schemas: ${error}`);
            this.abandonChanges();
            throw new IModelError(DbResult.BE_SQLITE_ERROR, `Failed to drop schemas: ${error}`);
        }
    }
    /**
     * Returns the full schema for the input name.
     * @param name The name of the schema e.g. 'ECDbMeta'
     * @returns The SchemaProps for the requested schema
     * @throws if the schema can not be found or loaded.
     */
    getSchemaProps(name) {
        return this[_nativeDb].getSchemaProps(name);
    }
    /**
     * Use a prepared ECSQL statement, potentially from the statement cache. If the requested statement doesn't exist
     * in the statement cache, a new statement is prepared. After the callback completes, the statement is reset and saved
     * in the statement cache so it can be reused in the future. Use this method for ECSQL statements that will be
     * reused often and are expensive to prepare. The statement cache holds the most recently used statements, discarding
     * the oldest statements as it fills. For statements you don't intend to reuse, instead use [[withStatement]].
     * @param sql The SQLite SQL statement to execute
     * @param callback the callback to invoke on the prepared statement
     * @param logErrors Determines if error will be logged if statement fail to prepare
     * @returns the value returned by `callback`.
     * @see [[withWriteStatement]]
     * @beta
     */
    withCachedWriteStatement(ecsql, callback, logErrors = true) {
        // eslint-disable-next-line @typescript-eslint/no-deprecated
        const stmt = this._statementCache.findAndRemove(ecsql) ?? this.prepareStatement(ecsql, logErrors);
        const release = () => this._statementCache.addOrDispose(stmt);
        try {
            const val = callback(new ECSqlWriteStatement(stmt));
            if (val instanceof Promise) {
                val.then(release, release);
            }
            else {
                release();
            }
            return val;
        }
        catch (err) {
            release();
            throw err;
        }
    }
    /**
     * Prepared and execute a callback on an ECSQL statement. After the callback completes the statement is disposed.
     * Use this method for ECSQL statements are either not expected to be reused, or are not expensive to prepare.
     * For statements that will be reused often, instead use [[withPreparedStatement]].
     * @param sql The SQLite SQL statement to execute
     * @param callback the callback to invoke on the prepared statement
     * @param logErrors Determines if error will be logged if statement fail to prepare
     * @returns the value returned by `callback`.
     * @see [[withCachedWriteStatement]]
     * @beta
     */
    withWriteStatement(ecsql, callback, logErrors = true) {
        const stmt = this.prepareWriteStatement(ecsql, logErrors);
        const release = () => stmt[Symbol.dispose]();
        try {
            const val = callback(stmt);
            if (val instanceof Promise) {
                val.then(release, release);
            }
            else {
                release();
            }
            return val;
        }
        catch (err) {
            release();
            throw err;
        }
    }
    /** Prepare an ECSQL statement.
    * @param ecsql The ECSQL statement to prepare
    * @param logErrors Determines if error will be logged if statement fail to prepare
    * @throws [IModelError]($common) if there is a problem preparing the statement.
    * @beta
    */
    prepareWriteStatement(ecsql, logErrors = true) {
        // eslint-disable-next-line @typescript-eslint/no-deprecated
        return new ECSqlWriteStatement(this.prepareStatement(ecsql, logErrors));
    }
    /**
     * Use a prepared ECSQL statement, potentially from the statement cache. If the requested statement doesn't exist
     * in the statement cache, a new statement is prepared. After the callback completes, the statement is reset and saved
     * in the statement cache so it can be reused in the future. Use this method for ECSQL statements that will be
     * reused often and are expensive to prepare. The statement cache holds the most recently used statements, discarding
     * the oldest statements as it fills. For statements you don't intend to reuse, instead use [[withStatement]].
     * @param sql The SQLite SQL statement to execute
     * @param callback the callback to invoke on the prepared statement
     * @param logErrors Determines if error will be logged if statement fail to prepare
     * @returns the value returned by `callback`.
     * @see [[withStatement]]
     * @public
     * @deprecated in 4.11 - will not be removed until after 2026-06-13.  Use [[createQueryReader]] for SELECT statements and [[withCachedWriteStatement]] for INSERT/UPDATE/DELETE instead.
     */
    // eslint-disable-next-line @typescript-eslint/no-deprecated
    withPreparedStatement(ecsql, callback, logErrors = true) {
        // eslint-disable-next-line @typescript-eslint/no-deprecated
        const stmt = this._statementCache.findAndRemove(ecsql) ?? this.prepareStatement(ecsql, logErrors);
        const release = () => this._statementCache.addOrDispose(stmt);
        try {
            const val = callback(stmt);
            if (val instanceof Promise) {
                val.then(release, release);
            }
            else {
                release();
            }
            return val;
        }
        catch (err) {
            release();
            throw err;
        }
    }
    /**
     * Prepared and execute a callback on an ECSQL statement. After the callback completes the statement is disposed.
     * Use this method for ECSQL statements are either not expected to be reused, or are not expensive to prepare.
     * For statements that will be reused often, instead use [[withPreparedStatement]].
     * @param sql The SQLite SQL statement to execute
     * @param callback the callback to invoke on the prepared statement
     * @param logErrors Determines if error will be logged if statement fail to prepare
     * @returns the value returned by `callback`.
     * @see [[withPreparedStatement]]
     * @public
     * @deprecated in 4.11 - will not be removed until after 2026-06-13.  Use [[createQueryReader]] for SELECT statements and [[withWriteStatement]] for INSERT/UPDATE/DELETE instead.
     */
    // eslint-disable-next-line @typescript-eslint/no-deprecated
    withStatement(ecsql, callback, logErrors = true) {
        // eslint-disable-next-line @typescript-eslint/no-deprecated
        const stmt = this.prepareStatement(ecsql, logErrors);
        const release = () => stmt[Symbol.dispose]();
        try {
            const val = callback(stmt);
            if (val instanceof Promise) {
                val.then(release, release);
            }
            else {
                release();
            }
            return val;
        }
        catch (err) {
            release();
            throw err;
        }
    }
    /** Prepare an ECSQL statement.
     * @param ecsql The ECSQL statement to prepare
     * @param logErrors Determines if error will be logged if statement fail to prepare
     * @throws [IModelError]($common) if there is a problem preparing the statement.
     * @deprecated in 4.11 - will not be removed until after 2026-06-13.  Use [[prepareWriteStatement]] when preparing an INSERT/UPDATE/DELETE statement or [[createQueryReader]] to execute a SELECT statement.
     */
    // eslint-disable-next-line @typescript-eslint/no-deprecated
    prepareStatement(ecsql, logErrors = true) {
        // eslint-disable-next-line @typescript-eslint/no-deprecated
        const stmt = new ECSqlStatement();
        stmt.prepare(this[_nativeDb], ecsql, logErrors);
        return stmt;
    }
    /**
     * Use a prepared SQL statement, potentially from the statement cache. If the requested statement doesn't exist
     * in the statement cache, a new statement is prepared. After the callback completes, the statement is reset and saved
     * in the statement cache so it can be reused in the future. Use this method for SQL statements that will be
     * reused often and are expensive to prepare. The statement cache holds the most recently used statements, discarding
     * the oldest statements as it fills. For statements you don't intend to reuse, instead use [[withSqliteStatement]].
     * @param sql The SQLite SQL statement to execute
     * @param callback the callback to invoke on the prepared statement
     * @param logErrors Determines if error will be logged if statement fail to prepare
     * @returns the value returned by `callback`.
     * @see [[withPreparedStatement]]
     * @public
     */
    withPreparedSqliteStatement(sql, callback, logErrors = true) {
        const stmt = this._sqliteStatementCache.findAndRemove(sql) ?? this.prepareSqliteStatement(sql, logErrors);
        const release = () => this._sqliteStatementCache.addOrDispose(stmt);
        try {
            const val = callback(stmt);
            if (val instanceof Promise) {
                val.then(release, release);
            }
            else {
                release();
            }
            return val;
        }
        catch (err) {
            release();
            throw err;
        }
    }
    /**
     * Prepared and execute a callback on a SQL statement. After the callback completes the statement is disposed.
     * Use this method for SQL statements are either not expected to be reused, or are not expensive to prepare.
     * For statements that will be reused often, instead use [[withPreparedSqliteStatement]].
     * @param sql The SQLite SQL statement to execute
     * @param callback the callback to invoke on the prepared statement
     * @param logErrors Determines if error will be logged if statement fail to prepare
     * @returns the value returned by `callback`.
     * @public
     */
    withSqliteStatement(sql, callback, logErrors = true) {
        const stmt = this.prepareSqliteStatement(sql, logErrors);
        const release = () => stmt[Symbol.dispose]();
        try {
            const val = callback(stmt);
            if (val instanceof Promise) {
                val.then(release, release);
            }
            else {
                release();
            }
            return val;
        }
        catch (err) {
            release();
            throw err;
        }
    }
    /** Prepare an SQL statement.
     * @param sql The SQLite SQL statement to prepare
     * @param logErrors Determines if error will be logged if statement fail to prepare
     * @throws [IModelError]($common) if there is a problem preparing the statement.
     * @internal
     */
    prepareSqliteStatement(sql, logErrors = true) {
        const stmt = new SqliteStatement(sql);
        stmt.prepare(this[_nativeDb], logErrors);
        return stmt;
    }
    /** @internal */
    get [_nativeDb]() {
        assert(undefined !== this._nativeDb);
        return this._nativeDb;
    }
    /** Allow to execute query and read results along with meta data. The result are streamed.
     *
     * See also:
     * - [ECSQL Overview]($docs/learning/backend/ExecutingECSQL)
     * - [Code Examples]($docs/learning/backend/ECSQLCodeExamples)
     * - [ECSQL Row Format]($docs/learning/ECSQLRowFormat)
     *
     * @param params The values to bind to the parameters (if the ECSQL has any).
     * @param config Allow to specify certain flags which control how query is executed.
     * @returns Returns an [ECSqlReader]($common) which helps iterate over the result set and also give access to metadata.
     * @public
     * */
    createQueryReader(ecsql, params, config) {
        if (!this._nativeDb || !this._nativeDb.isOpen()) {
            throw new IModelError(DbResult.BE_SQLITE_ERROR, "db not open");
        }
        const executor = {
            execute: async (request) => {
                return ConcurrentQuery.executeQueryRequest(this[_nativeDb], request);
            },
        };
        return new ECSqlReader(executor, ecsql, params, config);
    }
}
//# sourceMappingURL=ECDb.js.map