'use strict';

var expoSqlite = require('expo-sqlite');

const getTypeOf = (thing) => typeof thing;
const TINYBASE = 'tinybase';
const EMPTY_STRING = '';
const COMMA = ',';
const STRING = getTypeOf(EMPTY_STRING);
const UNDEFINED = '\uFFFC';

const promise = Promise;
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 isString = (thing) => getTypeOf(thing) == STRING;
const isArray = (thing) => Array.isArray(thing);
const slice = (arrayOrString, start, end) => arrayOrString.slice(start, end);
const size = (arrayOrString) => arrayOrString.length;
const promiseAll = async (promises) => promise.all(promises);
const errorNew = (message) => {
  throw new Error(message);
};

const arrayNew = (size2, cb) =>
  arrayMap(new Array(size2).fill(0), (_, index) => cb(index));
const arrayJoin = (array, sep = EMPTY_STRING) => array.join(sep);
const arrayMap = (array, cb) => array.map(cb);
const arrayIsEmpty = (array) => size(array) == 0;
const arrayFilter = (array, cb) => array.filter(cb);
const arrayClear = (array, to) => array.splice(0, to);
const arrayPush = (array, ...values) => array.push(...values);
const arrayShift = (array) => array.shift();

const SINGLE_ROW_ID = '_';
const DEFAULT_ROW_ID_COLUMN_NAME = '_id';
const SELECT = 'SELECT';
const WHERE = 'WHERE';
const TABLE = 'TABLE';
const ALTER_TABLE = 'ALTER ' + TABLE;
const DELETE_FROM = 'DELETE FROM';
const SELECT_STAR_FROM = SELECT + '*FROM';
const PRAGMA = 'pragma_';
const DATA_VERSION = 'data_version';
const SCHEMA_VERSION = 'schema_version';
const FROM = 'FROM ';
const PRAGMA_TABLE = 'pragma_table_';
const getWrappedCommand = (executeCommand, onSqlCommand) =>
  onSqlCommand
    ? async (sql, params) => {
        onSqlCommand(sql, params);
        return await executeCommand(sql, params);
      }
    : executeCommand;
const escapeId = (str) => `"${str.replace(/"/g, '""')}"`;
const getPlaceholders = (array) =>
  arrayJoin(
    arrayMap(array, (_, index) => '$' + (index + 1)),
    COMMA,
  );

const collHas = (coll, keyOrValue) => coll?.has(keyOrValue) ?? false;
const collValues = (coll) => [...(coll?.values() ?? [])];
const collClear = (coll) => coll.clear();
const collDel = (coll, keyOrValue) => coll?.delete(keyOrValue);

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 objMerge = (...objs) => object.assign({}, ...objs);
const objHas = (obj, id) => id in obj;
const objDel = (obj, id) => {
  delete obj[id];
  return obj;
};
const objToArray = (obj, cb) =>
  arrayMap(objEntries(obj), ([id, value]) => cb(value, id));
const objMap = (obj, cb) =>
  objNew(objToArray(obj, (value, id) => [id, cb(value, id)]));
const objValues = (obj) => object.values(obj);
const objSize = (obj) => size(objIds(obj));
const objIsEmpty = (obj) => isObject(obj) && objSize(obj) == 0;

const jsonString = JSON.stringify;
const jsonParse = JSON.parse;
const jsonStringWithUndefined = (obj) =>
  jsonString(obj, (_key, value) => (value === void 0 ? UNDEFINED : value));
const jsonParseWithUndefined = (str) =>
  jsonParse(str, (_key, value) => (value === UNDEFINED ? void 0 : value));

