/*
This file is part of the Notesnook project (https://notesnook.com/)

Copyright (C) 2023 Streetwriters (Private) Limited

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

import * as SQLite from "./sqlite-constants.js";
export * from "./sqlite-constants.js";

const MAX_INT64 = 0x7fffffffffffffffn;
const MIN_INT64 = -0x8000000000000000n;

export class SQLiteError extends Error {
  constructor(message, code) {
    super(message);
    this.code = code;
  }
}

const async = true;

/**
 * Builds a Javascript API from the Emscripten module. This API is still
 * low-level and closely corresponds to the C API exported by the module,
 * but differs in some specifics like throwing exceptions on errors.
 * @param {*} Module SQLite Emscripten module
 */
export function Factory(Module) {
  /** @type {SQLiteAPI} */
  const sqlite3 = {};

  const sqliteFreeAddress = Module._getSqliteFree();

  // Allocate some space for 32-bit returned values.
  const tmp = Module._malloc(8);
  const tmpPtr = [tmp, tmp + 4];

  // Convert a JS string to a C string. sqlite3_malloc is used to allocate
  // memory (use sqlite3_free to deallocate).
  function createUTF8(s) {
    if (typeof s !== "string") return 0;
    const n = Module.lengthBytesUTF8(s);
    const zts = Module._sqlite3_malloc(n + 1);
    Module.stringToUTF8(s, zts, n + 1);
    return zts;
  }

  /**
   * Concatenate 32-bit numbers into a 64-bit (signed) BigInt.
   * @param {number} lo32
   * @param {number} hi32
   * @returns {bigint}
   */
  function cvt32x2ToBigInt(lo32, hi32) {
    return (BigInt(hi32) << 32n) | (BigInt(lo32) & 0xffffffffn);
  }

  /**
   * Concatenate 32-bit numbers and return as number or BigInt, depending
   * on the value.
   * @param {number} lo32
   * @param {number} hi32
   * @returns {number|bigint}
   */
  const cvt32x2AsSafe = (function () {
    const hiMax = BigInt(Number.MAX_SAFE_INTEGER) >> 32n;
    const hiMin = BigInt(Number.MIN_SAFE_INTEGER) >> 32n;

    return function (lo32, hi32) {
      if (hi32 > hiMax || hi32 < hiMin) {
        // Can't be expressed as a Number so use BigInt.
        return cvt32x2ToBigInt(lo32, hi32);
      } else {
        // Combine the upper and lower 32-bit numbers. The complication is
        // that lo32 is a signed integer which makes manipulating its bits
        // a little tricky - the sign bit gets handled separately.
        return hi32 * 0x100000000 + (lo32 & 0x7fffffff) - (lo32 & 0x80000000);
      }
    };
  })();

  const databases = new Set();
  function verifyDatabase(db) {
    if (!databases.has(db)) {
      throw new SQLiteError("not a database", SQLite.SQLITE_MISUSE);
    }
  }

  const mapStmtToDB = new Map();
  function verifyStatement(stmt) {
    if (!mapStmtToDB.has(stmt)) {
      throw new SQLiteError("not a statement", SQLite.SQLITE_MISUSE);
    }
  }

  sqlite3.bind_collection = function (stmt, bindings) {
    verifyStatement(stmt);
    const isArray = Array.isArray(bindings);
    const nBindings = sqlite3.bind_parameter_count(stmt);
    for (let i = 1; i <= nBindings; ++i) {
      const key = isArray ? i - 1 : sqlite3.bind_parameter_name(stmt, i);
      const value = bindings[key];
      if (value !== undefined) {
        sqlite3.bind(stmt, i, value);
      }
    }
    return SQLite.SQLITE_OK;
  };

  sqlite3.bind = function (stmt, i, value) {
    // verifyStatement(stmt);
    switch (typeof value) {
      case "number":
        if (value === (value | 0)) {
          return sqlite3.bind_int(stmt, i, value);
        } else {
          return sqlite3.bind_double(stmt, i, value);
        }
      case "string":
        return sqlite3.bind_text(stmt, i, value);
      default:
        if (value instanceof Uint8Array || Array.isArray(value)) {
          return sqlite3.bind_blob(stmt, i, value);
        } else if (value === null) {
          return sqlite3.bind_null(stmt, i);
        } else if (typeof value === "bigint") {
          return sqlite3.bind_int64(stmt, i, value);
        } else if (value === undefined) {
          // Existing binding (or NULL) will be used.
          return SQLite.SQLITE_NOTICE;
        } else {
          console.warn("unknown binding converted to null", value);
          return sqlite3.bind_null(stmt, i);
        }
    }
  };

  sqlite3.bind_blob = (function () {
    const fname = "sqlite3_bind_blob";
    const f = Module.cwrap(fname, ...decl("nnnnn:n"));
    return function (stmt, i, value) {
      //  verifyStatement(stmt);
      // @ts-ignore
      const byteLength = value.byteLength ?? value.length;
      const ptr = Module._sqlite3_malloc(byteLength);
      Module.HEAPU8.subarray(ptr).set(value);
      const result = f(stmt, i, ptr, byteLength, sqliteFreeAddress);
      // trace(fname, result);
      return check(fname, result, null, stmt);
    };
  })();

  sqlite3.bind_parameter_count = (function () {
    const fname = "sqlite3_bind_parameter_count";
    const f = Module.cwrap(fname, ...decl("n:n"));
    return function (stmt) {
      // verifyStatement(stmt);
      const result = f(stmt);
      // trace(fname, result);
      return result;
    };
  })();

  sqlite3.clear_bindings = (function () {
    const fname = "sqlite3_clear_bindings";
    const f = Module.cwrap(fname, ...decl("n:n"));
    return function (stmt) {
      // verifyStatement(stmt);
      const result = f(stmt);
      // trace(fname, result);
      return result;
    };
  })();

  sqlite3.bind_double = (function () {
    const fname = "sqlite3_bind_double";
    const f = Module.cwrap(fname, ...decl("nnn:n"));
    return function (stmt, i, value) {
      // verifyStatement(stmt);
      const result = f(stmt, i, value);
      // trace(fname, result);
      return check(fname, result, null, stmt);
    };
  })();

  sqlite3.bind_int = (function () {
    const fname = "sqlite3_bind_int";
    const f = Module.cwrap(fname, ...decl("nnn:n"));
    return function (stmt, i, value) {
      // verifyStatement(stmt);
      if (value > 0x7fffffff || value < -0x80000000) return SQLite.SQLITE_RANGE;

      const result = f(stmt, i, value);
      // trace(fname, result);
      return check(fname, result, null, stmt);
    };
  })();

  sqlite3.bind_int64 = (function () {
    const fname = "sqlite3_bind_int64";
    const f = Module.cwrap(fname, ...decl("nnnn:n"));
    return function (stmt, i, value) {
      // verifyStatement(stmt);
      if (value > MAX_INT64 || value < MIN_INT64) return SQLite.SQLITE_RANGE;

      const lo32 = value & 0xffffffffn;
      const hi32 = value >> 32n;
      const result = f(stmt, i, Number(lo32), Number(hi32));
      // trace(fname, result);
      return check(fname, result, null, stmt);
    };
  })();

  sqlite3.bind_null = (function () {
    const fname = "sqlite3_bind_null";
    const f = Module.cwrap(fname, ...decl("nn:n"));
    return function (stmt, i) {
      // verifyStatement(stmt);
      const result = f(stmt, i);
      // trace(fname, result);
      return check(fname, result, null, stmt);
    };
  })();

  sqlite3.bind_parameter_name = (function () {
    const fname = "sqlite3_bind_parameter_name";
    const f = Module.cwrap(fname, ...decl("n:s"));
    return function (stmt, i) {
      // verifyStatement(stmt);
      const result = f(stmt, i);
      // trace(fname, result);
      return result;
    };
  })();

  sqlite3.bind_text = (function () {
    const fname = "sqlite3_bind_text";
    const f = Module.cwrap(fname, ...decl("nnnnn:n"));
    return function (stmt, i, value) {
      // verifyStatement(stmt);
      const ptr = createUTF8(value);
      const result = f(stmt, i, ptr, -1, sqliteFreeAddress);
      // trace(fname, result);
      return check(fname, result, null, stmt);
    };
  })();

  sqlite3.changes = (function () {
    const fname = "sqlite3_changes";
    const f = Module.cwrap(fname, ...decl("n:n"));
    return function (db) {
      verifyDatabase(db);
      const result = f(db);
      // trace(fname, result);
      return result;
    };
  })();

  sqlite3.close = (function () {
    const fname = "sqlite3_close";
    const f = Module.cwrap(fname, ...decl("n:n"), { async });
    return async function (db) {
      verifyDatabase(db);
      const result = await f(db);
      databases.delete(db);
      return check(fname, result, db);
    };
  })();

  sqlite3.column = function (stmt, iCol) {
    verifyStatement(stmt);
    const type = sqlite3.column_type(stmt, iCol);
    switch (type) {
      case SQLite.SQLITE_BLOB:
        return sqlite3.column_blob(stmt, iCol);
      case SQLite.SQLITE_FLOAT:
        return sqlite3.column_double(stmt, iCol);
      case SQLite.SQLITE_INTEGER: {
        const lo32 = sqlite3.column_int(stmt, iCol);
        const hi32 = Module.getTempRet0();
        return cvt32x2AsSafe(lo32, hi32);
      }
      case SQLite.SQLITE_NULL:
        return null;
      case SQLite.SQLITE_TEXT:
        return sqlite3.column_text(stmt, iCol);
      default:
        throw new SQLiteError("unknown type", type);
    }
  };

  sqlite3.column_blob = (function () {
    const fname = "sqlite3_column_blob";
    const f = Module.cwrap(fname, ...decl("nn:n"));
    return function (stmt, iCol) {
      verifyStatement(stmt);
      const nBytes = sqlite3.column_bytes(stmt, iCol);
      const address = f(stmt, iCol);
      const result = Module.HEAPU8.subarray(address, address + nBytes);
      // trace(fname, result);
      return result;
    };
  })();

  sqlite3.column_bytes = (function () {
    const fname = "sqlite3_column_bytes";
    const f = Module.cwrap(fname, ...decl("nn:n"));
    return function (stmt, iCol) {
      verifyStatement(stmt);
      const result = f(stmt, iCol);
      // trace(fname, result);
      return result;
    };
  })();

  sqlite3.column_count = (function () {
    const fname = "sqlite3_column_count";
    const f = Module.cwrap(fname, ...decl("n:n"));
    return function (stmt) {
      verifyStatement(stmt);
      const result = f(stmt);
      // trace(fname, result);
      return result;
    };
  })();

  sqlite3.column_double = (function () {
    const fname = "sqlite3_column_double";
    const f = Module.cwrap(fname, ...decl("nn:n"));
    return function (stmt, iCol) {
      verifyStatement(stmt);
      const result = f(stmt, iCol);
      // trace(fname, result);
      return result;
    };
  })();

  sqlite3.column_int = (function () {
    // Retrieve int64 but use only the lower 32 bits. The upper 32-bits are
    // accessible with Module.getTempRet0().
    const fname = "sqlite3_column_int64";
    const f = Module.cwrap(fname, ...decl("nn:n"));
    return function (stmt, iCol) {
      verifyStatement(stmt);
      const result = f(stmt, iCol);
      // trace(fname, result);
      return result;
    };
  })();

  sqlite3.column_int64 = (function () {
    const fname = "sqlite3_column_int64";
    const f = Module.cwrap(fname, ...decl("nn:n"));
    return function (stmt, iCol) {
      verifyStatement(stmt);
      const lo32 = f(stmt, iCol);
      const hi32 = Module.getTempRet0();
      const result = cvt32x2ToBigInt(lo32, hi32);
      // trace(fname, result);
      return result;
    };
  })();

  sqlite3.column_name = (function () {
    const fname = "sqlite3_column_name";
    const f = Module.cwrap(fname, ...decl("nn:s"));
    return function (stmt, iCol) {
      verifyStatement(stmt);
      const result = f(stmt, iCol);
      // trace(fname, result);
      return result;
    };
  })();

  sqlite3.column_names = function (stmt) {
    const columns = [];
    const nColumns = sqlite3.column_count(stmt);
    for (let i = 0; i < nColumns; ++i) {
      columns.push(sqlite3.column_name(stmt, i));
    }
    return columns;
  };

  sqlite3.column_text = (function () {
    const fname = "sqlite3_column_text";
    const f = Module.cwrap(fname, ...decl("nn:s"));
    return function (stmt, iCol) {
      verifyStatement(stmt);
      const result = f(stmt, iCol);
      // trace(fname, result);
      return result;
    };
  })();

  sqlite3.column_type = (function () {
    const fname = "sqlite3_column_type";
    const f = Module.cwrap(fname, ...decl("nn:n"));
    return function (stmt, iCol) {
      verifyStatement(stmt);
      const result = f(stmt, iCol);
      // trace(fname, result);
      return result;
    };
  })();

  sqlite3.create_function = function (
    db,
    zFunctionName,
    nArg,
    eTextRep,
    pApp,
    xFunc,
    xStep,
    xFinal
  ) {
    verifyDatabase(db);
    if (xFunc && !xStep && !xFinal) {
      const result = Module.createFunction(
        db,
        zFunctionName,
        nArg,
        eTextRep,
        pApp,
        xFunc
      );
      return check("sqlite3_create_function", result, db);
    }

    if (!xFunc && xStep && xFinal) {
      const result = Module.createAggregate(
        db,
        zFunctionName,
        nArg,
        eTextRep,
        pApp,
        xStep,
        xFinal
      );
      return check("sqlite3_create_function", result, db);
    }

    throw new SQLiteError("invalid function combination", SQLite.SQLITE_MISUSE);
  };

  sqlite3.create_module = function (db, zName, module, appData) {
    verifyDatabase(db);
    const result = Module.createModule(db, zName, module, appData);
    return check("sqlite3_create_module", result, db);
  };

  sqlite3.data_count = (function () {
    const fname = "sqlite3_data_count";
    const f = Module.cwrap(fname, ...decl("n:n"));
    return function (stmt) {
      verifyStatement(stmt);
      const result = f(stmt);
      // trace(fname, result);
      return result;
    };
  })();

  sqlite3.declare_vtab = (function () {
    const fname = "sqlite3_declare_vtab";
    const f = Module.cwrap(fname, ...decl("ns:n"));
    return function (pVTab, zSQL) {
      const result = f(pVTab, zSQL);
      return check("sqlite3_declare_vtab", result);
    };
  })();

  sqlite3.exec = async function (db, sql, callback) {
    for await (const stmt of sqlite3.statements(db, sql)) {
      let columns;
      while ((await sqlite3.step(stmt)) === SQLite.SQLITE_ROW) {
        if (callback) {
          columns = columns ?? sqlite3.column_names(stmt);
          const row = sqlite3.row(stmt);
          await callback(row, columns);
        }
      }
    }
    return SQLite.SQLITE_OK;
  };

  sqlite3.finalize = (function () {
    const fname = "sqlite3_finalize";
    const f = Module.cwrap(fname, ...decl("n:n"), { async });
    return async function (stmt) {
      if (!mapStmtToDB.has(stmt)) {
        return SQLite.SQLITE_MISUSE;
      }
      const result = await f(stmt);

      // const db = mapStmtToDB.get(stmt);
      mapStmtToDB.delete(stmt);

      // Don't throw on error here. Typically the error has already been
      // thrown and finalize() is part of the cleanup.
      return result;
    };
  })();

  sqlite3.get_autocommit = (function () {
    const fname = "sqlite3_get_autocommit";
    const f = Module.cwrap(fname, ...decl("n:n"));
    return function (db) {
      const result = f(db);
      return result;
    };
  })();

  sqlite3.libversion = (function () {
    const fname = "sqlite3_libversion";
    const f = Module.cwrap(fname, ...decl(":s"));
    return function () {
      const result = f();
      return result;
    };
  })();

  sqlite3.libversion_number = (function () {
    const fname = "sqlite3_libversion_number";
    const f = Module.cwrap(fname, ...decl(":n"));
    return function () {
      const result = f();
      return result;
    };
  })();

  sqlite3.limit = (function () {
    const fname = "sqlite3_limit";
    const f = Module.cwrap(fname, ...decl("nnn:n"));
    return function (db, id, newVal) {
      const result = f(db, id, newVal);
      return result;
    };
  })();

  sqlite3.open_v2 = (function () {
    const fname = "sqlite3_open_v2";
    const f = Module.cwrap(fname, ...decl("snnn:n"), { async });
    return async function (zFilename, flags, zVfs) {
      flags = flags || SQLite.SQLITE_OPEN_CREATE | SQLite.SQLITE_OPEN_READWRITE;
      zVfs = createUTF8(zVfs);
      const result = await f(zFilename, tmpPtr[0], flags, zVfs);

      const db = Module.getValue(tmpPtr[0], "*");
      databases.add(db);
      Module._sqlite3_free(zVfs);

      check(fname, result);
      return db;
    };
  })();

  sqlite3.register_extensions = (function () {
    return async function (db) {
      Module.ccall("RegisterExtensionFunctions", "void", ["number"], [db]);
      Module.ccall("sqlite3Fts5BetterTrigramInit", "void", ["number"], [db]);
      Module.ccall("sqlite3Fts5HtmlInit", "void", ["number"], [db]);
    };
  })();

  sqlite3.prepare_v2 = (function () {
    const fname = "sqlite3_prepare_v2";
    const f = Module.cwrap(fname, ...decl("nnnnn:n"), { async });
    return async function (db, sql) {
      const result = await f(db, sql, -1, tmpPtr[0], tmpPtr[1]);
      check(fname, result, db);

      const stmt = Module.getValue(tmpPtr[0], "*");
      if (stmt) {
        mapStmtToDB.set(stmt, db);
        return { stmt, sql: Module.getValue(tmpPtr[1], "*") };
      }
      return null;
    };
  })();

  sqlite3.progress_handler = function (db, nProgressOps, handler, userData) {
    verifyDatabase(db);
    Module.progressHandler(db, nProgressOps, handler, userData);
  };

  sqlite3.reset = (function () {
    const fname = "sqlite3_reset";
    const f = Module.cwrap(fname, ...decl("n:n"), { async });
    return async function (stmt) {
      verifyStatement(stmt);
      const result = await f(stmt);
      return check(fname, result, null, stmt);
    };
  })();

  sqlite3.result = function (context, value) {
    switch (typeof value) {
      case "number":
        if (value === (value | 0)) {
          sqlite3.result_int(context, value);
        } else {
          sqlite3.result_double(context, value);
        }
        break;
      case "string":
        sqlite3.result_text(context, value);
        break;
      default:
        if (value instanceof Uint8Array || Array.isArray(value)) {
          sqlite3.result_blob(context, value);
        } else if (value === null) {
          sqlite3.result_null(context);
        } else if (typeof value === "bigint") {
          return sqlite3.result_int64(context, value);
        } else {
          console.warn("unknown result converted to null", value);
          sqlite3.result_null(context);
        }
        break;
    }
  };

  sqlite3.result_blob = (function () {
    const fname = "sqlite3_result_blob";
    const f = Module.cwrap(fname, ...decl("nnnn:n"));
    return function (context, value) {
      // @ts-ignore
      const byteLength = value.byteLength ?? value.length;
      const ptr = Module._sqlite3_malloc(byteLength);
      Module.HEAPU8.subarray(ptr).set(value);
      f(context, ptr, byteLength, sqliteFreeAddress); // void return
    };
  })();

  sqlite3.result_double = (function () {
    const fname = "sqlite3_result_double";
    const f = Module.cwrap(fname, ...decl("nn:n"));
    return function (context, value) {
      f(context, value); // void return
    };
  })();

  sqlite3.result_int = (function () {
    const fname = "sqlite3_result_int";
    const f = Module.cwrap(fname, ...decl("nn:n"));
    return function (context, value) {
      f(context, value); // void return
    };
  })();

  sqlite3.result_int64 = (function () {
    const fname = "sqlite3_result_int64";
    const f = Module.cwrap(fname, ...decl("nnn:n"));
    return function (context, value) {
      if (value > MAX_INT64 || value < MIN_INT64) return SQLite.SQLITE_RANGE;

      const lo32 = value & 0xffffffffn;
      const hi32 = value >> 32n;
      f(context, Number(lo32), Number(hi32)); // void return
    };
  })();

  sqlite3.result_null = (function () {
    const fname = "sqlite3_result_null";
    const f = Module.cwrap(fname, ...decl("n:n"));
    return function (context) {
      f(context); // void return
    };
  })();

  sqlite3.result_text = (function () {
    const fname = "sqlite3_result_text";
    const f = Module.cwrap(fname, ...decl("nnnn:n"));
    return function (context, value) {
      const ptr = createUTF8(value);
      f(context, ptr, -1, sqliteFreeAddress); // void return
    };
  })();

  sqlite3.row = function (stmt) {
    const row = [];
    const nColumns = sqlite3.data_count(stmt);
    for (let i = 0; i < nColumns; ++i) {
      const value = sqlite3.column(stmt, i);

      // Copy blob if aliasing volatile WebAssembly memory. This avoids an
      // unnecessary copy if users monkey patch column_blob to copy.
      // @ts-ignore
      row.push(value?.buffer === Module.HEAPU8.buffer ? value.slice() : value);
    }
    return row;
  };

  sqlite3.set_authorizer = function (db, authFunction, userData) {
    verifyDatabase(db);
    const result = Module.setAuthorizer(db, authFunction, userData);
    return check("sqlite3_set_authorizer", result, db);
  };

  sqlite3.sql = (function () {
    const fname = "sqlite3_sql";
    const f = Module.cwrap(fname, ...decl("n:s"));
    return function (stmt) {
      verifyStatement(stmt);
      const result = f(stmt);
      // trace(fname, result);
      return result;
    };
  })();

  sqlite3.statements = function (db, sql) {
    return (async function* () {
      const str = sqlite3.str_new(db, sql);
      let prepared = { stmt: null, sql: sqlite3.str_value(str) };
      try {
        while ((prepared = await sqlite3.prepare_v2(db, prepared.sql))) {
          // console.log(sqlite3.sql(prepared.stmt));
          yield prepared.stmt;
          sqlite3.finalize(prepared.stmt);
          prepared.stmt = null;
        }
      } finally {
        if (prepared?.stmt) {
          sqlite3.finalize(prepared.stmt);
        }
        sqlite3.str_finish(str);
      }
    })();
  };

  sqlite3.step = (function () {
    const fname = "sqlite3_step";
    const f = Module.cwrap(fname, ...decl("n:n"), { async });
    return async function (stmt) {
      // verifyStatement(stmt);
      const result = await f(stmt);
      return check(fname, result, null, stmt, [
        SQLite.SQLITE_ROW,
        SQLite.SQLITE_DONE
      ]);
    };
  })();

  sqlite3.last_insert_rowid = (function () {
    const fname = "sqlite3_last_insert_rowid";
    const f = Module.cwrap(fname, ...decl("n:n"));
    return function (db) {
      verifyDatabase(db);
      const result = f(db);
      return result;
    };
  })();

  // Duplicate some of the SQLite dynamic string API but without
  // calling SQLite (except for memory allocation). We need some way
  // to transfer Javascript strings and might as well use an API
  // that mimics the SQLite API.
  let stringId = 0;
  const strings = new Map();

  sqlite3.str_new = function (db, s = "") {
    const sBytes = Module.lengthBytesUTF8(s);
    const str = stringId++ & 0xffffffff;
    const data = {
      offset: Module._sqlite3_malloc(sBytes + 1),
      bytes: sBytes
    };
    strings.set(str, data);
    Module.stringToUTF8(s, data.offset, data.bytes + 1);
    return str;
  };

  sqlite3.str_appendall = function (str, s) {
    if (!strings.has(str)) {
      throw new SQLiteError("not a string", SQLite.SQLITE_MISUSE);
    }
    const data = strings.get(str);

    const sBytes = Module.lengthBytesUTF8(s);
    const newBytes = data.bytes + sBytes;
    const newOffset = Module._sqlite3_malloc(newBytes + 1);
    const newArray = Module.HEAPU8.subarray(
      newOffset,
      newOffset + newBytes + 1
    );
    newArray.set(Module.HEAPU8.subarray(data.offset, data.offset + data.bytes));
    Module.stringToUTF8(s, newOffset + data.bytes, sBytes + 1);

    Module._sqlite3_free(data.offset);
    data.offset = newOffset;
    data.bytes = newBytes;
    strings.set(str, data);
  };

  sqlite3.str_finish = function (str) {
    if (!strings.has(str)) {
      throw new SQLiteError("not a string", SQLite.SQLITE_MISUSE);
    }
    const data = strings.get(str);
    strings.delete(str);
    Module._sqlite3_free(data.offset);
  };

  sqlite3.str_value = function (str) {
    if (!strings.has(str)) {
      throw new SQLiteError("not a string", SQLite.SQLITE_MISUSE);
    }
    return strings.get(str).offset;
  };

  sqlite3.user_data = function (context) {
    return Module.getFunctionUserData(context);
  };

  sqlite3.value = function (pValue) {
    const type = sqlite3.value_type(pValue);
    switch (type) {
      case SQLite.SQLITE_BLOB:
        return sqlite3.value_blob(pValue);
      case SQLite.SQLITE_FLOAT:
        return sqlite3.value_double(pValue);
      case SQLite.SQLITE_INTEGER: {
        const lo32 = sqlite3.value_int(pValue);
        const hi32 = Module.getTempRet0();
        return cvt32x2AsSafe(lo32, hi32);
      }
      case SQLite.SQLITE_NULL:
        return null;
      case SQLite.SQLITE_TEXT:
        return sqlite3.value_text(pValue);
      default:
        throw new SQLiteError("unknown type", type);
    }
  };

  sqlite3.value_blob = (function () {
    const fname = "sqlite3_value_blob";
    const f = Module.cwrap(fname, ...decl("n:n"));
    return function (pValue) {
      const nBytes = sqlite3.value_bytes(pValue);
      const address = f(pValue);
      const result = Module.HEAPU8.subarray(address, address + nBytes);
      // trace(fname, result);
      return result;
    };
  })();

  sqlite3.value_bytes = (function () {
    const fname = "sqlite3_value_bytes";
    const f = Module.cwrap(fname, ...decl("n:n"));
    return function (pValue) {
      const result = f(pValue);
      // trace(fname, result);
      return result;
    };
  })();

  sqlite3.value_double = (function () {
    const fname = "sqlite3_value_double";
    const f = Module.cwrap(fname, ...decl("n:n"));
    return function (pValue) {
      const result = f(pValue);
      // trace(fname, result);
      return result;
    };
  })();

  sqlite3.value_int = (function () {
    const fname = "sqlite3_value_int64";
    const f = Module.cwrap(fname, ...decl("n:n"));
    return function (pValue) {
      const result = f(pValue);
      // trace(fname, result);
      return result;
    };
  })();

  sqlite3.value_int64 = (function () {
    const fname = "sqlite3_value_int64";
    const f = Module.cwrap(fname, ...decl("n:n"));
    return function (pValue) {
      const lo32 = f(pValue);
      const hi32 = Module.getTempRet0();
      const result = cvt32x2ToBigInt(lo32, hi32);
      // trace(fname, result);
      return result;
    };
  })();

  sqlite3.value_text = (function () {
    const fname = "sqlite3_value_text";
    const f = Module.cwrap(fname, ...decl("n:s"));
    return function (pValue) {
      const result = f(pValue);
      // trace(fname, result);
      return result;
    };
  })();

  sqlite3.value_type = (function () {
    const fname = "sqlite3_value_type";
    const f = Module.cwrap(fname, ...decl("n:n"));
    return function (pValue) {
      const result = f(pValue);
      // trace(fname, result);
      return result;
    };
  })();

  sqlite3.vfs_register = function (vfs, makeDefault) {
    const result = Module.registerVFS(vfs, makeDefault);
    return check("sqlite3_vfs_register", result);
  };

  function check(
    fname,
    result,
    db = null,
    stmt = null,
    allowed = [SQLite.SQLITE_OK]
  ) {
    // trace(fname, result);
    if (allowed.includes(result)) return result;
    db = db || (stmt !== null ? mapStmtToDB.get(stmt) : null);
    const message = db
      ? Module.ccall("sqlite3_errmsg", "string", ["number"], [db])
      : fname;
    throw new SQLiteError(message, result);
  }

  return sqlite3;
}

function trace(...args) {
  // const date = new Date();
  // const t = date.getHours().toString().padStart(2, '0') + ':' +
  //           date.getMinutes().toString().padStart(2, '0') + ':' +
  //           date.getSeconds().toString().padStart(2, '0') + '.' +
  //           date.getMilliseconds().toString().padStart(3, '0');
  // console.debug(t, ...args);
}

// Helper function to use a more compact signature specification.
function decl(s) {
  const result = [];
  const m = s.match(/([ns@]*):([nsv@])/);
  switch (m[2]) {
    case "n":
      result.push("number");
      break;
    case "s":
      result.push("string");
      break;
    case "v":
      result.push(null);
      break;
  }

  const args = [];
  for (let c of m[1]) {
    switch (c) {
      case "n":
        args.push("number");
        break;
      case "s":
        args.push("string");
        break;
    }
  }
  result.push(args);
  return result;
}
