/**
 * This helper groups multiple function calls into one during the given graceDelay.
 * It's similar to a debounce but all the arguments are stacked and given to the
 * callback function a the end of the delay.
 * It's useful, for example, if you want to group many calls of a
 * function that query the server into one to have only one server call.
 * The callback function receives the array of args of all calls during the grace
 * time and must return a function that return the right value given the originally
 * specified args.
 *
 * Example:
 *
 * ```js
 * const groupGetNameCalls = callGrouper(50)
 *
 * const getName = groupGetNameCalls(async (argList) => {
 *   const result = await (
 *     await fetch('some/url', doSomethingWithArgList(argList))
 *   ).json()
 *   return (id) => {
 *     return result[id]
 *   }
 * })
 *
 * // Somewhere in the code
 * const name = await getName(42)
 * // Somewhere else
 * const name = await getName(4)
 *
 * ```
 *
 * If the two calls are triggered within the 50ms the `groupCalls` callback
 * function will be called with `[42, 4]`.
 *
 * @param {int} graceDelay the grace delay in ms during which the calls are grouped
 * @returns A function you can call with a callback to create the final function that
 *  behave like the original function. The callback will receive an array of all the
 *  arguments and must return a function that returns the original result given the
 *  provided args.
 */
export const callGrouper = (graceDelay) => {
  let argsList, delay, currentResolve, currentPromise

  const init = () => {
    argsList = []
    delay = null
    currentResolve = null
    currentPromise = null
  }
  init()

  return (callback) =>
    async (...args) => {
      clearTimeout(delay)

      argsList.push(args)

      if (currentPromise === null) {
        currentPromise = new Promise((resolve) => (currentResolve = resolve))
      }

      delay = setTimeout(async () => {
        currentResolve(await callback(argsList))
        init()
      }, graceDelay)

      return (await currentPromise)(...args)
    }
}