import {existsSync, writeFileSync, watch} from 'fs';
import {readFile, writeFile} from 'fs/promises';

const EMPTY_STRING = '';
const UTF8 = 'utf8';
const UNDEFINED = '\uFFFC';

const isUndefined = (thing) => thing == undefined;
const ifNotUndefined = (value, then, otherwise) =>
  isUndefined(value)
    ? otherwise == null
      ? undefined
      : otherwise()
    : then(value);
const isArray = (thing) => Array.isArray(thing);
const size = (arrayOrString) => arrayOrString.length;
const test = (regex, subject) => regex.test(subject);
const errorNew = (message) => {
  throw new Error(message);
};

const arrayForEach = (array, cb) => array.forEach(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 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 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 === undefined ? UNDEFINED : value));
const jsonParseWithUndefined = (str) =>
  jsonParse(str, (_key, value) => (value === UNDEFINED ? undefined : value));

const collSize = (coll) => {
  var _a;
  return (_a = coll == null ? undefined : coll.size) != null ? _a : 0;
};
const collHas = (coll, keyOrValue) => {
  var _a;
  return (_a = coll == null ? undefined : coll.has(keyOrValue)) != null
    ? _a
    : false;
};
const collIsEmpty = (coll) => isUndefined(coll) || collSize(coll) == 0;
const collForEach = (coll, cb) => (coll == null ? undefined : coll.forEach(cb));
const collDel = (coll, keyOrValue) =>
  coll == null ? undefined : coll.delete(keyOrValue);

const mapNew = (entries) => new Map(entries);
const mapGet = (map, key) => (map == null ? undefined : map.get(key));
const mapSet = (map, key, value) =>
  isUndefined(value)
    ? (collDel(map, key), map)
    : map == null
      ? undefined
      : map.set(key, value);
const mapEnsure = (map, key, getDefaultValue, hadExistingValue) => {
  if (!collHas(map, key)) {
    mapSet(map, key, getDefaultValue());
  } else {
    hadExistingValue == null ? undefined : hadExistingValue(mapGet(map, key));
  }
  return mapGet(map, key);
};
const visitTree = (node, path, ensureLeaf, pruneLeaf, p = 0) =>
  ifNotUndefined(
    (ensureLeaf ? mapEnsure : mapGet)(
      node,
      path[p],
      p > size(path) - 2 ? ensureLeaf : mapNew,
    ),
    (nodeOrLeaf) => {
      if (p > size(path) - 2) {
        if (pruneLeaf == null ? undefined : pruneLeaf(nodeOrLeaf)) {
          mapSet(node, path[p]);
        }
        return nodeOrLeaf;
      }
      const leaf = visitTree(nodeOrLeaf, path, ensureLeaf, pruneLeaf, p + 1);
      if (collIsEmpty(nodeOrLeaf)) {
        mapSet(node, path[p]);
      }
      return leaf;
    },
  );

const setNew = (entryOrEntries) =>
  new Set(
    isArray(entryOrEntries) || isUndefined(entryOrEntries)
      ? entryOrEntries
      : [entryOrEntries],
  );
const setAdd = (set, value) => (set == null ? undefined : set.add(value));

const INTEGER = /^\d+$/;
const getPoolFunctions = () => {
  const pool = [];
  let nextId = 0;
  return [
    (reuse) => {
      var _a;
      return (_a = reuse ? arrayShift(pool) : null) != null
        ? _a
        : EMPTY_STRING + nextId++;
    },
    (id) => {
      if (test(INTEGER, id) && size(pool) < 1e3) {
        arrayPush(pool, id);
      }
    },
  ];
};

const getWildcardedLeaves = (deepIdSet, path = [EMPTY_STRING]) => {
  const leaves = [];
  const deep = (node, p) =>
    p == size(path)
      ? arrayPush(leaves, node)
      : path[p] === null
        ? collForEach(node, (node2) => deep(node2, p + 1))
        : arrayForEach([path[p], null], (id) => deep(mapGet(node, id), p + 1));
  deep(deepIdSet, 0);
  return leaves;
};
const getListenerFunctions = (getThing) => {
  let thing;
  const [getId, releaseId] = getPoolFunctions();
  const allListeners = mapNew();
  const addListener = (
    listener,
    idSetNode,
    path,
    pathGetters = [],
    extraArgsGetter = () => [],
  ) => {
    thing != null ? thing : (thing = getThing());
    const id = getId(1);
    mapSet(allListeners, id, [
      listener,
      idSetNode,
      path,
      pathGetters,
      extraArgsGetter,
    ]);
    setAdd(
      visitTree(idSetNode, path != null ? path : [EMPTY_STRING], setNew),
      id,
    );
    return id;
  };
  const callListeners = (idSetNode, ids, ...extraArgs) =>
    arrayForEach(getWildcardedLeaves(idSetNode, ids), (set) =>
      collForEach(set, (id) =>
        mapGet(allListeners, id)[0](
          thing,
          ...(ids != null ? ids : []),
          ...extraArgs,
        ),
      ),
    );
  const delListener = (id) =>
    ifNotUndefined(mapGet(allListeners, id), ([, idSetNode, idOrNulls]) => {
      visitTree(
        idSetNode,
        idOrNulls != null ? idOrNulls : [EMPTY_STRING],
        undefined,
        (idSet) => {
          collDel(idSet, id);
          return collIsEmpty(idSet) ? 1 : 0;
        },
      );
      mapSet(allListeners, id);
      releaseId(id);
      return idOrNulls;
    });
  const callListener = (id) =>
    ifNotUndefined(
      mapGet(allListeners, id),
      ([listener, , path = [], pathGetters, extraArgsGetter]) => {
        const callWithIds = (...ids) => {
          var _a, _b;
          const index = size(ids);
          if (index == size(path)) {
            listener(thing, ...ids, ...extraArgsGetter(ids));
          } else if (isUndefined(path[index])) {
            arrayForEach(
              (_b =
                (_a = pathGetters[index]) == null
                  ? undefined
                  : _a.call(pathGetters, ...ids)) != null
                ? _b
                : [],
              (id2) => callWithIds(...ids, id2),
            );
          } else {
            callWithIds(...ids, path[index]);
          }
        };
        callWithIds();
      },
    );
  return [addListener, callListeners, delListener, callListener];
};

