import _ from '../../_.js';
import iterate from '../../fn/loop/iterate.js';
import log from '../../fn/browser/log.js';
import isObject from '../../fn/type/isObject.js';
import isArray from '../../fn/type/isArray.js';
import extend from '../../fn/object/extend.js';
import transactionDone from '../functions/transactionDone.js';
import getPrimaryKey from '../functions/getPrimaryKey.js';
import requestToPromise from '../functions/requestToPromise.js';
import transformResult from '../functions/transformResult.js';
import fieldsFromFilter from '../functions/fieldsFromFilter.js';
import bbnDbObjectProxy from './ObjectProxy.js';
const idb = globalThis.indexedDB ||
    globalThis.webkitIndexedDB ||
    globalThis.mozIndexedDB ||
    globalThis.OIndexedDB ||
    globalThis.msIndexedDB;
class DbObject {
    _dbName;
    lastErr = null;
    constructor(dbName) {
        this._dbName = dbName;
    }
    get dbName() {
        return this._dbName;
    }
    hasMissingStores(database, conn) {
        const structures = db._structures[database] || {};
        return Object.keys(structures).some(storeName => !conn.objectStoreNames.contains(storeName));
    }
    async getConnection(database = '') {
        let conn = db._connections[database || this.dbName];
        if (!conn) {
            debugger;
            log("Opening connection through getConnection to database: " + (database || this.dbName));
            await db.open(database || this.dbName);
            if (!db._connections[database || this.dbName]) {
                throw new Error(_('The database %s is not open', this.dbName));
            }
            conn = db._connections[database || this.dbName];
        }
        return conn;
    }
    get structure() {
        const structure = db._structures[this.dbName];
        if (!structure) {
            throw new Error(_('No structure defined for database %s', this.dbName));
        }
        return structure;
    }
    async getStore(table, mode) {
        const connection = await this.getConnection();
        if (!this.structure[table]) {
            log("No structure for table: " + table, this.structure, db._structures);
            throw new Error(_('Table %s is not defined in database structure %s', table, this.dbName));
        }
        if (!connection.objectStoreNames.contains(table)) {
            throw new Error(_('Table %s does not exist in database %s. Schema upgrade required.', table, this.dbName));
        }
        const tx = connection.transaction([table], mode);
        tx.onabort = () => {
            this.lastErr = tx.error;
            log("Abort in store transaction: " + table, tx.error);
        };
        tx.onerror = () => {
            this.lastErr = tx.error;
            log("Error in store transaction: " + table, tx.error);
        };
        try {
            const os = tx.objectStore(table);
            return [tx, os];
        }
        catch (e) {
            this.lastErr = e;
            log("Error getting object store: " + table, e);
            throw new Error(_('Error getting table %s from database %s', table, this.dbName));
        }
    }
    lastError() {
        return this.lastErr;
    }
    close() {
        return db.close(this.dbName);
    }
    async insert(table, data) {
        const rows = Array.isArray(data) ? data : [data];
        if (!rows.length) {
            return 0;
        }
        const [tx, store] = await this.getStore(table, 'readwrite');
        let inserted = 0;
        for (const row of rows) {
            const req = store.put(row);
            req.onsuccess = () => {
                inserted++;
            };
            req.onerror = () => {
                this.lastErr = req.error;
                log(req.error);
            };
        }
        await transactionDone(tx);
        return inserted;
    }
    async update(table, data, where, replace = false) {
        const rows = await this.selectAll(table, [], where);
        if (!rows.length) {
            return 0;
        }
        const structure = this.structure[table];
        const primary = getPrimaryKey(structure);
        if (Array.isArray(primary)) {
            throw new Error(_('Composite primary keys are not supported by this update implementation'));
        }
        const [tx, store] = await this.getStore(table, 'readwrite');
        let updated = 0;
        for (const row of rows) {
            const nextRow = extend({}, replace ? { [primary]: row[primary] } : row, data);
            if (!(primary in nextRow)) {
                throw new Error(_('No primary key in the data'));
            }
            const req = store.put(nextRow);
            req.onsuccess = () => {
                updated++;
            };
            req.onerror = () => {
                this.lastErr = req.error;
                log(req.error);
            };
        }
        await transactionDone(tx);
        return updated;
    }
    async delete(table, where) {
        const structure = this.structure[table];
        const primary = getPrimaryKey(structure);
        if (Array.isArray(primary)) {
            throw new Error(_('Composite primary keys are not supported by this delete implementation'));
        }
        if (!(primary in where)) {
            throw new Error(_('No primary key in the filter'));
        }
        const [tx, store] = await this.getStore(table, 'readwrite');
        store.delete(where[primary]);
        await transactionDone(tx);
        return 1;
    }
    async selectOne(table, field, where = null, order = null, start = 0, limit = 1) {
        const rows = await this.selectAll(table, [field], where, order, start, limit);
        return rows?.[0]?.[field];
    }
    async select(table, fields = [], where = null, order = null, start = 0) {
        const rows = await this.selectAll(table, fields, where, order, start, 1);
        return rows.length ? rows[0] : null;
    }
    /* async selectAll(
      table: string,
      fields: string[] = [],
      where: unknown = null,
      order: OrderClause = null,
      start = 0,
      limit: number | null = null
    ): Promise<Record<string, unknown>[]> {
      void order;
  
      const [tx, store] = await this.getStore(table, 'readonly');
      const structure = this.structure[table];
      const primary = getPrimaryKey(structure);
      const results: Record<string, unknown>[] = [];
  
      const searchField = isObject(where)
        ? Object.keys(where as Record<string, unknown>)[0]
        : (!where || isArray(where) ? null : primary);
  
      if (!Array.isArray(primary) && searchField === primary) {
        if (Array.isArray((where as any)?.[primary])) {
          const ids = (where as any)[primary] as IDBValidKey[];
          const max = Math.min(ids.length - start, limit ?? ids.length);
          const slice = ids.slice(start, start + max);
  
          for (const id of slice) {
            const row = await requestToPromise<Record<string, unknown> | undefined>(store.get(id));
            const transformed = transformResult(row, fields);
            if (transformed) {
              results.push(transformed);
            }
          }
  
          await transactionDone(tx);
          return results;
        }
  
        const key = isObject(where)
          ? (where as Record<string, unknown>)[primary]
          : where;
  
        const row = await requestToPromise<Record<string, unknown> | undefined>(
          store.get(key as IDBValidKey)
        );
  
        const transformed = transformResult(row, fields);
        if (transformed) {
          results.push(transformed);
        }
  
        await transactionDone(tx);
        return results;
      }
  
      await new Promise<void>((resolve, reject) => {
        const req = store.openCursor();
        let i = 0;
  
        req.onsuccess = (e: Event) => {
          const cursor = (e.target as IDBRequest<IDBCursorWithValue | null>).result;
  
          if (!cursor) {
            resolve();
            return;
          }
  
          const matches = !where || !(globalThis as any).bbn?.fn?.search
            ? true
            : 0 === (globalThis as any).bbn.fn.search([cursor.value], where);
  
          if (matches) {
            if (i >= start) {
              const transformed = transformResult(cursor.value, fields);
              if (transformed) {
                results.push(transformed);
              }
  
              if (limit !== null && results.length >= limit) {
                resolve();
                return;
              }
            }
            i++;
          }
  
          cursor.continue();
        };
  
        req.onerror = () => {
          this.lastErr = req.error;
          log(req.error);
          reject(req.error);
        };
      });
  
      await transactionDone(tx);
      return results;
    } */
    async selectAll(table, fields = [], where = null, order = null, start = 0, limit = null) {
        const [tx, store] = await this.getStore(table, 'readonly');
        const structure = this.structure[table];
        const primary = getPrimaryKey(structure);
        const rows = [];
        const normalizeDir = (dir) => {
            return String(dir).toLowerCase() === 'desc' ? 'desc' : 'asc';
        };
        const normalizeOrder = (order) => {
            if (!order) {
                return [];
            }
            /*
            * Format:
            * { field: 'asc' }
            * { field: 'desc' }
            */
            if (isObject(order) && !Array.isArray(order)) {
                return Object.keys(order).map(field => ({
                    field,
                    dir: normalizeDir(order[field])
                }));
            }
            /*
            * Format:
            * [
            *   { field: 'field', dir: 'asc' },
            *   { field: 'field2', dir: 'desc' }
            * ]
            */
            if (Array.isArray(order)) {
                return order
                    .filter(item => isObject(item) && typeof item.field === 'string')
                    .map(item => ({
                    field: item.field,
                    dir: normalizeDir(item.dir)
                }));
            }
            return [];
        };
        const orderClauses = normalizeOrder(order);
        const compareValues = (a, b) => {
            if (a === b) {
                return 0;
            }
            if (a === undefined || a === null) {
                return -1;
            }
            if (b === undefined || b === null) {
                return 1;
            }
            if (typeof a === 'number' && typeof b === 'number') {
                return a - b;
            }
            if (a instanceof Date && b instanceof Date) {
                return a.getTime() - b.getTime();
            }
            return String(a).localeCompare(String(b));
        };
        const sortRows = (rows) => {
            if (!orderClauses.length) {
                return;
            }
            rows.sort((a, b) => {
                for (const clause of orderClauses) {
                    const cmp = compareValues(a[clause.field], b[clause.field]);
                    if (cmp !== 0) {
                        return clause.dir === 'desc' ? -cmp : cmp;
                    }
                }
                return 0;
            });
        };
        const searchField = isObject(where)
            ? Object.keys(where)[0]
            : (!where || isArray(where) ? null : primary);
        if (!Array.isArray(primary) && searchField === primary) {
            if (Array.isArray(where?.[primary])) {
                const ids = where[primary];
                for (const id of ids) {
                    const row = await requestToPromise(store.get(id));
                    if (row) {
                        rows.push(row);
                    }
                }
                await transactionDone(tx);
                sortRows(rows);
                const sliced = limit === null
                    ? rows.slice(start)
                    : rows.slice(start, start + limit);
                return sliced
                    .map(row => transformResult(row, fields))
                    .filter((row) => !!row);
            }
            const key = isObject(where)
                ? where[primary]
                : where;
            const row = await requestToPromise(store.get(key));
            if (row) {
                rows.push(row);
            }
            await transactionDone(tx);
            return rows
                .map(row => transformResult(row, fields))
                .filter((row) => !!row);
        }
        await new Promise((resolve, reject) => {
            const req = store.openCursor();
            req.onsuccess = (e) => {
                const cursor = e.target.result;
                if (!cursor) {
                    resolve();
                    return;
                }
                const matches = !where || !globalThis.bbn?.fn?.search
                    ? true
                    : 0 === globalThis.bbn.fn.search([cursor.value], where);
                if (matches) {
                    rows.push(cursor.value);
                }
                cursor.continue();
            };
            req.onerror = () => {
                this.lastErr = req.error;
                log(req.error);
                reject(req.error);
            };
        });
        await transactionDone(tx);
        sortRows(rows);
        const sliced = limit === null
            ? rows.slice(start)
            : rows.slice(start, start + limit);
        return sliced
            .map(row => transformResult(row, fields))
            .filter((row) => !!row);
    }
    async getColumnValues(table, field, where = null, order = null, start = 0, limit = null) {
        const rows = await this.selectAll(table, fieldsFromFilter(where, [field]), where, order, start, limit);
        return rows
            .map(row => row[field])
            .filter(v => v !== undefined);
    }
    async count(table, where = null) {
        const [tx, store] = await this.getStore(table, 'readonly');
        const structure = this.structure[table];
        const primary = getPrimaryKey(structure);
        const searchField = isObject(where)
            ? Object.keys(where)[0]
            : (!where || isArray(where) ? null : primary);
        if (!where) {
            const total = await requestToPromise(store.count());
            await transactionDone(tx);
            return total;
        }
        if (!Array.isArray(primary) && searchField === primary) {
            if (Array.isArray(where?.[primary])) {
                const ids = where[primary];
                let total = 0;
                for (const id of ids) {
                    const exists = await requestToPromise(store.getKey(id));
                    if (exists !== undefined) {
                        total++;
                    }
                }
                await transactionDone(tx);
                return total;
            }
            const key = isObject(where)
                ? where[primary]
                : where;
            const total = await requestToPromise(store.count(key));
            await transactionDone(tx);
            return total;
        }
        if (typeof searchField === 'string' &&
            isObject(where) &&
            !Array.isArray(where[searchField]) &&
            store.indexNames.contains(searchField)) {
            const index = store.index(searchField);
            const value = where[searchField];
            const total = await requestToPromise(index.count(IDBKeyRange.only(value)));
            await transactionDone(tx);
            return total;
        }
        let total = 0;
        await new Promise((resolve, reject) => {
            const req = store.openCursor();
            req.onsuccess = (e) => {
                const cursor = e.target.result;
                if (!cursor) {
                    resolve();
                    return;
                }
                const matches = !where || !globalThis.bbn?.fn?.search
                    ? true
                    : 0 === globalThis.bbn.fn.search([cursor.value], where);
                if (matches) {
                    total++;
                }
                cursor.continue();
            };
            req.onerror = () => {
                this.lastErr = req.error;
                log(req.error);
                reject(req.error);
            };
        });
        await transactionDone(tx);
        return total;
    }
    async copyTable(target, table, fields = [], where = null, order = null, start = 0, limit = null) {
        if (!this.structure[table]) {
            throw new Error(_('Source table %s does not exist in structure', table));
        }
        let connection = await this.getConnection();
        if (!connection.objectStoreNames.contains(target)) {
            connection.close();
            await db.add(this.dbName, target, this.structure[table]);
            connection = await this.getConnection();
        }
        if (!connection.objectStoreNames.contains(target)) {
            throw new Error(_('The target table %s does not exist', target));
        }
        const rows = await this.selectAll(table, fields, where, order, start, limit);
        if (!rows.length) {
            return 0;
        }
        const res = await this.insert(target, rows);
        connection.close();
        return res;
    }
    async deleteTable(table) {
        await db.remove(this.dbName, table);
        return true;
    }
}
const db = {
    _structures: {},
    _connections: {},
    _objects: {},
    _stores: {},
    ok: idb !== undefined,
    close(database) {
        const conn = this._connections[database];
        if (conn) {
            conn.close();
            delete this._connections[database];
            log("Closing connection to database: " + database);
        }
        if (this._objects[database]) {
            delete this._objects[database];
        }
    },
    updateStructure(storeName, structure, database) {
        log("fn updateStructure, table: " + storeName + ", in SW? " + !globalThis.document);
        const primary = getPrimaryKey(structure);
        if (!database.objectStoreNames.contains(storeName)) {
            const store = database.createObjectStore(storeName, {
                keyPath: primary
            });
            iterate(structure.keys, (a, n) => {
                if (n !== 'PRIMARY') {
                    store.createIndex(n, a.columns.length > 1 ? a.columns : a.columns[0], { unique: !!a.unique });
                }
            });
        }
    },
    async importStructure(database, structure) {
        log("fn importStructure");
        if (!this._structures[database]) {
            this._structures[database] = {};
        }
        if (!this._connections[database]) {
            await this.open(database);
        }
        const conn = this._connections[database];
        let upgrade = false;
        for (const storeName in structure) {
            if (!structure[storeName]?.keys?.PRIMARY || !structure[storeName]?.fields) {
                log("Error on importStructure with store " + storeName, structure[storeName]);
                throw new Error(_('The database structure for %s is not valid (missing keys, fields, or primary key)', storeName));
            }
            this._structures[database][storeName] = structure[storeName];
            // DB exists already: open temporarily and check if store exists
            const hasStore = conn.objectStoreNames.contains(storeName);
            if (!hasStore) {
                upgrade = true;
            }
        }
        if (upgrade) {
            await this.reopenWithUpgrade(database);
        }
        this.close(database);
    },
    async getExistingVersion(name) {
        const live = this._connections[name];
        if (live) {
            return live.version;
        }
        return new Promise((resolve, reject) => {
            if (!idb) {
                reject(new Error(_('IndexedDB is not available')));
                return;
            }
            const req = idb.open(name);
            req.onsuccess = () => {
                const database = req.result;
                const version = database.version;
                database.close();
                resolve(version);
            };
            req.onupgradeneeded = () => {
                // Database did not exist before; this open created it temporarily.
                // Version is therefore effectively 1.
                const database = req.result;
                const version = database.version;
                database.close();
                resolve(version);
            };
            req.onerror = () => reject(req.error);
        });
    },
    async reopenWithUpgrade(name) {
        log("fn reopenWithUpgrade", this._structures);
        if (!this._structures[name]) {
            throw new Error(_('Impossible to find a structure for the database %s', name));
        }
        const existingVersion = await this.getExistingVersion(name) || 0;
        const nextVersion = existingVersion + 1;
        log(_('Going from version %s to version %s for database %s', existingVersion, nextVersion, name));
        if (this._connections[name]) {
            this.close(name);
        }
        return await this.open(name, nextVersion);
    },
    async open(database, version) {
        log("fn open database " + database + " version " + version, this._structures);
        if (database !== 'bbn') {
            log("THERE SHOULD BE A DEBUGGER!!!!!!!");
            debugger;
        }
        if (!idb) {
            throw new Error(_('IndexedDB is not available'));
        }
        if (navigator.serviceWorker?.controller) {
            return new Promise(resolve => resolve(new bbnDbObjectProxy(database)));
        }
        if (!this._structures[database]) {
            throw new Error(_('Impossible to find a structure for the database %s', database));
        }
        if (this._objects[database] && this._connections[database]) {
            return this._objects[database];
        }
        await new Promise((resolve, reject) => {
            const req = version ? idb.open(database, version) : idb.open(database);
            req.onupgradeneeded = () => {
                const db = req.result;
                this._connections[database] = req.result;
                const dbStructure = this._structures[database] || {};
                iterate(dbStructure, (structure, storeName) => {
                    if (!db.objectStoreNames.contains(storeName)) {
                        this.updateStructure(storeName, structure, db);
                    }
                });
            };
            req.onsuccess = () => {
                this._connections[database] = req.result;
                resolve(req.result);
            };
            req.onerror = () => reject(req.error);
            req.onblocked = (event) => {
                reject(_('open: Upgrade blocked for database %s. Please close other connections to proceed.', database));
            };
        });
        this._objects[database] = new DbObject(database);
        return this._objects[database];
    },
    async add(database, name, structure) {
        log("fn add " + name);
        if (!structure?.keys?.PRIMARY || !structure?.fields) {
            log("Error on add", structure);
            throw new Error(_('The database structure for %s is not valid (missing keys, fields, or primary key)', name));
        }
        if (!this._structures[database]) {
            this._structures[database] = {};
        }
        this._structures[database][name] = structure;
        if (this._connections[database]) {
            throw new Error(_('Database %s is already open. Close it before adding a new store.', database));
        }
        await this.open(database);
        let conn = this._connections[database];
        // DB exists already: open temporarily and check if store exists
        const hasStore = conn.objectStoreNames.contains(name);
        if (!hasStore) {
            const tmp = await this.reopenWithUpgrade(database);
            if (!this._connections[database]) {
                throw new Error(_('The database %s is not open after upgrade', database));
            }
            conn = this._connections[database];
        }
        this.close(database);
        return true;
    },
    async remove(database, name) {
        log("fn remove");
        const currentStructure = this._structures[database];
        if (!currentStructure?.[name]) {
            return;
        }
        const old = currentStructure[name];
        delete currentStructure[name];
        try {
            const conn = this._connections[database];
            if (!conn) {
                return;
            }
            const nextVersion = conn.version + 1;
            this.close(database);
            await new Promise((resolve, reject) => {
                if (!idb) {
                    reject(new Error(_('IndexedDB is not available')));
                    return;
                }
                const req = idb.open(database, nextVersion);
                req.onupgradeneeded = () => {
                    const dbInstance = req.result;
                    if (dbInstance.objectStoreNames.contains(name)) {
                        dbInstance.deleteObjectStore(name);
                        dbInstance.close();
                    }
                };
                req.onsuccess = () => {
                    req.result.close();
                    resolve();
                };
                req.onerror = () => reject(req.error);
                req.onblocked = () => reject(new Error(_('Remove blocked for database %s', database)));
            });
        }
        catch (e) {
            currentStructure[name] = old;
            throw e;
        }
    }
};
export default db;
