/* global chromeLocal workerUtil createChromeStorageDB */
/* exported db */
/*
Initialize a database. There are some problems using IndexedDB in Firefox:
https://www.reddit.com/r/firefox/comments/74wttb/note_to_firefox_webextension_developers_who_use/

Some of them are fixed in FF59:
https://www.reddit.com/r/firefox/comments/7ijuaq/firefox_59_webextensions_can_use_indexeddb_when/
*/
'use strict';

const db = (() => {
  const DATABASE = 'stylish';
  const STORE = 'styles';
  const FALLBACK = 'dbInChromeStorage';
  const dbApi = {
    async exec(...args) {
      dbApi.exec = await tryUsingIndexedDB().catch(useChromeStorage);
      return dbApi.exec(...args);
    },
  };
  return dbApi;

  async function tryUsingIndexedDB() {
    // we use chrome.storage.local fallback if IndexedDB doesn't save data,
    // which, once detected on the first run, is remembered in chrome.storage.local
    // for reliablility and in localStorage for fast synchronous access
    // (FF may block localStorage depending on its privacy options)
    // note that it may throw when accessing the variable
    // https://github.com/openstyles/stylus/issues/615
    if (typeof indexedDB === 'undefined') {
      throw new Error('indexedDB is undefined');
    }
    switch (await getFallback()) {
      case true: throw null;
      case false: break;
      default: await testDB();
    }
    return useIndexedDB();
  }

  async function getFallback() {
    return localStorage[FALLBACK] === 'true' ? true :
      localStorage[FALLBACK] === 'false' ? false :
        chromeLocal.getValue(FALLBACK);
  }

  async function testDB() {
    let e = await dbExecIndexedDB('getAllKeys', IDBKeyRange.lowerBound(1), 1);
    // throws if result is null
    e = e.target.result[0];
    const id = `${performance.now()}.${Math.random()}.${Date.now()}`;
    await dbExecIndexedDB('put', {id});
    e = await dbExecIndexedDB('get', id);
    // throws if result or id is null
    await dbExecIndexedDB('delete', e.target.result.id);
  }

  function useChromeStorage(err) {
    chromeLocal.setValue(FALLBACK, true);
    if (err) {
      chromeLocal.setValue(FALLBACK + 'Reason', workerUtil.cloneError(err));
      console.warn('Failed to access indexedDB. Switched to storage API.', err);
    }
    localStorage[FALLBACK] = 'true';
    return createChromeStorageDB().exec;
  }

  function useIndexedDB() {
    chromeLocal.setValue(FALLBACK, false);
    localStorage[FALLBACK] = 'false';
    return dbExecIndexedDB;
  }

  async function dbExecIndexedDB(method, ...args) {
    const mode = method.startsWith('get') ? 'readonly' : 'readwrite';
    const store = (await open()).transaction([STORE], mode).objectStore(STORE);
    const fn = method === 'putMany' ? putMany : storeRequest;
    return fn(store, method, ...args);
  }

  function storeRequest(store, method, ...args) {
    return new Promise((resolve, reject) => {
      const request = store[method](...args);
      request.onsuccess = resolve;
      request.onerror = reject;
    });
  }

  function putMany(store, _method, items) {
    return Promise.all(items.map(item => storeRequest(store, 'put', item)));
  }

  function open() {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(DATABASE, 2);
      request.onsuccess = () => resolve(request.result);
      request.onerror = reject;
      request.onupgradeneeded = create;
    });
  }

  function create(event) {
    if (event.oldVersion === 0) {
      event.target.result.createObjectStore(STORE, {
        keyPath: 'id',
        autoIncrement: true,
      });
    }
  }
})();