var __defProp = Object.defineProperty;
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __propIsEnum = Object.prototype.propertyIsEnumerable;
var __defNormalProp = (obj, key, value) =>
  key in obj
    ? __defProp(obj, key, {
        enumerable: true,
        configurable: true,
        writable: true,
        value,
      })
    : (obj[key] = value);
var __spreadValues = (a, b) => {
  for (var prop in b || (b = {}))
    if (__hasOwnProp.call(b, prop)) __defNormalProp(a, prop, b[prop]);
  if (__getOwnPropSymbols)
    for (var prop of __getOwnPropSymbols(b)) {
      if (__propIsEnum.call(b, prop)) __defNormalProp(a, prop, b[prop]);
    }
  return a;
};
var __async$1 = (__this, __arguments, generator) => {
  return new Promise((resolve, reject) => {
    var fulfilled = (value) => {
      try {
        step(generator.next(value));
      } catch (e) {
        reject(e);
      }
    };
    var rejected = (value) => {
      try {
        step(generator.throw(value));
      } catch (e) {
        reject(e);
      }
    };
    var step = (x) =>
      x.done
        ? resolve(x.value)
        : Promise.resolve(x.value).then(fulfilled, rejected);
    step((generator = generator.apply(__this, __arguments)).next());
  });
};
const scheduleRunning = mapNew();
const scheduleActions = mapNew();
const getStoreFunctions = (
  persist = 1 /* StoreOnly */,
  store,
  isSynchronizer,
) =>
  persist != 1 /* StoreOnly */ && store.isMergeable()
    ? [
        1,
        store.getMergeableContent,
        () => store.getTransactionMergeableChanges(!isSynchronizer),
        ([[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 = {},
  isSynchronizer = 0,
  scheduleId = [],
) => {
  let status = 0; /* Idle */
  let loads = 0;
  let saves = 0;
  let action;
  let autoLoadHandle;
  let autoSaveListenerId;
  mapEnsure(scheduleRunning, scheduleId, () => 0);
  mapEnsure(scheduleActions, scheduleId, () => []);
  const statusListeners = mapNew();
  const [
    isMergeableStore,
    getContent,
    getChanges,
    hasChanges,
    setDefaultContent,
  ] = getStoreFunctions(persist, store, isSynchronizer);
  const [addListener, callListeners, delListenerImpl] = getListenerFunctions(
    () => persister,
  );
  const setStatus = (newStatus) => {
    if (newStatus != status) {
      status = newStatus;
      callListeners(statusListeners, undefined, status);
    }
  };
  const run = () =>
    __async$1(undefined, null, function* () {
      /* istanbul ignore else */
      if (!mapGet(scheduleRunning, scheduleId)) {
        mapSet(scheduleRunning, scheduleId, 1);
        while (
          !isUndefined(
            (action = arrayShift(mapGet(scheduleActions, scheduleId))),
          )
        ) {
          try {
            yield action();
          } catch (error) {
            /* istanbul ignore next */
            onIgnoredError == null ? undefined : onIgnoredError(error);
          }
        }
        mapSet(scheduleRunning, scheduleId, 0);
      }
    });
  const setContentOrChanges = (contentOrChanges) => {
    (isMergeableStore &&
      isArray(contentOrChanges == null ? undefined : contentOrChanges[0])
      ? (contentOrChanges == null ? undefined : contentOrChanges[2]) === 1
        ? store.applyMergeableChanges
        : store.setMergeableContent
      : (contentOrChanges == null ? undefined : contentOrChanges[2]) === 1
        ? store.applyChanges
        : store.setContent)(contentOrChanges);
  };
  const load = (initialContent) =>
    __async$1(undefined, null, function* () {
      /* istanbul ignore else */
      if (status != 2 /* Saving */) {
        setStatus(1 /* Loading */);
        loads++;
        yield schedule(() =>
          __async$1(undefined, null, function* () {
            try {
              const content = yield getPersisted();
              if (isArray(content)) {
                setContentOrChanges(content);
              } else if (initialContent) {
                setDefaultContent(initialContent);
              } else {
                errorNew(`Content is not an array: ${content}`);
              }
            } catch (error) {
              onIgnoredError == null ? undefined : onIgnoredError(error);
              if (initialContent) {
                setDefaultContent(initialContent);
              }
            }
            setStatus(0 /* Idle */);
          }),
        );
      }
      return persister;
    });
  const startAutoLoad = (initialContent) =>
    __async$1(undefined, null, function* () {
      stopAutoLoad();
      yield load(initialContent);
      try {
        autoLoadHandle = yield addPersisterListener((content, changes) =>
          __async$1(void 0, null, function* () {
            if (changes || content) {
              /* istanbul ignore else */
              if (status != 2 /* Saving */) {
                setStatus(1 /* Loading */);
                loads++;
                setContentOrChanges(changes != null ? changes : content);
                setStatus(0 /* Idle */);
              }
            } else {
              yield load();
            }
          }),
        );
      } catch (error) {
        /* istanbul ignore next */
        onIgnoredError == null ? undefined : onIgnoredError(error);
      }
      return persister;
    });
  const stopAutoLoad = () => {
    if (autoLoadHandle) {
      delPersisterListener(autoLoadHandle);
      autoLoadHandle = undefined;
    }
    return persister;
  };
  const isAutoLoading = () => !isUndefined(autoLoadHandle);
  const save = (changes) =>
    __async$1(undefined, null, function* () {
      /* istanbul ignore else */
      if (status != 1 /* Loading */) {
        setStatus(2 /* Saving */);
        saves++;
        yield schedule(() =>
          __async$1(undefined, null, function* () {
            try {
              yield setPersisted(getContent, changes);
            } catch (error) {
              /* istanbul ignore next */
              onIgnoredError == null ? undefined : onIgnoredError(error);
            }
            setStatus(0 /* Idle */);
          }),
        );
      }
      return persister;
    });
  const startAutoSave = () =>
    __async$1(undefined, null, function* () {
      stopAutoSave();
      yield save();
      autoSaveListenerId = store.addDidFinishTransactionListener(() => {
        const changes = getChanges();
        if (hasChanges(changes)) {
          save(changes);
        }
      });
      return persister;
    });
  const stopAutoSave = () => {
    if (autoSaveListenerId) {
      store.delListener(autoSaveListenerId);
      autoSaveListenerId = undefined;
    }
    return persister;
  };
  const isAutoSaving = () => !isUndefined(autoSaveListenerId);
  const getStatus = () => status;
  const addStatusListener = (listener) =>
    addListener(listener, statusListeners);
  const delListener = (listenerId) => {
    delListenerImpl(listenerId);
    return store;
  };
  const schedule = (...actions) =>
    __async$1(undefined, null, function* () {
      arrayPush(mapGet(scheduleActions, scheduleId), ...actions);
      yield run();
      return persister;
    });
  const getStore = () => store;
  const destroy = () => {
    arrayClear(mapGet(scheduleActions, scheduleId));
    return stopAutoLoad().stopAutoSave();
  };
  const getStats = () => ({loads, saves});
  const persister = __spreadValues(
    {
      load,
      startAutoLoad,
      stopAutoLoad,
      isAutoLoading,
      save,
      startAutoSave,
      stopAutoSave,
      isAutoSaving,
      getStatus,
      addStatusListener,
      delListener,
      schedule,
      getStore,
      destroy,
      getStats,
    },
    extra,
  );
  return objFreeze(persister);
};

var __async = (__this, __arguments, generator) => {
  return new Promise((resolve, reject) => {
    var fulfilled = (value) => {
      try {
        step(generator.next(value));
      } catch (e) {
        reject(e);
      }
    };
    var rejected = (value) => {
      try {
        step(generator.throw(value));
      } catch (e) {
        reject(e);
      }
    };
    var step = (x) =>
      x.done
        ? resolve(x.value)
        : Promise.resolve(x.value).then(fulfilled, rejected);
    step((generator = generator.apply(__this, __arguments)).next());
  });
};
const createFilePersister = (store, filePath, onIgnoredError) => {
  const getPersisted = () =>
    __async(undefined, null, function* () {
      return jsonParseWithUndefined(yield readFile(filePath, UTF8));
    });
  const setPersisted = (getContent) =>
    __async(undefined, null, function* () {
      return yield writeFile(
        filePath,
        jsonStringWithUndefined(getContent()),
        UTF8,
      );
    });
  const addPersisterListener = (listener) => {
    if (!existsSync(filePath)) {
      writeFileSync(filePath, EMPTY_STRING, UTF8);
    }
    return watch(filePath, () => listener());
  };
  const delPersisterListener = (watcher) =>
    watcher == null ? undefined : watcher.close();
  return createCustomPersister(
    store,
    getPersisted,
    setPersisted,
    addPersisterListener,
    delPersisterListener,
    onIgnoredError,
    3,
    // StoreOrMergeableStore,
    {getFilePath: () => filePath},
  );
};

export {createFilePersister};
