import { RWLock } from "@rocicorp/lock";
import { deepFreeze } from "../frozen-json.js";
import { throwIfStoreClosed, throwIfTransactionClosed } from "./throw-if-closed.js";
class SQLiteStore {
  #filename;
  #entry;
  #closed = false;
  constructor(name, create, opts) {
    this.#filename = safeFilename(name);
    this.#entry = getOrCreateEntry(name, create, opts);
  }
  async read() {
    throwIfStoreClosed(this);
    const entry = this.#entry;
    const { db, lock, preparedStatements } = entry;
    const release = await lock.read();
    if (entry.activeReaders === 0) {
      db.execSync("BEGIN");
    }
    entry.activeReaders++;
    return new SQLiteStoreRead(() => {
      entry.activeReaders--;
      if (entry.activeReaders === 0) {
        db.execSync("COMMIT");
      }
      release();
    }, preparedStatements);
  }
  async write() {
    throwIfStoreClosed(this);
    const { lock, db, preparedStatements } = this.#entry;
    const release = await lock.write();
    db.execSync("BEGIN IMMEDIATE");
    return new SQLiteWrite(release, db, preparedStatements);
  }
  async close() {
    if (this.#closed) {
      return;
    }
    const { lock, db } = this.#entry;
    const writeRelease = await lock.write();
    decrementStoreRefCount(this.#filename, db);
    this.#closed = true;
    writeRelease();
  }
  get closed() {
    return this.#closed;
  }
}
function safeFilename(name) {
  return name.replace(/[^a-zA-Z0-9]/g, "_");
}
function setupDatabase(delegate, opts) {
  delegate.execSync(`PRAGMA busy_timeout = ${opts?.busyTimeout ?? 200}`);
  delegate.execSync(`PRAGMA journal_mode = '${opts?.journalMode ?? "WAL"}'`);
  delegate.execSync(`PRAGMA synchronous = '${opts?.synchronous ?? "NORMAL"}'`);
  delegate.execSync(
    `PRAGMA read_uncommitted = ${Boolean(opts?.readUncommitted)}`
  );
  delegate.execSync(`
    CREATE TABLE IF NOT EXISTS entry (
      key TEXT PRIMARY KEY, 
      value TEXT NOT NULL
    ) WITHOUT ROWID
  `);
  return {
    has: delegate.prepare(`SELECT 1 FROM entry WHERE key = ? LIMIT 1`),
    get: delegate.prepare("SELECT value FROM entry WHERE key = ?"),
    put: delegate.prepare(
      "INSERT OR REPLACE INTO entry (key, value) VALUES (?, ?)"
    ),
    del: delegate.prepare("DELETE FROM entry WHERE key = ?")
  };
}
class SQLiteStoreRead {
  #release;
  #closed = false;
  #preparedStatements;
  constructor(release, preparedStatements) {
    this.#release = release;
    this.#preparedStatements = preparedStatements;
  }
  async has(key) {
    throwIfTransactionClosed(this);
    const value = await this.#preparedStatements.has.firstValue([key]);
    return value !== void 0;
  }
  async get(key) {
    throwIfTransactionClosed(this);
    const value = await this.#preparedStatements.get.firstValue([key]);
    if (!value) {
      return void 0;
    }
    const parsedValue = JSON.parse(value);
    return deepFreeze(parsedValue);
  }
  release() {
    if (!this.#closed) {
      this.#closed = true;
      this.#release();
    }
  }
  get closed() {
    return this.#closed;
  }
}
class SQLiteWrite {
  #release;
  #dbDelegate;
  #preparedStatements;
  #committed = false;
  #closed = false;
  constructor(release, dbDelegate, preparedStatements) {
    this.#release = release;
    this.#dbDelegate = dbDelegate;
    this.#preparedStatements = preparedStatements;
  }
  async has(key) {
    throwIfTransactionClosed(this);
    const value = await this.#preparedStatements.has.firstValue([key]);
    return value !== void 0;
  }
  async get(key) {
    throwIfTransactionClosed(this);
    const value = await this.#preparedStatements.get.firstValue([key]);
    if (!value) {
      return void 0;
    }
    const parsedValue = JSON.parse(value);
    return deepFreeze(parsedValue);
  }
  async put(key, value) {
    throwIfTransactionClosed(this);
    await this.#preparedStatements.put.exec([key, JSON.stringify(value)]);
  }
  async del(key) {
    throwIfTransactionClosed(this);
    await this.#preparedStatements.del.exec([key]);
  }
  // oxlint-disable-next-line require-await
  async commit() {
    throwIfTransactionClosed(this);
    this.#dbDelegate.execSync("COMMIT");
    this.#committed = true;
  }
  release() {
    if (!this.#closed) {
      this.#closed = true;
      if (!this.#committed) {
        this.#dbDelegate.execSync("ROLLBACK");
      }
      this.#release();
    }
  }
  get closed() {
    return this.#closed;
  }
}
const stores = /* @__PURE__ */ new Map();
function getOrCreateEntry(name, create, opts) {
  const filename = safeFilename(name);
  const entry = stores.get(filename);
  if (entry) {
    entry.refCount++;
    return entry;
  }
  const dbDelegate = create(filename, opts);
  const preparedStatements = setupDatabase(dbDelegate, opts);
  const lock = new RWLock();
  const newEntry = {
    lock,
    db: dbDelegate,
    refCount: 1,
    activeReaders: 0,
    preparedStatements
  };
  stores.set(filename, newEntry);
  return newEntry;
}
function decrementStoreRefCount(filename, dbDelegate) {
  const entry = stores.get(filename);
  if (entry) {
    entry.refCount--;
    if (entry.refCount <= 0) {
      dbDelegate.close();
      stores.delete(filename);
    }
  }
}
function clearAllNamedStoresForTesting() {
  for (const entry of stores.values()) {
    entry.db.close();
  }
  stores.clear();
}
function dropStore(name, createDelegate) {
  const filename = safeFilename(name);
  const entry = stores.get(filename);
  if (entry) {
    try {
      entry.db.close();
    } catch {
    }
    stores.delete(filename);
  }
  const tempDelegate = createDelegate(filename);
  try {
    tempDelegate.close();
  } catch {
  }
  try {
    tempDelegate.destroy();
  } catch {
  }
  return Promise.resolve();
}
export {
  SQLiteStore,
  SQLiteStoreRead,
  SQLiteWrite,
  clearAllNamedStoresForTesting,
  dropStore,
  safeFilename,
  setupDatabase
};
//# sourceMappingURL=sqlite-store.js.map
