'use strict';

const T = 't';
const V = 'v';

const promise = Promise;
const GLOBAL = globalThis;
const WINDOW = GLOBAL.window;
const startInterval = (callback, sec, immediate) => {
  return setInterval(callback, sec * 1e3);
};
const stopInterval = clearInterval;
const isUndefined = (thing) => thing == void 0;
const ifNotUndefined = (value, then, otherwise) =>
  isUndefined(value) ? otherwise?.() : then(value);
const isArray = (thing) => Array.isArray(thing);
const size = (arrayOrString) => arrayOrString.length;
const promiseNew = (resolver) => new promise(resolver);
const promiseAll = async (promises) => promise.all(promises);
const errorNew = (message) => {
  throw new Error(message);
};

const arrayMap = (array, cb) => array.map(cb);
const arrayClear = (array, to) => array.splice(0, to);
const arrayPush = (array, ...values) => array.push(...values);
const arrayShift = (array) => array.shift();

const object = Object;
const getPrototypeOf = (obj) => object.getPrototypeOf(obj);
const objEntries = object.entries;
const isObject = (obj) =>
  !isUndefined(obj) &&
  ifNotUndefined(
    getPrototypeOf(obj),
    (objPrototype) =>
      objPrototype == object.prototype ||
      isUndefined(getPrototypeOf(objPrototype)),

    /* istanbul ignore next */
    () => true,
  );
const objIds = object.keys;
const objFreeze = object.freeze;
const objNew = (entries = []) => object.fromEntries(entries);
const objHas = (obj, id) => id in obj;
const objToArray = (obj, cb) =>
  arrayMap(objEntries(obj), ([id, value]) => cb(value, id));
const objSize = (obj) => size(objIds(obj));
const objIsEmpty = (obj) => isObject(obj) && objSize(obj) == 0;

const collHas = (coll, keyOrValue) => coll?.has(keyOrValue) ?? false;
const collDel = (coll, keyOrValue) => coll?.delete(keyOrValue);

const mapNew = (entries) => new Map(entries);
const mapGet = (map, key) => map?.get(key);
const mapSet = (map, key, value) =>
  isUndefined(value) ? (collDel(map, key), map) : map?.set(key, value);
const mapEnsure = (map, key, getDefaultValue, hadExistingValue) => {
  if (!collHas(map, key)) {
    mapSet(map, key, getDefaultValue());
  }
  return mapGet(map, key);
};

const scheduleRunning = mapNew();
const scheduleActions = mapNew();
const getStoreFunctions = (persist = 1 /* StoreOnly */, store) =>
  persist != 1 /* StoreOnly */ && store.isMergeable()
    ? [
        1,
        store.getMergeableContent,
        store.getTransactionMergeableChanges,
        ([[changedTables], [changedValues]]) =>
          !objIsEmpty(changedTables) || !objIsEmpty(changedValues),
        store.setDefaultContent,
      ]
    : persist != 2 /* MergeableStoreOnly */
      ? [
          0,
          store.getContent,
          store.getTransactionChanges,
          ([changedTables, changedValues]) =>
            !objIsEmpty(changedTables) || !objIsEmpty(changedValues),
          store.setContent,
        ]
      : errorNew('Store type not supported by this Persister');
