type UpdaterFnParam2<T, W> = ((prev: T, prevWhole: W) => T) | (T extends Function ? never : T) // (T | ((prev: T, prevWhole: W) => T));
type UpdaterFn2<T, W> = (arg: UpdaterFnParam2<T, W>) => void;

/**
 * Represent the type which has the same deep object struture as T but instead of values, it has
 * functions on [getUpdater], [enqueueUpdate], and [update] attached to every level of the object structure.
 */
export type UpdaterGeneratorType2<T, W = T> = {
  [k in keyof T]: ((T[k] extends { [kkt: string]: any } ? UpdaterGeneratorType2<T[k], W> : {}) & {
    getUpdater: () => UpdaterFn2<T[k], W>,
    enqueueUpdate: UpdaterFn2<T[k], W>,
  })
} & {
  getUpdater: () => UpdaterFn2<T, W>,
  enqueueUpdate: UpdaterFn2<T, W>,
}

// helper method for the recursion
function updaterGenerator2Helper<T, W>(dataObject: T, dataUpdater: UpdaterFn2<T, W>): UpdaterGeneratorType2<T, W> {
  const updaters: UpdaterGeneratorType2<T, W> = {} as any;
  updaters.getUpdater = () => dataUpdater;
  updaters.enqueueUpdate = dataUpdater;
  if (typeof dataObject !== "object") return updaters;
  else {
    const keys: (keyof T)[] = Object.keys(dataObject) as any;
    keys.forEach((key: (keyof T)) => {
      if (key === "enqueueUpdate" || key === "getUpdater" || key === "update") {
        throw Error(`Invalid key in updaterGenerator: ${key} conflicts with reserved keywords enqueueUpdate, update, getUpdater.`);
      }
      function keyUpdater(newValueOrCallback: UpdaterFnParam2<T[typeof key], W>) {
        if (typeof newValueOrCallback === "function") {
          dataUpdater((oldData: T, wholeData: W) => {
            const newKey = (newValueOrCallback as ((prev: T[typeof key], whole: W) => T[typeof key]))(oldData[key], wholeData);
            if (oldData[key] === newKey) {
              return oldData; // no update detected, no need to update anything
            } else {
              const newData = {
                ...oldData,
                [key]: newKey
              };
              return newData;
            }
          });
        } else {
          dataUpdater((oldData, wholeData) => ({ ...oldData, [key]: newValueOrCallback }));
        }
      }
      updaters[key] = (updaterGenerator2Helper<T[typeof key], W>(dataObject[key], keyUpdater) as unknown as (typeof updaters)[typeof key]);
    });
    return updaters;
  }
}

/**
 * Convenience method for generating setState<FancyObject.sub.component>() from setState<FancyObject> callbacks.
 * If used in react, recommended that this be memoized.
 * 
 * @generic T should be a data-only object - nested objects are allowed but arrays, sets not supported
 * @param dataObject ANY instance of T, used only for its keys. MUST have all keys present
 * @param setState an updater function, which can be called as: dataUpdater(newT) or
 *   dataUpdater((oldT) => { return newTFromOldT(oldT) }) ; e.g. react setState() function.
 * @return a deep object that has the same keys as T, except each key also has a getUpdater()/set/update member;
 *   the getUpdater() on a subobject of T acts similarly to the [setState] param but to the subobject rather than the whole object;
 *   the whole object is also available as the second argument of the callback
 * e.g. :
 *   let gameStateUpdater = updaterGenerator(skeletonObject, setGameState);
 *   let setName = gameStateUpdater.player.name.getUpdater();
 *   gameStateUpdater.player.name.set(newName);
 *   gameStateUpdater.player.name.update((oldName, wholeObject) => oldName + " ");
 * 
 */
export function updaterGenerator2<T>(dataObject: T, setState: UpdaterFn<T>): UpdaterGeneratorType2<T> {
  const dataUpdater2 = (stateCallbackFunction: UpdaterFnParam2<T, T>) => {
    if (typeof stateCallbackFunction === 'function') {
      setState((prev: T) => {
        // if T is a function type already, typescript correctly notifies us that this will fail
        const next = (stateCallbackFunction as ((prev: T, prevWhole: T) => T))(prev, prev);
        // console.log(" in updater generator 2", { next });
        return next;
      })
    } else {
      setState(stateCallbackFunction);
    }
  };
  return updaterGenerator2Helper<T, T>(dataObject, dataUpdater2);
}

export type UpdaterFnParam<T> = (T extends Function ? never  : T) | ((prev: T) => T);
export type UpdaterFn<T> = (arg: UpdaterFnParam<T>) => void;