import * as R from "ramda";
import { useRef } from "react";

/**
 *
 * @param callback - The function to throttle.
 * @param delay - The delay in milliseconds.
 * @returns A throttled version of the callback.
 */
export const useThrottle = (callback: () => void, delay: number) => {
  const lastCallRef = useRef<number>(0);
  const timeoutRef = useRef<any | null>(null);

  const throttledFunction = () => {
    const now = Date.now();

    if (now - lastCallRef.current >= delay) {
      callback();
      lastCallRef.current = now;
    } else if (!timeoutRef.current) {
      const remainingTime = delay - (now - lastCallRef.current);

      timeoutRef.current = setTimeout(() => {
        callback();
        lastCallRef.current = Date.now();
        timeoutRef.current = null;
      }, remainingTime);
    }
  };

  const cancel = () => {
    if (timeoutRef.current) {
      clearTimeout(timeoutRef.current);
      timeoutRef.current = null;
    }
  };

  return { throttledFunction, cancel };
};

/**
 * standard debounce function.
 * @param {Object} options
 * @param {Function} options.fn function to debounce
 * @param {Number} options.wait debounce duration in ms. defaults to 200
 * @param {Boolean} options.immediate will trigger immediately instead of after wait
 * @param {Object} options.context context to apply the functions on
 * @param {Function} debounced function
 */
export function debounce({
  fn,
  wait = 200,
  immediate = true,
  context,
}: {
  fn: Function;
  wait?: number;
  immediate?: boolean;
  context?: any;
}) {
  let timeout;

  return function debounced(...args) {
    const that = context || this;

    function callLater() {
      timeout = null;

      if (!immediate) {
        fn.apply(that, args);
      }
    }

    const callNow = immediate && !timeout;
    clearTimeout(timeout);
    timeout = setTimeout(callLater, wait);

    if (callNow) {
      fn.apply(that, args);
    }
  };
}

/**
 * a function that does nothing
 */
export function noop() {
  void 0;
}

/**
 * returns if passed value is a function
 * @param {*} functionToCheck to add the subscription feature on
 * @returns {Bool} subscriber object
 */
export function isFunction(functionToCheck) {
  return functionToCheck ? typeof functionToCheck === "function" : false;
}

export type ParametersExceptFirst<T extends (...args: any) => any> = T extends (
  ignored: infer _,
  ...args: infer P
) => any
  ? P
  : never;

type SubscriberFn<T> = (
  event: string | number,
  handler: (...args: any[]) => void
) => T;

export type EventHandlerFn<T> = (event: string, ...args: unknown[]) => T;

type SubscriberObj = Partial<{
  on?: SubscriberFn<SubscriberObj>;
  invokeHandler: EventHandlerFn<SubscriberObj>;
  removeHandler: EventHandlerFn<SubscriberObj>;
  handlers: Record<string, SubscriberFn<SubscriberObj>[] | []>;
}>;

export const clearObject = (object: { [key: string]: unknown }) =>
  Object.keys(object).forEach((key) => delete object[key]);

/**
 * returns a subscribe object to which handlers can be attached
 * @param {Object} obj to add the subscription feature on
 * @returns {Object} subscriber object
 */
export function subscriber(obj: SubscriberObj = {}): SubscriberObj {
  if (!obj.handlers) {
    obj.handlers = {};
  }

  const handlers = obj.handlers;

  /**
   * creates a dispose function to remove handlers on a given event
   * @param {string} event to dispose handlers for
   * @param {function} handler to remove
   * @returns {function} to dispose handlers
   */
  function dispose(event, handler) {
    /**
     * function to invoke to remove a handler on an event.
     * handlers are invoked with this function as last argument
     * @param {object} options
     * @param {boolean} options.disposeAll if set to true, will
     * remove all handlers on the object
     */
    return function ({ disposeAll = false } = {}) {
      if (disposeAll) {
        clearObject(handlers);

        return;
      }

      handlers[event] = R.reject(R.equals(handler), handlers[event] || []) as
        | SubscriberFn<SubscriberObj>[]
        | [];
    };
  }

  /**
   * adds a handler for a specific event
   * @param {string} event to trigger the handler
   * @param {function} handler to invoke
   * @returns {object} returns `this` so that handlers declaration
   * can be chained
   */
  obj.on = function (event, handler) {
    handlers[event] = R.append(handler, handlers[event] || []) as
      | SubscriberFn<SubscriberObj>[]
      | [];

    return obj;
  };

  /**
   * invokes handlers for a specific event with the provided args
   * will append the dispose function as last argument to the handler
   * @param {string} event for which handlers should be invoked
   * @param {Array<Any>} args variadic list args of args to invoke the handlers with
   * @returns {object} returns `this` so handlers invocation can be chained
   */
  obj.invokeHandler = function (event: string, ...args) {
    const callHandlerWithDispose = (handler) =>
      handler(...args, dispose(event, handler));

    R.compose(R.forEach(callHandlerWithDispose), R.propOr([], event))(handlers);

    return obj;
  };

  obj.removeHandler = function (event, handler) {
    handlers[event] = R.reject(R.equals(handler), handlers[event]);

    return obj;
  };

  return obj;
}

/**
 * tries to parse a json. if it fails returns the initial value
 * @param {Any} data
 * @returns {any}
 */
export const parseJsonIfNeeded = R.tryCatch(JSON.parse, R.flip(R.identity));

/**
 * map function which works with async functions. Has the same signature as Ramda's map
 * function, but allows async functions, and will return a promise
 * @param {Function} fn function to run
 * @param {[Any]} arr to run the async function unto
 * @returns {Promise<[Any]>} a promise which resolves to an array
 */
export const mapAsync = R.curry(async (fn, arr: Promise<unknown>[]) => {
  return await R.compose(
    (promises: Promise<any>[]) => Promise.all(promises),
    R.map(fn)
  )(arr);
});

/**
 * This function will determine if the given prop or path is present
 * if present return that prop or path to the user
 * When using a path, this function only accepts . as a delimiter i.e. extensions.free
 * It is important to use currying when passing the args i.e. objectHasPropOrPath(arg1)(arg2)
 * @param {String} property - i.e. property, this is what you want to pull from mapped object
 * @param {Object} data - i.e. navigator?.screenData or any data you need to extract key from
 * @param {Object} mappedObject - i.e mapping[i] the value of the mapped object
 */
export const extractDataFromProperty = (property, data): any =>
  // TODO fix ramda types
  // @ts-ignore
  R.compose(
    // @ts-ignore
    R.ifElse(R.hasPath(R.__, data), R.path(R.__, data), R.always(null)),
    R.tryCatch(R.split("."), R.always([])),
    R.when(R.has(property), R.prop(property))
  );

export const then = (fn) => (promise) => promise.then(fn);
