"use strict";
import assert from "node:assert";
import { runInThisContext } from "node:vm";
import { fileURLToPath } from "node:url";
import { join } from "node:path";
import bindings from "node-gyp-build";
const ROOT_DIR = import.meta.url ? fileURLToPath(new URL("..", import.meta.url)) : join(void 0, "..");
const addon = bindings(ROOT_DIR);
class Statement {
  #needsTranslation;
  #cache;
  #createRow;
  #native;
  #onClose;
  /** @internal */
  constructor(db, query, { persistent, pluck, bigint }, onClose) {
    this.#needsTranslation = persistent === true && !pluck;
    this.#native = addon.statementNew(
      db,
      query,
      persistent === true,
      pluck === true,
      bigint === true
    );
    this.#onClose = onClose;
  }
  /**
   * Run the statement's query without returning any rows.
   *
   * @param params - Parameters to be bound to query placeholders before
   *                 executing the statement.
   * @returns An object with `changes` and `lastInsertedRowid` integers.
   */
  run(params) {
    if (this.#native === void 0) {
      throw new Error("Statement closed");
    }
    const result = [0, 0];
    this.#checkParams(params);
    addon.statementRun(this.#native, params, result);
    return { changes: result[0], lastInsertRowid: result[1] };
  }
  /**
   * Run the statement's query and return the first row of the result or
   * `undefined` if no rows matched.
   *
   * @param params - Parameters to be bound to query placeholders before
   *                 executing the statement.
   * @returns A row object or a single column if `pluck: true` is set in the
   *          statement options.
   */
  get(params) {
    if (this.#native === void 0) {
      throw new Error("Statement closed");
    }
    this.#checkParams(params);
    const result = addon.statementStep(this.#native, params, this.#cache, true);
    if (result === void 0) {
      return void 0;
    }
    if (!this.#needsTranslation) {
      return result;
    }
    const createRow = this.#updateCache(result);
    return createRow(result);
  }
  /**
   * Run the statement's query and return the all rows of the result or
   * `undefined` if no rows matched.
   *
   * @param params - Parameters to be bound to query placeholders before
   *                 executing the statement.
   * @returns A list of row objects or single columns if `pluck: true` is set in
   *          the statement options.
   */
  all(params) {
    if (this.#native === void 0) {
      throw new Error("Statement closed");
    }
    const result = [];
    this.#checkParams(params);
    let singleUseParams = params;
    while (true) {
      const single = addon.statementStep(
        this.#native,
        singleUseParams,
        this.#cache,
        false
      );
      singleUseParams = null;
      if (single === void 0) {
        break;
      }
      if (!this.#needsTranslation) {
        result.push(single);
        continue;
      }
      const createRow = this.#updateCache(single);
      result.push(createRow(single));
    }
    return result;
  }
  /**
   * Close the statement and release the used memory.
   */
  close() {
    if (this.#native === void 0) {
      throw new Error("Statement already closed");
    }
    addon.statementClose(this.#native);
    this.#native = void 0;
    this.#onClose?.();
  }
  /** @internal */
  #updateCache(result) {
    if (this.#cache === result) {
      assert(this.#createRow !== void 0);
      return this.#createRow;
    }
    const half = result.length >>> 1;
    const lines = [];
    for (let i = 0; i < half; i += 1) {
      lines.push(`${JSON.stringify(result[i])}: value[${half} + ${i}],`);
    }
    this.#cache = result;
    const createRow = runInThisContext(`(function createRow(value) {
      return {
        ${lines.join("\n")}
      };
    })`);
    this.#createRow = createRow;
    return createRow;
  }
  /** @internal */
  #checkParams(params) {
    if (params === void 0) {
      return;
    }
    if (typeof params !== "object") {
      throw new TypeError("Params must be either object or array");
    }
    if (params === null) {
      throw new TypeError("Params cannot be null");
    }
  }
}
export {};
export default class Database {
  #native;
  #transactionDepth = 0;
  #isCacheEnabled;
  #statementCache = /* @__PURE__ */ new Map();
  #transactionStmts;
  /**
   * Constructor
   *
   * @param path - The path to the database file or ':memory:'/'' for opening
   *               the in-memory database.
   */
  constructor(path = ":memory:", { cacheStatements } = {}) {
    if (typeof path !== "string") {
      throw new TypeError("Invalid database path");
    }
    this.#native = addon.databaseOpen(path);
    this.#isCacheEnabled = cacheStatements === true;
  }
  initTokenizer() {
    if (this.#native === void 0) {
      throw new Error("Database closed");
    }
    addon.databaseInitTokenizer(this.#native);
  }
  /**
   * Execute one or multiple SQL statements in a given `sql` string.
   *
   * @param sql - one or multiple SQL statements
   */
  exec(sql) {
    if (this.#native === void 0) {
      throw new Error("Database closed");
    }
    if (typeof sql !== "string") {
      throw new TypeError("Invalid sql argument");
    }
    addon.databaseExec(this.#native, sql);
  }
  prepare(query, options = {}) {
    if (this.#native === void 0) {
      throw new Error("Database closed");
    }
    if (typeof query !== "string") {
      throw new TypeError("Invalid query argument");
    }
    if (!this.#isCacheEnabled || options.persistent === false) {
      return new Statement(this.#native, query, options);
    }
    const cacheKey = `${options.pluck}:${options.bigint}:${query}`;
    const cached = this.#statementCache.get(cacheKey);
    if (cached !== void 0) {
      return cached;
    }
    const stmt = new Statement(
      this.#native,
      query,
      {
        persistent: true,
        pluck: options.pluck,
        bigint: options.bigint
      },
      () => this.#statementCache.delete(cacheKey)
    );
    this.#statementCache.set(cacheKey, stmt);
    return stmt;
  }
  /**
   * Close the database and all associated statements.
   */
  close() {
    if (this.#native === void 0) {
      throw new Error("Database already closed");
    }
    addon.databaseClose(this.#native);
    this.#native = void 0;
  }
  pragma(source, { simple } = {}) {
    if (typeof source !== "string") {
      throw new TypeError("Invalid pragma argument");
    }
    if (simple === true) {
      const stmt2 = this.prepare(`PRAGMA ${source}`, { pluck: true });
      return stmt2.get();
    }
    const stmt = this.prepare(`PRAGMA ${source}`);
    return stmt.all();
  }
  /**
   * Wrap `fn()` in a transaction.
   *
   * @param fn - a function to be executed within a transaction.
   * @returns The value returned by `fn()`.
   */
  transaction(fn) {
    return (...params) => {
      if (this.#transactionStmts === void 0) {
        const options = { persistent: true, pluck: true };
        this.#transactionStmts = {
          begin: this.prepare("BEGIN", options),
          rollback: this.prepare("ROLLBACK", options),
          commit: this.prepare("COMMIT", options),
          savepoint: this.prepare("SAVEPOINT signalappsqlcipher", options),
          rollbackTo: this.prepare("ROLLBACK TO signalappsqlcipher", options),
          release: this.prepare("RELEASE signalappsqlcipher", options)
        };
      }
      this.#transactionDepth += 1;
      let begin;
      let rollback;
      let commit;
      if (this.#transactionDepth === 1) {
        ({ begin, rollback, commit } = this.#transactionStmts);
      } else {
        ({
          savepoint: begin,
          rollbackTo: rollback,
          release: commit
        } = this.#transactionStmts);
      }
      begin.run();
      try {
        const result = fn(...params);
        commit.run();
        return result;
      } catch (error) {
        rollback.run();
        throw error;
      } finally {
        this.#transactionDepth -= 1;
      }
    };
  }
  /**
   * Tokenize a given sentence with a Signal-FTS5-Extension.
   *
   * @param value - a sentence
   * @returns a list of word-like tokens.
   *
   * @see {@link https://github.com/signalapp/Signal-FTS5-Extension}
   */
  signalTokenize(value) {
    if (typeof value !== "string") {
      throw new TypeError("Invalid value");
    }
    return addon.signalTokenize(value);
  }
}
export { Database };
