/**
 * Utilities used in most IO modules. Typically called from other J5e methods, not by user code.
 * @module j5e/fn
 */

/** Normalize parameters passed on device instantiation
 * @param {(number|string|object)} ioOpts - A pin number, pin identifier or a complete IO options object
 * @param {(number|string)} [ioOpts.pin] - If passing an object, a pin number or pin identifier
 * @param {(string|constructor)} [ioOpts.io] - If passing an object, a string specifying a path to the IO provider or a constructor
 * @param {object} [deviceOpts={}] - An object containing device options
 * @ignore
 */
export function normalizeParams(options = {}) {
  return normalizeDevice(normalizeIO(options));
};

/** Normalize IO parameters
 * @param {(number|string|object)} options - A pin number, pin identifier or a complete IO options object
 * @param {(number|string)} [ioOpts.pin] - If passing an object, a pin number or pin identifier
 * @param {(string|constructor)} [ioOpts.io] - If passing an object, a string specifying a path to the IO provider or a constructor
 * @ignore
 */
export function normalizeIO(pin = null) {
  if (typeof pin === "number" || typeof pin === "string") {
    return { pin };
  }
  if (Array.isArray(pin)) {
    return { pins: pin };
  }
  return pin;
};

/** Normalize Device parameter
 * @param {object} [options={}] - An object containing device options
 * @ignore
 */
export function normalizeDevice(options = {}) {
  return options;
};

/** Normalize Multi-pin Device parameter
 * @param {object} [options={}] - An object containing device options
 * @ignore
 */
export function normalizeMulti(options = {}) {
  if (!Array.isArray(options)) {
    let { io, pins, ...rest } = options;
    // Loop through the pins property array and make sure each is an object
    pins = options.pins.map(pin => normalizeIO(pin));
    // If IO is defined on options use it as default
    if (io) {
      pins.forEach(pin => pin.io = pin.io || io);
    }

    // Copy each property that is not ```pins``` onto each member of the pins array
    return pins.map(pin => Object.assign(pin, rest));
  } else {
    return options.map(pin => normalizeIO(pin));
  }
};

/** Wait for an async forEach loop. Does not run in parallel.
 * @param {array[]} array - An input array
 * @param {function} callback - A function to execute when iteration is complete
 * @author Sebastien Chopin
 * @see {@link https://codeburst.io/javascript-async-await-with-foreach-b6ba62bbf404|Sebastien's Medium article} for more information
 * @example
 * const waitFor = (ms) => new Promise(r => setTimeout(r, ms));
 *
 * await asyncForEach([1, 2, 3], async (num) => {
 *   await waitFor(50);
 *   console.log(num);
 * });
 * console.log('Done');
 */
export async function asyncForEach(array, callback) {
  for (let index = 0; index < array.length; index++) {
    await callback(array[index], index, array);
  }
};

/**
 * Map a value (number) from one range to another. Based on Arduino's map().
 * Truncates the returned value to an integer
 *
 * @param {Number} value    - value to map
 * @param {Number} fromLow  - low end of originating range
 * @param {Number} fromHigh - high end of originating range
 * @param {Number} toLow    - low end of target range
 * @param {Number} toHigh   - high end of target range
 * @return {Number} mapped value (integer)
 * @example
 * Fn.map(500, 0, 1000, 0, 255); // -> 127
 */
export function map(value, fromLow, fromHigh, toLow, toHigh) {
  return (((value - fromLow) * (toHigh - toLow) / (fromHigh - fromLow) + toLow) | 0);
};

/**
 * Like map, but does not truncate the returned value
 *
 * @param {Number} value    - value to map
 * @param {Number} fromLow  - low end of originating range
 * @param {Number} fromHigh - high end of originating range
 * @param {Number} toLow    - low end of target range
 * @param {Number} toHigh   - high end of target range
 * @return {Number}
 */
export function fmap(value, fromLow, fromHigh, toLow, toHigh) {
  return (value - fromLow) * (toHigh - toLow) / (fromHigh - fromLow) + toLow;
};

/** Constrain a value to a range.
 * @param {number} value - An input value
 * @param {number} low - The minimum allowed value (inclusive)
 * @param {number} high - The maximum allowed value (inclusive)
 * @return {Number} constrained value
 * @example
 * constrain(120, 0, 100); // -> 100
 */
export function constrain(value, low, high) {
  if (value > high) {
    value = high;
  }
  if (value < low) {
    value = low;
  }
  return value;
};

/** Asynchronously load a provider. This allows users to simply pass a path or skip specifying a provider altogether (uses builtins).
 * @param {object} ioOpts - An IO options object
 * @param {string|io} [ioOpts.io] - The path to the IO class or an io instance
 * @param {string} defaultProvider - The default provider to use if none was passed in the io object
 * @ignore
 */
export async function getProvider(ioOpts, ioType) {
  if (!ioOpts) {
    return null;
  }
  if (ioOpts.io) {
    if (typeof ioOpts.io === "string") {
      const Provider = await import(ioOpts.io);
      return Provider;
    } else {
      return ioOpts.io;
    }
  }
  return defaultProvider(ioType);
}

/** Divine the default provider and return constructor for desired io type
 * @param {string} ioType - The desired io type
 * @ignore
 */
export async function defaultProvider(ioType) {
  let defaultProvider = device.io;
  return defaultProvider[ioType];
}