const createCustomPersister = (
  store,
  getPersisted,
  setPersisted,
  addPersisterListener,
  delPersisterListener,
  onIgnoredError,
  persist,
  extra = {},
  scheduleId = [],
) => {
  let loadSave = 0;
  let loads = 0;
  let saves = 0;
  let action;
  let autoLoadHandle;
  let autoSaveListenerId;
  mapEnsure(scheduleRunning, scheduleId, () => 0);
  mapEnsure(scheduleActions, scheduleId, () => []);
  const [
    isMergeableStore,
    getContent,
    getChanges,
    hasChanges,
    setDefaultContent,
  ] = getStoreFunctions(persist, store);
  const run = async () => {
    /* istanbul ignore else */
    if (!mapGet(scheduleRunning, scheduleId)) {
      mapSet(scheduleRunning, scheduleId, 1);
      while (
        !isUndefined((action = arrayShift(mapGet(scheduleActions, scheduleId))))
      ) {
        try {
          await action();
        } catch (error) {
          /* istanbul ignore next */
          onIgnoredError?.(error);
        }
      }
      mapSet(scheduleRunning, scheduleId, 0);
    }
  };
  const setContentOrChanges = (contentOrChanges) => {
    (isMergeableStore && isArray(contentOrChanges?.[0])
      ? contentOrChanges?.[2] === 1
        ? store.applyMergeableChanges
        : store.setMergeableContent
      : contentOrChanges?.[2] === 1
        ? store.applyChanges
        : store.setContent)(contentOrChanges);
  };
  const load = async (initialContent) => {
    /* istanbul ignore else */
    if (loadSave != 2) {
      loadSave = 1;
      loads++;
      await schedule(async () => {
        try {
          const content = await getPersisted();
          if (isArray(content)) {
            setContentOrChanges(content);
          } else {
            errorNew(`Content is not an array ${content}`);
          }
        } catch (error) {
          onIgnoredError?.(error);
          if (initialContent) {
            setDefaultContent(initialContent);
          }
        }
        loadSave = 0;
      });
    }
    return persister;
  };
  const startAutoLoad = async (initialContent) => {
    await stopAutoLoad().load(initialContent);
    try {
      autoLoadHandle = await addPersisterListener(async (content, changes) => {
        if (changes || content) {
          /* istanbul ignore else */
          if (loadSave != 2) {
            loadSave = 1;
            loads++;
            setContentOrChanges(changes ?? content);
            loadSave = 0;
          }
        } else {
          await load();
        }
      });
    } catch (error) {
      /* istanbul ignore next */
      onIgnoredError?.(error);
    }
    return persister;
  };
  const stopAutoLoad = () => {
    if (autoLoadHandle) {
      delPersisterListener(autoLoadHandle);
      autoLoadHandle = void 0;
    }
    return persister;
  };
  const isAutoLoading = () => !isUndefined(autoLoadHandle);
  const save = async (changes) => {
    /* istanbul ignore else */
    if (loadSave != 1) {
      loadSave = 2;
      saves++;
      await schedule(async () => {
        try {
          await setPersisted(getContent, changes);
        } catch (error) {
          /* istanbul ignore next */
          onIgnoredError?.(error);
        }
        loadSave = 0;
      });
    }
    return persister;
  };
  const startAutoSave = async () => {
    await stopAutoSave().save();
    autoSaveListenerId = store.addDidFinishTransactionListener(() => {
      const changes = getChanges();
      if (hasChanges(changes)) {
        save(changes);
      }
    });
    return persister;
  };
  const stopAutoSave = () => {
    ifNotUndefined(autoSaveListenerId, store.delListener);
    autoSaveListenerId = void 0;
    return persister;
  };
  const isAutoSaving = () => !isUndefined(autoSaveListenerId);
  const schedule = async (...actions) => {
    arrayPush(mapGet(scheduleActions, scheduleId), ...actions);
    await run();
    return persister;
  };
  const getStore = () => store;
  const destroy = () => {
    arrayClear(mapGet(scheduleActions, scheduleId));
    return stopAutoLoad().stopAutoSave();
  };
  const getStats = () => ({loads, saves});
  const persister = {
    load,
    startAutoLoad,
    stopAutoLoad,
    isAutoLoading,
    save,
    startAutoSave,
    stopAutoSave,
    isAutoSaving,
    schedule,
    getStore,
    destroy,
    getStats,
    ...extra,
  };
  return objFreeze(persister);
};

const OBJECT_STORE_NAMES = [T, V];
const KEY_PATH = {keyPath: 'k'};
const objectStoreMatch = async (objectStore, obj) => {
  const actions = objToArray(obj, (v, k) =>
    execObjectStore(objectStore, 'put', {k, v}),
  );
  arrayMap(await execObjectStore(objectStore, 'getAllKeys'), (id) =>
    objHas(obj, id)
      ? 0
      : arrayPush(actions, execObjectStore(objectStore, 'delete', id)),
  );
  await promiseAll(actions);
};
const execObjectStore = async (objectStore, func, arg) =>
  promiseNew((resolve, reject) => {
    const request = objectStore[func](arg);
    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(`objectStore.${func} error`);
  });
const createIndexedDbPersister = (
  store,
  dbName,
  autoLoadIntervalSeconds = 1,
  onIgnoredError,
) => {
  const forObjectStores = async (forObjectStore, params = [], create = 0) =>
    promiseNew((resolve, reject) => {
      const request = (WINDOW ? WINDOW.indexedDB : indexedDB).open(
        dbName,
        create ? 2 : void 0,
      );
      request.onupgradeneeded = () =>
        create &&
        arrayMap(OBJECT_STORE_NAMES, (objectStoreName) => {
          try {
            request.result.createObjectStore(objectStoreName, KEY_PATH);
          } catch {}
        });
      request.onsuccess = async () => {
        try {
          const transaction = request.result.transaction(
            OBJECT_STORE_NAMES,
            'readwrite',
          );
          const result = await promiseAll(
            arrayMap(
              OBJECT_STORE_NAMES,
              async (objectStoreName, index) =>
                await forObjectStore(
                  transaction.objectStore(objectStoreName),
                  params[index],
                ),
            ),
          );
          request.result.close();
          resolve(result);
        } catch (e) {
          request.result.close();
          reject(e);
        }
      };
      request.onerror = () => reject('indexedDB.open error');
    });
  const getPersisted = async () =>
    await forObjectStores(async (objectStore) =>
      objNew(
        arrayMap(await execObjectStore(objectStore, 'getAll'), ({k, v}) => [
          k,
          v,
        ]),
      ),
    );
  const setPersisted = async (getContent) =>
    await forObjectStores(
      async (objectStore, content) =>
        await objectStoreMatch(objectStore, content),
      getContent(),
      1,
    );
  const addPersisterListener = (listener) =>
    startInterval(listener, autoLoadIntervalSeconds);
  const delPersisterListener = (interval) => stopInterval(interval);
  return createCustomPersister(
    store,
    getPersisted,
    setPersisted,
    addPersisterListener,
    delPersisterListener,
    onIgnoredError,
    1,
    // StoreOnly,
    {getDbName: () => dbName},
  );
};

exports.createIndexedDbPersister = createIndexedDbPersister;
exports.objectStoreMatch = objectStoreMatch;
