/**
 * 
 * @param fn an arbitrary callback which performs some operation with side effects.
 * @returns a tuple: [batchedFn, fireBatch]. 
 * batchedFn takes the same arguments as fn, but the side effects are delayed until fireBatch is called.
 * if batchedFn is called multiple times, those invocations are stored in order, and then popped off in order when fireBatch is called.
 */
export function batchify<A extends any[]>(fn: (...args: A)=> void): [((...args: A) => void), () => void] {
  let batch: A[] = [];

  return [(...args: A) => {
    batch.push(args);
    // console.log({ stack: new Error().stack, batchSize: batch.length });
    // console.log({ batchSize: batch.length });
  }, (() => {
      if (batch.length !== 0) { console.log({ fired: batch.length }); }
      for (let a of batch) {
        fn(...a);
      }
      batch = [];
    })
  ];
}

/**
 * Same use case and types as [batchify], however, specifically we expect [fn] to be a setState function which takes value-or-callback
 * as its single argument, and instead of calling [fn] repeatedly for each callback in the batch, we apply the callbacks in the batch
 * sequentially to get a single state update which we then provide to [fn].
 */
export function batchifySetState<T>(
  fn: (arg: T) => void
): [((arg: T) => void), () => void] {
  let batch: T[] = [];

  return [(arg: T) => {
    batch.push(arg);
    // console.log({ batchSize: batch.length });
  }, (() => {
      if (batch.length === 0) {
        return;
      }
      // console.log({ fired: batch.length });
      let thisBatch = [...batch];
      batch = [];
      (fn as any)((prev: any) => {
        let next = prev;
        for (let valueOrCallback of thisBatch) {
          if (typeof valueOrCallback === 'function') {
            next = valueOrCallback(next);
          } else {
            next = valueOrCallback;
          }
        }
        return next;
      });
  })]
}