import { Factory as WaSqliteFactory, SQLITE_ROW } from '@journeyapps/wa-sqlite';
import { DEFAULT_MODULE_FACTORIES } from './vfs.js';
/**
 * A small wrapper around WA-sqlite to help with opening databases and running statements by preparing them internally.
 *
 * This is an internal class, and it must never be used directly. Wrappers are required to ensure raw connections aren't
 * used concurrently across tabs.
 */
export class RawSqliteConnection {
    options;
    _sqliteAPI = null;
    /**
     * The `sqlite3*` connection pointer.
     */
    db = 0;
    _moduleFactory;
    constructor(options) {
        this.options = options;
        this._moduleFactory = DEFAULT_MODULE_FACTORIES[this.options.vfs];
    }
    get isOpen() {
        return this.db != 0;
    }
    async init() {
        const api = (this._sqliteAPI = await this.openSQLiteAPI());
        this.db = await api.open_v2(this.options.dbFilename, this.options.isReadOnly ? 1 /* SQLITE_OPEN_READONLY */ : 6 /* SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE */);
        await this.executeRaw(`PRAGMA temp_store = ${this.options.temporaryStorage};`);
        if (this.options.encryptionKey) {
            const escapedKey = this.options.encryptionKey.replace("'", "''");
            await this.executeRaw(`PRAGMA key = '${escapedKey}'`);
        }
        await this.executeRaw(`PRAGMA cache_size = -${this.options.cacheSizeKb};`);
        await this.executeRaw(`SELECT powersync_update_hooks('install');`);
    }
    async openSQLiteAPI() {
        const { module, vfs } = await this._moduleFactory({
            dbFileName: this.options.dbFilename,
            encryptionKey: this.options.encryptionKey
        });
        const sqlite3 = WaSqliteFactory(module);
        sqlite3.vfs_register(vfs, true);
        /**
         * Register the PowerSync core SQLite extension
         */
        module.ccall('powersync_init_static', 'int', []);
        /**
         * Create the multiple cipher vfs if an encryption key is provided
         */
        if (this.options.encryptionKey) {
            const createResult = module.ccall('sqlite3mc_vfs_create', 'int', ['string', 'int'], [this.options.dbFilename, 1]);
            if (createResult !== 0) {
                throw new Error('Failed to create multiple cipher vfs, Database encryption will not work');
            }
        }
        return sqlite3;
    }
    requireSqlite() {
        if (!this._sqliteAPI) {
            throw new Error(`Initialization has not completed`);
        }
        return this._sqliteAPI;
    }
    /**
     * Checks if the database connection is in autocommit mode.
     * @returns true if in autocommit mode, false if in a transaction
     */
    isAutoCommit() {
        return this.requireSqlite().get_autocommit(this.db) != 0;
    }
    async execute(sql, bindings) {
        const resultSet = await this.executeSingleStatementRaw(sql, bindings);
        return this.wrapQueryResults(this.requireSqlite(), resultSet);
    }
    async executeBatch(sql, bindings) {
        const results = [];
        const api = this.requireSqlite();
        for await (const stmt of api.statements(this.db, sql)) {
            let columns;
            for (const parameterSet of bindings) {
                const rs = await this.stepThroughStatement(api, stmt, parameterSet, columns, false);
                results.push(this.wrapQueryResults(api, rs));
            }
            // executeBatch can only use a single statement
            break;
        }
        return results;
    }
    wrapQueryResults(api, rs) {
        return {
            changes: api.changes(this.db),
            lastInsertRowId: api.last_insert_id(this.db),
            autocommit: api.get_autocommit(this.db) != 0,
            resultSet: rs
        };
    }
    /**
     * This executes a single statement using SQLite3 and returns the results as a {@link RawResultSet}.
     */
    async executeSingleStatementRaw(sql, bindings) {
        const results = await this.executeRaw(sql, bindings);
        return results.length ? results[0] : undefined;
    }
    async executeRaw(sql, bindings) {
        const results = [];
        const api = this.requireSqlite();
        for await (const stmt of api.statements(this.db, sql)) {
            let columns;
            const rs = await this.stepThroughStatement(api, stmt, bindings ?? [], columns);
            columns = rs.columns;
            if (columns.length) {
                results.push(rs);
            }
            // When binding parameters, only a single statement is executed.
            if (bindings) {
                break;
            }
        }
        return results;
    }
    async stepThroughStatement(api, stmt, bindings, knownColumns, includeResults = true) {
        // TODO not sure why this is needed currently, but booleans break
        bindings.forEach((b, index, arr) => {
            if (typeof b == 'boolean') {
                arr[index] = b ? 1 : 0;
            }
        });
        api.reset(stmt);
        if (bindings) {
            api.bind_collection(stmt, bindings);
        }
        const rows = [];
        while ((await api.step(stmt)) === SQLITE_ROW) {
            if (includeResults) {
                const row = api.row(stmt);
                rows.push(row);
            }
        }
        knownColumns ??= api.column_names(stmt);
        return { columns: knownColumns, rows };
    }
    async close() {
        if (this.isOpen) {
            await this.requireSqlite().close(this.db);
            this.db = 0;
        }
    }
}
