const _ = require('lodash');
const DataLoader = require('dataloader');
const cache = require('../../services/cache');

/**
 * SingletonResolver is a cached loader for a single result.
 */
class SingletonResolver {
  constructor(resolver) {
    this._cache = null;
    this._resolver = resolver;
  }

  load() {
    if (this._cache) {
      return this._cache;
    }

    let promise = this._resolver(arguments).then((result) => {
      return result;
    });

    // Set the promise on the cache.
    this._cache = promise;

    return promise;
  }
}

/**
 * This joins a set of results with a specific keys and sets an empty array in
 * place if it was not found.
 * @param  {Array}  ids ids to locate
 * @param  {String} key key to group by
 * @return {Array}      array of results
 */
const arrayJoinBy = (ids, key) => (items) => {
  const itemsByKey = _.groupBy(items, key);

  return ids.map((id) => {
    if (id in itemsByKey) {
      return itemsByKey[id];
    }

    return [];
  });
};

/**
 * This joins a set of results with a specific keys and sets null in place if it
 * was not found.
 * @param  {Array}  ids ids to locate
 * @param  {String} key key to group by
 * @return {Array}      array of results
 */
const singleJoinBy = (ids, key) => (items) => {
  const itemsByKey = _.groupBy(items, key);
  return ids.map((id) => {
    if (id in itemsByKey) {
      return itemsByKey[id][0];
    }

    return null;
  });
};

/**
 * SharedCacheDataLoader provides a version of the DataLoader that wraps up a
 * redis backed cache with the dataloader's request cache.
 */
class SharedCacheDataLoader extends DataLoader {
  constructor(prefix, expiry, batchLoadFn, options) {
    super(SharedCacheDataLoader.batchLoadFn(prefix, expiry, batchLoadFn), options);

    this._prefix = prefix;
    this._expiry = expiry;
    this._keyFunc = SharedCacheDataLoader.keyFunc(this._prefix);
  }

  /**
   * clear the key from the shared cache and the request cache
   */
  clear(key) {
    return cache
      .invalidate(key, this._keyFunc)
      .then(() => super.clear(key));
  }

  /**
   * prime the shared cache and the request cache
   */
  prime(key, value) {
    return cache
      .set(key, value, this._expiry, this._keyFunc)
      .then(() => super.prime(key, value));
  }

  /**
   * prime many values in the shared cache and the request cache
   */
  primeMany(keys, values) {
    return cache
      .setMany(keys, values, this._expiry, this._keyFunc)
      .then(() => keys.map((key, i) => super.prime(key, values[i])));
  }

  /**
   * wraps up the prefix needed for the redis backed shared cache driver
   */
  static keyFunc(prefix) {
    return (key) => `cache.sbl[${prefix}][${key}]`;
  }

  /**
   * wraps the dataloader batchLoadFn with the shared cache's wrapper
   */
  static batchLoadFn(prefix, expiry, batchLoadFn) {
    return (ids) => cache.wrapMany(ids, expiry, (workKeys) => {
      return batchLoadFn(workKeys);
    }, SharedCacheDataLoader.keyFunc(prefix));
  }
}

/**
 * SharedCounterDataLoader is identical to SharedCacheDataLoader with the
 * exception in that it is designed to work with numerical cached data.
 */
class SharedCounterDataLoader extends SharedCacheDataLoader {

  /**
   * Increments the key in the cache if it already exists in the cache, if not
   * it does nothing.
   */
  incr(key) {
    return cache.incr(key, this._expiry, this._keyFunc);
  }

  /**
   * Decrements the key in the cache if it already exists in the cache, if not
   * it does nothing.
   */
  decr(key) {
    return cache.decr(key, this._expiry, this._keyFunc);
  }
}

/**
 * Maps an object's paths to a string that can be used as a cache key.
 * @param  {Array} paths paths on the object to be used to generate the cache
 *                       key
 */
const objectCacheKeyFn = (...paths) => (obj) => {
  return paths.map((path) => obj[path]).join(':');
};

/**
 * Maps an object's paths to a string that can be used as a cache key.
 * @param  {Array} paths paths on the object to be used to generate the cache
 *                       key
 */
const arrayCacheKeyFn = (arr) => {
  return arr.sort().join(':');
};

module.exports = {
  singleJoinBy,
  arrayJoinBy,
  objectCacheKeyFn,
  arrayCacheKeyFn,
  SingletonResolver,
  SharedCacheDataLoader,
  SharedCounterDataLoader
};