/** Wrapper for setInterval, clearInterval, and setImmediate. This is necessary so we can use the Global methods in node.js and the System methods in XS.
 * @namespace timer
 */
export const timer = Object.freeze({

  /**
   * Execute a callback on a recurring interval
   * @function setInterval
   * @memberof timer
   * @param {function} callback
   * @param {Number} duration
   * @returns {Interval}
   * @example
   * <caption>Blink an LED (We're pretending Led.blink() doesn't exist here)</caption>
   * import LED from "j5e/led";
   * import {timer} from "j5e/fn";
   *
   * const led = await new LED(12);
   *
   * timer.setInterval(function() {
   *   led.toggle();
   * }, 100);
   */
  setInterval(callback, duration) {
    if (global && global.setInterval) {
      return global.setInterval(callback, duration);
    }
    if (typeof System !== "undefined" && System.setInterval) {
      return System.setInterval(callback, duration);
    }
  },

  /**
   * Stop a recurring interval
   * @function clearInterval
   * @memberof timer
   * @param {Interval} identifier
   * @example
   * <caption>Blink an LED for one second and then stop (We're pretending Led.blink() doesn't exist here)</caption>
   * import LED from "j5e/led";
   * import {timer} from "j5e/fn";
   *
   * const led = await new LED(12);
   *
   * let myTimer = timer.setInterval(function() {
   *   led.toggle();
   * }, 100)
   *
   * timer.setTimeout(function() {
   *   timer.clearInterval(myTimer);
   * }, 1000);
   */
  clearInterval(identifier) {
    if (global && global.clearInterval) {
      return global.clearInterval(identifier);
    }
    if (typeof System !== "undefined" && System.clearInterval) {
      return System.clearInterval(identifier);
    }
  },

  /**
   * Execute a callback after a specified period of time
   * @function setInterval
   * @memberof timer
   * @param {function} callback
   * @param {Number} duration
   * @returns {Timer}
   * @example
   * <caption>Blink an LED for one second and then stop</caption>
   * import LED from "j5e/led";
   * import {timer} from "j5e/fn";
   *
   * const led = await new LED(12);
   * led.blink();
   *
   * timer.setTimeout(function() {
   *   led.stop();
   * }, 1000);
   */
  setTimeout(callback, duration) {
    if (global && global.setTimeout) {
      return global.setTimeout(callback, duration);
    }
    if (typeof System !== "undefined" && System.setTimeout) {
      return System.setTimeout(callback, duration);
    }
  },
  /**
   * Stop a timeout before it occurs
   * @function clearTimeout
   * @memberof timer
   * @param {Interval} identifier
   * @example
   * <caption>Clear a debounce timeout</caption>
   * import LED from "j5e/led";
   * import {timer} from "j5e/fn";
   *
   * const debounce = (f, ms) => {
   *   let timeout;
   *   return (...args) => {
   *     if (timeout) {
   *       timer.clearTimeout(timeout);
   *     }
   *     timeout = timer.setTimeout(() => {
   *       timeout = null;
   *       f(...args);
   *     }, ms);
   *   };
   * };
   */
  clearTimeout(identifier) {
    if (global && global.clearTimeout) {
      return global.clearTimeout(identifier);
    }
    if (typeof System !== "undefined" && System.clearTimeout && identifier) {
      return System.clearTimeout(identifier);
    }
  },
  /**
   * Execute a callback on next tick
   * @function setImmediate
   * @memberof timer
   * @param {function} callback
   */
  setImmediate(callback) {
    if (typeof process !== "undefined" && global.setImmediate) {
      global.setImmediate(callback);
    }
    if (typeof System !== "undefined" && System.setTimeout) {
      System.setTimeout(callback);
    }
  }

});

/** Debounce a function so that it is not invoked unless it stops being called for N milliseconds
 * @function debounce
 * @param {function} func The function to be debounced
 * @param {number} wait The number of milliseconds to wait
 * @param {boolean} [immediate]  Triggers the function on the leading edge
 * @returns {function}
 * @author David Walsh
 * @see {@link https://davidwalsh.name/javascript-debounce-function} For more information
 */
export function debounce(func, wait, immediate) {
  let timeout;
  return () => {
    const args = arguments;

    const later = () => {
      timeout = null;
      if (!immediate) {
        func.apply(this, args);
      };
    };

    let callNow = immediate && !timeout;

    timer.clearTimeout(timeout);
    timeout = timer.setTimeout(later, wait);

    if (callNow) {
      func.apply(this, args);
    }
  };
};


/** Format a number such that it has a given number of digits after the
 * decimal point
 *
 * @param {Number} number - The number to format
 * @param {Number} [digits = 0] - The number of digits after the decimal point
 * @return {Number} Formatted number
 * @example
 * Fn.toFixed(5.4564, 2); // -> 5.46
 * @example
 * Fn.toFixed(1.5, 2); // -> 1.5
 */
export function toFixed(number, digits) {
  return +(number || 0).toFixed(digits);
};

/** left Pad a Number with zeros
 * @param {number} value - An input value
 * @param {number} length - The desired length
 * @return {String} The padded string
 * @example
 * pad(3, 2); // -> "03"
 */
export function pad(value, length) {
  return String(value).padStart(length, "0");
}