import { BinaryReader, BinaryWriter } from "@dao-xyz/borsh";
import { fromBase64URL, toBase64URL } from "@peerbit/crypto";
import {} from "@sqlite.org/sqlite-wasm";
import { v4 as uuid } from "uuid";
import {} from "./types.js";
export const encodeName = (name) => {
    // since "/" and perhaps other characters might not be allowed we do encode
    const writer = new BinaryWriter();
    writer.string(name);
    return toBase64URL(writer.finalize());
};
export const decodeName = (name) => {
    // since "/" and perhaps other characters might not be allowed we do encode
    const writer = new BinaryReader(fromBase64URL(name));
    return writer.string();
};
class Statement {
    statement;
    id;
    constructor(statement, id) {
        this.statement = statement;
        this.id = id;
    }
    async bind(values) {
        await this.statement.bind(values);
        return this;
    }
    async finalize() {
        const out = await this.statement.finalize();
        if (out != null && out > 0) {
            throw new Error("Error finalizing statement");
        }
    }
    get(values) {
        if (values && values?.length > 0) {
            this.statement.bind(values);
        }
        let step = this.statement.step();
        if (!step) {
            // no data available
            this.statement.reset();
            return undefined;
        }
        const results = this.statement.get({});
        this.statement.reset();
        return results;
    }
    run(values) {
        this.statement.bind(values);
        this.statement.stepReset();
    }
    async reset() {
        try {
            await this.statement.reset(); // this returns the last rc
        }
        catch (e) {
            // sqlite3_reset() can surface the prior step/exec rc.
            // The statement *is* reset; ignore benign codes.
            const msg = e?.message || "";
            const rc = e?.rc;
            const code = e?.code;
            const isFk = code === "SQLITE_CONSTRAINT_FOREIGNKEY" ||
                rc === 787 ||
                msg.includes("SQLITE_CONSTRAINT_FOREIGNKEY") ||
                msg.includes("FOREIGN KEY constraint failed");
            const isBusy = code === "SQLITE_BUSY" || rc === 5; // optional
            if (isFk || isBusy) {
                return this; // swallow; stmt is reset
            }
            // (keep throwing for other unexpected errors)
            throw e;
        }
        return this;
    }
    all(values) {
        if (values && values.length > 0) {
            this.statement.bind(values);
        }
        let results = [];
        while (this.statement.step()) {
            results.push(this.statement.get({}));
        }
        this.statement.reset();
        return results;
    }
    step() {
        return this.statement.step();
    }
}
// eslint-disable-next-line no-console
const log = (...args) => console.log(...args);
// eslint-disable-next-line no-console
const error = (...args) => console.error(...args);
const SQLITE3_ASSET_BASE = "/peerbit/sqlite3";
const SQLITE3_ASSET_DIR = `${SQLITE3_ASSET_BASE}/`;
const SQLITE3_WASM_PATH = `${SQLITE3_ASSET_BASE}/sqlite3.wasm`;
let sqlite3InitModulePromise;
const ensureSqlite3InitModuleState = () => {
    const globalWithSqlite = globalThis;
    const existing = globalWithSqlite.sqlite3InitModuleState ?? {};
    const debugModule = typeof existing.debugModule === "function"
        ? existing.debugModule
        : () => { };
    existing.debugModule = debugModule;
    existing.wasmFilename = SQLITE3_WASM_PATH;
    existing.sqlite3Dir = SQLITE3_ASSET_DIR;
    globalWithSqlite.sqlite3InitModuleState = existing;
};
const loadSqlite3InitModule = async () => {
    if (!sqlite3InitModulePromise) {
        sqlite3InitModulePromise = import("@sqlite.org/sqlite-wasm").then((mod) => mod.default);
    }
    const sqlite3InitModule = await sqlite3InitModulePromise;
    // sqlite-wasm reads sqlite3InitModuleState when sqlite3InitModule() runs.
    ensureSqlite3InitModuleState();
    return sqlite3InitModule;
};
let poolUtil = undefined;
let sqlite3 = undefined;
let sqlite3Promise = undefined;
const getSqlite3 = async () => {
    if (sqlite3) {
        return sqlite3;
    }
    if (!sqlite3Promise) {
        sqlite3Promise = (async () => {
            const sqlite3InitModule = await loadSqlite3InitModule();
            // sqlite-wasm deletes sqlite3InitModuleState at the beginning of
            // sqlite3InitModule(), so ensure the state is set immediately before
            // initializing (and guard against concurrent initializers).
            ensureSqlite3InitModuleState();
            return sqlite3InitModule({
                print: log,
                printErr: error,
                locateFile: (file) => `${SQLITE3_ASSET_BASE}/${file}`,
            });
        })().then((module) => {
            sqlite3 = module;
            return module;
        });
    }
    return sqlite3Promise;
};
const create = async (directory) => {
    let statements = new Map();
    const sqlite3 = await getSqlite3();
    let sqliteDb = undefined;
    let closeInternal = async () => {
        await Promise.all([...statements.values()].map((x) => x.finalize?.()));
        statements.clear();
        await sqliteDb?.close();
        sqliteDb = undefined;
    };
    let dbFileName;
    const cleanupPool = async (_label, preserveDbFile) => {
        if (!poolUtil || dbFileName == null) {
            return;
        }
        const relatedFiles = new Set([
            dbFileName,
            `${dbFileName}-journal`,
            `${dbFileName}-wal`,
            `${dbFileName}-shm`,
        ]);
        for (const fileName of relatedFiles) {
            if (preserveDbFile && fileName === dbFileName) {
                continue;
            }
            try {
                poolUtil.unlink(fileName);
            }
            catch {
                // ignore unlink failures
            }
        }
        const wipePool = async () => {
            if (preserveDbFile || !poolUtil?.wipeFiles) {
                return;
            }
            try {
                await poolUtil.wipeFiles();
            }
            catch {
                // ignore wipe failures
            }
        };
        const directoryPrefix = directory
            ? `${directory.replace(/\/$/, "")}/`
            : undefined;
        if (!directoryPrefix) {
            await wipePool();
            return;
        }
        let poolFiles = [];
        try {
            poolFiles = poolUtil.getFileNames?.() ?? [];
        }
        catch {
            poolFiles = [];
        }
        for (const name of poolFiles) {
            if (preserveDbFile && name === dbFileName) {
                continue;
            }
            if (relatedFiles.has(name)) {
                continue;
            }
            if (name.startsWith(directoryPrefix)) {
                try {
                    poolUtil.unlink(name);
                }
                catch {
                    // ignore unlink failures
                }
            }
        }
        await wipePool();
    };
    let close = async () => {
        await closeInternal();
        const preserve = Boolean(directory);
        await cleanupPool("close", preserve);
    };
    let drop = async () => {
        await closeInternal();
        await cleanupPool("drop", false);
    };
    let open = async () => {
        if (sqliteDb) {
            return sqliteDb;
        }
        if (directory) {
            // directory has to be absolute path. Remove leading dot if any
            // TODO show warning if directory is not absolute?
            directory = directory.replace(/^\./, "");
            dbFileName = `${directory}/db.sqlite`;
            const poolDirectory = `${directory}/peerbit/sqlite-opfs-pool`; // we do a unique directory else we will get problem open a client in multiple tabs
            const activePoolUtil = poolUtil ||
                (await sqlite3.installOpfsSAHPoolVfs({
                    directory: poolDirectory,
                }));
            poolUtil = activePoolUtil;
            await activePoolUtil.reserveMinimumCapacity(100);
            sqliteDb = new activePoolUtil.OpfsSAHPoolDb(dbFileName);
        }
        else {
            sqliteDb = new sqlite3.oo1.DB(":memory:");
        }
        if (!sqliteDb) {
            throw new Error("Failed to open sqlite database");
        }
        sqliteDb.exec("PRAGMA journal_mode = WAL");
        sqliteDb.exec("PRAGMA foreign_keys = on");
    };
    return {
        close,
        exec: (sql) => {
            return sqliteDb.exec(sql);
        },
        open,
        drop,
        prepare: async (sql, id) => {
            if (id == null) {
                id = uuid();
            }
            let prev = statements.get(id);
            if (prev) {
                await prev.reset();
                return prev;
            }
            const statement = sqliteDb.prepare(sql);
            const wrappedStatement = new Statement(statement, id);
            statements.set(id, wrappedStatement);
            return wrappedStatement;
        },
        get(sql) {
            return sqliteDb.exec({ sql, rowMode: "array" });
        },
        run(sql, bind) {
            return sqliteDb.exec(sql, { bind, rowMode: "array" });
        },
        status: () => (sqliteDb?.isOpen() ? "open" : "closed"),
        statements,
    };
};
export { create };
//# sourceMappingURL=sqlite3.wasm.js.map