import { IPluggableStorageWrapper } from '../types';
import { startsWith, toNumber } from '../../utils/lang';
import { setToArray } from '../../utils/lang/sets';

/**
 * Creates a IPluggableStorageWrapper implementation that stores items in memory.
 * The `_cache` property is the object were items are stored.
 * Intended for testing purposes.
 *
 * @param connDelay - delay in millis for `connect` resolve. If not provided, `connect` resolves immediately.
 */
export function inMemoryWrapperFactory(connDelay?: number): IPluggableStorageWrapper & { _cache: Record<string, string | string[] | Set<string>>, _setConnDelay(connDelay: number): void } {

  let _cache: Record<string, string | string[] | Set<string>> = {};
  let _connDelay = connDelay;

  return {
    /** Holds items (for key-value operations), list of items (for list operations) and set of items (for set operations) */
    _cache,

    get(key) {
      return Promise.resolve(key in _cache ? _cache[key] as string : null);
    },
    set(key: string, value: string) {
      const result = key in _cache;
      _cache[key] = value;
      return Promise.resolve(result);
    },
    getAndSet(key: string, value: string) {
      const result = key in _cache ? _cache[key] as string : null;
      _cache[key] = value;
      return Promise.resolve(result);
    },
    del(key: string) {
      const result = key in _cache;
      delete _cache[key];
      return Promise.resolve(result);
    },
    getKeysByPrefix(prefix: string) {
      return Promise.resolve(Object.keys(_cache).filter(key => startsWith(key, prefix)));
    },
    incr(key: string, increment = 1) {
      if (key in _cache) {
        const count = toNumber(_cache[key]) + increment;
        if (isNaN(count)) return Promise.reject('Given key is not a number');
        _cache[key] = count + '';
        return Promise.resolve(count);
      } else {
        _cache[key] = '' + increment;
        return Promise.resolve(1);
      }
    },
    decr(key: string, decrement = 1) {
      if (key in _cache) {
        const count = toNumber(_cache[key]) - decrement;
        if (isNaN(count)) return Promise.reject('Given key is not a number');
        _cache[key] = count + '';
        return Promise.resolve(count);
      } else {
        _cache[key] = '-' + decrement;
        return Promise.resolve(-1);
      }
    },
    getMany(keys: string[]) {
      return Promise.resolve(keys.map(key => _cache[key] ? _cache[key] as string : null));
    },
    pushItems(key: string, items: string[]) { // @ts-ignore
      if (!(key in _cache)) _cache[key] = [];
      const list = _cache[key];
      if (Array.isArray(list)) {
        list.push(...items);
        return Promise.resolve();
      }
      return Promise.reject('key is not a list');
    },
    popItems(key: string, count: number) {
      const list = _cache[key];
      return Promise.resolve(Array.isArray(list) ? list.splice(0, count) : []);
    },
    getItemsCount(key: string) {
      const list = _cache[key];
      return Promise.resolve(Array.isArray(list) ? list.length : 0);
    },
    itemContains(key: string, item: string) {
      const set = _cache[key];
      if (!set) return Promise.resolve(false);
      if (set instanceof Set) return Promise.resolve(set.has(item));
      return Promise.reject('key is not a set');
    },
    addItems(key: string, items: string[]) {
      if (!(key in _cache)) _cache[key] = new Set();
      const set = _cache[key];
      if (set instanceof Set) {
        items.forEach(item => set.add(item));
        return Promise.resolve();
      }
      return Promise.reject('key is not a set');
    },
    removeItems(key: string, items: string[]) {
      if (!(key in _cache)) _cache[key] = new Set();
      const set = _cache[key];
      if (set instanceof Set) {
        items.forEach(item => set.delete(item));
        return Promise.resolve();
      }
      return Promise.reject('key is not a set');
    },
    getItems(key: string) {
      const set = _cache[key];
      if (!set) return Promise.resolve([]);
      if (set instanceof Set) return Promise.resolve(setToArray(set));
      return Promise.reject('key is not a set');
    },

    // always connects and disconnects
    connect() {
      if (typeof _connDelay === 'number') {
        return new Promise(res => setTimeout(res, _connDelay));
      } else {
        return Promise.resolve();
      }
    },
    disconnect() { return Promise.resolve(); },

    // for testing
    _setConnDelay(connDelay: number) {
      _connDelay = connDelay;
    }
  };
}