const mapNew = (entries) => new Map(entries);
const mapGet = (map, key) => map?.get(key);
const mapMap = (coll, cb) =>
  arrayMap([...(coll?.entries() ?? [])], ([key, value]) => cb(value, 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 setNew = (entryOrEntries) =>
  new Set(
    isArray(entryOrEntries) || isUndefined(entryOrEntries)
      ? entryOrEntries
      : [entryOrEntries],
  );
const setAdd = (set, value) => set?.add(value);

const getCommandFunctions = (
  databaseExecuteCommand,
  managedTableNames,
  querySchema,
  onIgnoredError,
  columnType,
  orReplace,
  encode,
  decode,
) => {
  const schemaMap = mapNew();
  const canSelect = (tableName, rowIdColumnName) =>
    collHas(mapGet(schemaMap, tableName), rowIdColumnName);
  const refreshSchema = async () => {
    collClear(schemaMap);
    arrayMap(
      await querySchema(databaseExecuteCommand, managedTableNames),
      ({tn, cn}) => setAdd(mapEnsure(schemaMap, tn, setNew), cn),
    );
  };
  const loadTable = async (tableName, rowIdColumnName) =>
    canSelect(tableName, rowIdColumnName)
      ? objNew(
          arrayFilter(
            arrayMap(
              await databaseExecuteCommand(
                SELECT_STAR_FROM + escapeId(tableName),
              ),
              (row) => [
                row[rowIdColumnName],
                decode
                  ? objMap(objDel(row, rowIdColumnName), decode)
                  : objDel(row, rowIdColumnName),
              ],
            ),
            ([rowId, row]) => !isUndefined(rowId) && !objIsEmpty(row),
          ),
        )
      : {};
  const saveTable = async (
    tableName,
    rowIdColumnName,
    content,
    deleteEmptyColumns,
    deleteEmptyTable,
    partial = false,
  ) => {
    const tableCellOrValueIds = setNew();
    objToArray(content ?? {}, (contentRow) =>
      arrayMap(objIds(contentRow ?? {}), (cellOrValueId) =>
        setAdd(tableCellOrValueIds, cellOrValueId),
      ),
    );
    const tableColumnNames = collValues(tableCellOrValueIds);
    if (
      !partial &&
      deleteEmptyTable &&
      arrayIsEmpty(tableColumnNames) &&
      collHas(schemaMap, tableName)
    ) {
      await databaseExecuteCommand('DROP ' + TABLE + escapeId(tableName));
      mapSet(schemaMap, tableName);
      return;
    }
    const tableSchemaColumns = mapGet(schemaMap, tableName);
    const currentColumnNames = setNew(collValues(tableSchemaColumns));
    if (!arrayIsEmpty(tableColumnNames)) {
      if (!collHas(schemaMap, tableName)) {
        await databaseExecuteCommand(
          'CREATE ' +
            TABLE +
            escapeId(tableName) +
            `(${escapeId(rowIdColumnName)}${columnType} PRIMARY KEY${arrayJoin(
              arrayMap(
                tableColumnNames,
                (columnName) => COMMA + escapeId(columnName) + columnType,
              ),
            )});`,
        );
        mapSet(
          schemaMap,
          tableName,
          setNew([rowIdColumnName, ...tableColumnNames]),
        );
      } else {
        await promiseAll(
          arrayMap(
            [rowIdColumnName, ...tableColumnNames],
            async (columnName, index) => {
              if (!collDel(currentColumnNames, columnName)) {
                await databaseExecuteCommand(
                  ALTER_TABLE +
                    escapeId(tableName) +
                    'ADD' +
                    escapeId(columnName) +
                    columnType,
                );
                if (index == 0) {
                  await databaseExecuteCommand(
                    'CREATE UNIQUE INDEX pk ON ' +
                      escapeId(tableName) +
                      `(${escapeId(rowIdColumnName)})`,
                  );
                }
                setAdd(tableSchemaColumns, columnName);
              }
            },
          ),
        );
      }
    }
    await promiseAll([
      ...(!partial && deleteEmptyColumns
        ? arrayMap(collValues(currentColumnNames), async (columnName) => {
            if (columnName != rowIdColumnName) {
              await databaseExecuteCommand(
                ALTER_TABLE +
                  escapeId(tableName) +
                  'DROP' +
                  escapeId(columnName),
              );
              collDel(tableSchemaColumns, columnName);
            }
          })
        : []),
    ]);
    if (partial) {
      if (isUndefined(content)) {
        await databaseExecuteCommand(
          DELETE_FROM + escapeId(tableName) + WHERE + ' true',
        );
      } else {
        await promiseAll(
          objToArray(content, async (row, rowId) => {
            if (isUndefined(row)) {
              await databaseExecuteCommand(
                DELETE_FROM +
                  escapeId(tableName) +
                  WHERE +
                  escapeId(rowIdColumnName) +
                  '=$1',
                [rowId],
              );
            } else if (!arrayIsEmpty(tableColumnNames)) {
              await upsert(
                databaseExecuteCommand,
                tableName,
                rowIdColumnName,
                objIds(row),
                [
                  rowId,
                  ...(encode
                    ? arrayMap(objValues(row), encode)
                    : objValues(row)),
                ],
                orReplace,
              );
            }
          }),
        );
      }
    } else {
      if (!arrayIsEmpty(tableColumnNames)) {
        const changingColumnNames = arrayFilter(
          collValues(mapGet(schemaMap, tableName)),
          (columnName) => columnName != rowIdColumnName,
        );
        const params = [];
        const deleteRowIds = [];
        objToArray(content ?? {}, (row, rowId) => {
          arrayPush(
            params,
            rowId,
            ...arrayMap(changingColumnNames, (cellId) =>
              encode ? encode(row?.[cellId]) : row?.[cellId],
            ),
          );
          arrayPush(deleteRowIds, rowId);
        });
        await upsert(
          databaseExecuteCommand,
          tableName,
          rowIdColumnName,
          changingColumnNames,
          params,
          orReplace,
        );
        await databaseExecuteCommand(
          DELETE_FROM +
            escapeId(tableName) +
            WHERE +
            escapeId(rowIdColumnName) +
            `NOT IN(${getPlaceholders(deleteRowIds)})`,
          deleteRowIds,
        );
      } else if (collHas(schemaMap, tableName)) {
        await databaseExecuteCommand(
          DELETE_FROM + escapeId(tableName) + WHERE + ' true',
        );
      }
    }
  };
  const transaction = async (actions) => {
    let result;
    await databaseExecuteCommand('BEGIN');
    try {
      result = await actions();
    } catch (error) {
      onIgnoredError?.(error);
    }
    await databaseExecuteCommand('END');
    return result;
  };
  return [refreshSchema, loadTable, saveTable, transaction];
};
const upsert = async (
  executeCommand,
  tableName,
  rowIdColumnName,
  changingColumnNames,
  params,
  orReplace = 0,
) =>
  await executeCommand(
    'INSERT ' +
      (orReplace ? 'OR REPLACE ' : EMPTY_STRING) +
      'INTO' +
      escapeId(tableName) +
      '(' +
      escapeId(rowIdColumnName) +
      arrayJoin(
        arrayMap(
          changingColumnNames,
          (columnName) => COMMA + escapeId(columnName),
        ),
      ) +
      ')VALUES' +
      getUpsertPlaceholders(params, size(changingColumnNames) + 1) +
      (orReplace
        ? EMPTY_STRING
        : 'ON CONFLICT(' +
          escapeId(rowIdColumnName) +
          ')DO UPDATE SET' +
          arrayJoin(
            arrayMap(
              changingColumnNames,
              (columnName) =>
                escapeId(columnName) + '=excluded.' + escapeId(columnName),
            ),
            COMMA,
          )),
    arrayMap(params, (param) => param ?? null),
  );
const getUpsertPlaceholders = (array, columnCount) =>
  arrayJoin(
    arrayNew(
      size(array) / columnCount,
      (row) =>
        '(' +
        arrayJoin(
          arrayNew(
            columnCount,
            (column) => '$' + (row * columnCount + column + 1),
          ),
          COMMA,
        ) +
        ')',
    ),
    COMMA,
  );

const createJsonPersister = (
  store,
  executeCommand,
  addPersisterListener,
  delPersisterListener,
  onIgnoredError,
  destroyImpl,
  persist,
  [storeTableName, storeIdColumnName, storeColumnName],
  managedTableNames,
  querySchema,
  thing,
  getThing,
  columnType,
  orReplace,
) => {
  const [refreshSchema, loadTable, saveTable, transaction] =
    getCommandFunctions(
      executeCommand,
      managedTableNames,
      querySchema,
      onIgnoredError,
      columnType,
      orReplace,
    );
  const getPersisted = async () =>
    await transaction(async () => {
      await refreshSchema();
      return jsonParseWithUndefined(
        (await loadTable(storeTableName, storeIdColumnName))[SINGLE_ROW_ID]?.[
          storeColumnName
        ] ?? 'null',
      );
    });
  const setPersisted = async (getContent) =>
    await transaction(async () => {
      await refreshSchema();
      await saveTable(
        storeTableName,
        storeIdColumnName,
        {
          [SINGLE_ROW_ID]: {
            [storeColumnName]: jsonStringWithUndefined(getContent() ?? null),
          },
        },
        true,
        true,
      );
    });
  const destroy = () => {
    persister.stopAutoLoad().stopAutoSave();
    destroyImpl();
    return persister;
  };
  const persister = createCustomPersister(
    store,
    getPersisted,
    setPersisted,
    addPersisterListener,
    delPersisterListener,
    onIgnoredError,
    persist,
    {[getThing]: () => thing, destroy},
    thing,
  );
  return persister;
};

const createTabularPersister = (
  store,
  executeCommand,
  addPersisterListener,
  delPersisterListener,
  onIgnoredError,
  destroyImpl,
  persist,
  [
    tablesLoadConfig,
    tablesSaveConfig,
    [valuesLoad, valuesSave, valuesTableName],
  ],
  managedTableNames,
  querySchema,
  thing,
  getThing,
  columnType,
  orReplace,
  encode,
  decode,
) => {
  const [refreshSchema, loadTable, saveTable, transaction] =
    getCommandFunctions(
      executeCommand,
      managedTableNames,
      querySchema,
      onIgnoredError,
      columnType,
      orReplace,
      encode,
      decode,
    );
  const saveTables = async (tables, partial) =>
    await promiseAll(
      mapMap(
        tablesSaveConfig,
        async (
          [tableName, rowIdColumnName, deleteEmptyColumns, deleteEmptyTable],
          tableId,
        ) => {
          if (!partial || objHas(tables, tableId)) {
            await saveTable(
              tableName,
              rowIdColumnName,
              tables[tableId],
              deleteEmptyColumns,
              deleteEmptyTable,
              partial,
            );
          }
        },
      ),
    );
  const saveValues = async (values, partial) =>
    valuesSave
      ? await saveTable(
          valuesTableName,
          DEFAULT_ROW_ID_COLUMN_NAME,
          {[SINGLE_ROW_ID]: values},
          true,
          true,
          partial,
        )
      : null;
  const loadTables = async () =>
    objNew(
      arrayFilter(
        await promiseAll(
          mapMap(
            tablesLoadConfig,
            async ([tableId, rowIdColumnName], tableName) => [
              tableId,
              await loadTable(tableName, rowIdColumnName),
            ],
          ),
        ),
        (pair) => !objIsEmpty(pair[1]),
      ),
    );
  const loadValues = async () =>
    valuesLoad
      ? (await loadTable(valuesTableName, DEFAULT_ROW_ID_COLUMN_NAME))[
          SINGLE_ROW_ID
        ]
      : {};
  const getPersisted = async () =>
    await transaction(async () => {
      await refreshSchema();
      const tables = await loadTables();
      const values = await loadValues();
      return !objIsEmpty(tables) || !isUndefined(values)
        ? [tables, values]
        : void 0;
    });
  const setPersisted = async (getContent, changes) =>
    await transaction(async () => {
      await refreshSchema();
      if (!isUndefined(changes)) {
        await saveTables(changes[0], true);
        await saveValues(changes[1], true);
      } else {
        const [tables, values] = getContent();
        await saveTables(tables);
        await saveValues(values);
      }
    });
  const destroy = () => {
    persister.stopAutoLoad().stopAutoSave();
    destroyImpl();
    return persister;
  };
  const persister = createCustomPersister(
    store,
    getPersisted,
    setPersisted,
    addPersisterListener,
    delPersisterListener,
    onIgnoredError,
    persist,
    {[getThing]: () => thing, destroy},
    thing,
  );
  return persister;
};

const COLUMN_NAME = 'ColumnName';
const STORE = 'store';
const JSON$1 = 'json';
const STORE_TABLE_NAME = STORE + 'TableName';
const STORE_ID_COLUMN_NAME = STORE + 'Id' + COLUMN_NAME;
const STORE_COLUMN_NAME = STORE + COLUMN_NAME;
const AUTO_LOAD_INTERVAL_SECONDS = 'autoLoadIntervalSeconds';
const ROW_ID_COLUMN_NAME = 'rowId' + COLUMN_NAME;
const TABLE_ID = 'tableId';
const TABLE_NAME = 'tableName';
const DELETE_EMPTY_COLUMNS = 'deleteEmptyColumns';
const DELETE_EMPTY_TABLE = 'deleteEmptyTable';
const DEFAULT_CONFIG = {
  mode: JSON$1,
  [AUTO_LOAD_INTERVAL_SECONDS]: 1,
};
const DEFAULT_TABULAR_VALUES_CONFIG = {
  load: 0,
  save: 0,
  [TABLE_NAME]: TINYBASE + '_values',
};
const getDefaultedConfig = (configOrStoreTableName) =>
  objMerge(
    DEFAULT_CONFIG,
    isString(configOrStoreTableName)
      ? {[STORE_TABLE_NAME]: configOrStoreTableName}
      : (configOrStoreTableName ?? {}),
  );
const getDefaultedTabularConfigMap = (
  configsObj,
  defaultObj,
  tableField,
  exclude,
  then,
) => {
  const configMap = mapNew();
  objToArray(configsObj, (configObj, id) => {
    const defaultedConfig = slice(
      objValues(
        objMerge(
          defaultObj,
          isString(configObj) ? {[tableField]: configObj} : configObj,
        ),
      ),
      0,
      objSize(defaultObj),
    );
    if (!isUndefined(defaultedConfig[0]) && !exclude(id, defaultedConfig[0])) {
      then(id, defaultedConfig[0]);
      mapSet(configMap, id, defaultedConfig);
    }
  });
  return configMap;
};
const getConfigStructures = (configOrStoreTableName) => {
  const config = getDefaultedConfig(configOrStoreTableName);
  const autoLoadIntervalSeconds = config[AUTO_LOAD_INTERVAL_SECONDS];
  if (config.mode == JSON$1) {
    const storeTableName = config[STORE_TABLE_NAME] ?? TINYBASE;
    return [
      1,
      autoLoadIntervalSeconds,
      [
        storeTableName,
        config[STORE_ID_COLUMN_NAME] ?? DEFAULT_ROW_ID_COLUMN_NAME,
        config[STORE_COLUMN_NAME] ?? STORE,
      ],
      setNew(storeTableName),
    ];
  }
  const {tables: {load = {}, save = {}} = {}, values = {}} = config;
  const valuesConfig = slice(
    objValues(objMerge(DEFAULT_TABULAR_VALUES_CONFIG, values)),
    0,
    objSize(DEFAULT_TABULAR_VALUES_CONFIG),
  );
  const valuesTable = valuesConfig[2];
  const managedTableNames = setNew(valuesTable);
  const excludedTableNames = setNew(valuesTable);
  const tabularConfig = [
    getDefaultedTabularConfigMap(
      load,
      {[TABLE_ID]: null, [ROW_ID_COLUMN_NAME]: DEFAULT_ROW_ID_COLUMN_NAME},
      TABLE_ID,
      (tableName) => collHas(excludedTableNames, tableName),
      (tableName) => setAdd(managedTableNames, tableName),
    ),
    getDefaultedTabularConfigMap(
      save,
      {
        [TABLE_NAME]: null,
        [ROW_ID_COLUMN_NAME]: DEFAULT_ROW_ID_COLUMN_NAME,
        [DELETE_EMPTY_COLUMNS]: 0,
        [DELETE_EMPTY_TABLE]: 0,
      },
      TABLE_NAME,
      (_, tableName) => collHas(excludedTableNames, tableName),
      (_, tableName) => setAdd(managedTableNames, tableName),
    ),
    valuesConfig,
  ];
  return [0, autoLoadIntervalSeconds, tabularConfig, managedTableNames];
};

const createCustomSqlitePersister = (
  store,
  configOrStoreTableName,
  rawExecuteCommand,
  addChangeListener,
  delChangeListener,
  onSqlCommand,
  onIgnoredError,
  destroy,
  persist,
  thing,
  getThing = 'getDb',
  orReplace,
) => {
  let dataVersion;
  let schemaVersion;
  let totalChanges;
  const executeCommand = getWrappedCommand(rawExecuteCommand, onSqlCommand);
  const [
    isJson,
    autoLoadIntervalSeconds,
    defaultedConfig,
    managedTableNamesSet,
  ] = getConfigStructures(configOrStoreTableName);
  const addPersisterListener = (listener) => {
    let interval;
    const startPolling = () =>
      (interval = startInterval(async () => {
        try {
          const [{d, s, c}] = await executeCommand(
            // eslint-disable-next-line max-len
            `${SELECT} ${DATA_VERSION} d,${SCHEMA_VERSION} s,TOTAL_CHANGES() c FROM ${PRAGMA}${DATA_VERSION} JOIN ${PRAGMA}${SCHEMA_VERSION}`,
          );
          if (d != dataVersion || s != schemaVersion || c != totalChanges) {
            if (dataVersion != null) {
              listener();
            }
            dataVersion = d;
            schemaVersion = s;
            totalChanges = c;
          }
        } catch {}
      }, autoLoadIntervalSeconds));
    const stopPolling = () => {
      dataVersion = schemaVersion = totalChanges = null;
      stopInterval(interval);
    };
    const listeningHandle = addChangeListener((tableName) => {
      if (managedTableNamesSet.has(tableName)) {
        stopPolling();
        listener();
        startPolling();
      }
    });
    startPolling();
    return () => {
      stopPolling();
      delChangeListener(listeningHandle);
    };
  };
  const delPersisterListener = (stopPollingAndDelUpdateListener) =>
    stopPollingAndDelUpdateListener();
  return (isJson ? createJsonPersister : createTabularPersister)(
    store,
    executeCommand,
    addPersisterListener,
    delPersisterListener,
    onIgnoredError,
    destroy,
    persist,
    defaultedConfig,
    collValues(managedTableNamesSet),
    async (executeCommand2, managedTableNames) =>
      await executeCommand2(
        // eslint-disable-next-line max-len
        `${SELECT} t.name tn,c.name cn ${FROM}${PRAGMA_TABLE}list()t,${PRAGMA_TABLE}info(t.name)c ${WHERE} t.schema='main'AND t.type IN('table','view')AND t.name IN(${getPlaceholders(managedTableNames)})ORDER BY t.name,c.name`,
        managedTableNames,
      ),
    thing,
    getThing,
    EMPTY_STRING,
    orReplace,
    (cellOrValue) =>
      cellOrValue === true ? 1 : cellOrValue === false ? 0 : cellOrValue,
    void 0,
  );
};

const createExpoSqlitePersister = (
  store,
  db,
  configOrStoreTableName,
  onSqlCommand,
  onIgnoredError,
) =>
  createCustomSqlitePersister(
    store,
    configOrStoreTableName,
    async (sql, params = []) => await db.getAllAsync(sql, params),
    (listener) =>
      expoSqlite.addDatabaseChangeListener(({tableName}) =>
        listener(tableName),
      ),
    (subscription) => subscription.remove(),
    onSqlCommand,
    onIgnoredError,
    () => 0,
    3,
    // StoreOrMergeableStore,
    db,
  );

exports.createExpoSqlitePersister = createExpoSqlitePersister;
