/**
 * Class library for controlling LED's (Light Emitting Diodes). Connect the LED to a digital IO for on/off functionality. Use a PWM IO for varying brightness.
 * @module j5e/led
 * @requires module:j5e/animation
 * @requires module:j5e/easing
 * @requires module:j5e/fn
 */

import { normalizeIO, normalizeDevice, constrain, map, getProvider, timer } from "j5e/fn";
import { inOutSine, outSine } from "j5e/easing";
import Animation from "j5e/animation";

/**
 * The Led class constructs objects that represent a single Led attached to the physical board. Connect the LED to a digital IO for on/off functionality. Use a PWM IO for varying brightness.
 * @classdesc The LED class allows for control of Light Emitting Diodes
 * @async
 */
class LED {

  #state = {
    sink: false,
    isRunning: false,
    value: 0,
    direction: 1,
    interval: null
  };

  /**
   * Instantiate an LED
   * @param {number|string|object} io - Pin identifier or IO Options (See {@tutorial C-INSTANTIATING})
   * @example
   * <caption>Using a pin number</caption>
   * import LED from "j5e/led";
   *
   * const led = await new LED(12);
   * led.on();
   *
   * @example
   * <caption>Using a pin identifier string</caption>
   * import LED from "j5e/led";
   *
   * const led = await new LED("A1");
   * led.on();
   *
   * @example
   * <caption>Using device options</caption>
   * import LED from "j5e/led";
   *
   * const led = await new LED(12);
   * led.configure({
   *   pwm: true
   * });
   * led.on();
   */
  constructor(io) {
    return (async() => {
      io = normalizeIO(io);

      const Provider = await getProvider(io, "PWM");

      this.io = new Provider({
        pin: io.pin,
        mode: Provider.Output
      });

      this.LOW = 0;

      if (this.io.resolution) {
        this.HIGH = (1 << this.io.resolution) - 1;
      } else {
        this.HIGH = 1;
      }

      this.configure();

      return this;
    })();
  }

  /**
   * Configure an LED
   * @returns {LED} The instance on which the method was called
   * @param {object} options - Device configuration options
   * @param {number} [options.sink=false] - True if an element is wired for sink drive
   * @example
   * import LED from "j5e/led";
   *
   * const led = await new LED(14);
   * led.configure({
   *   sink: true
   * });
   *
   * // With sink: true, led.on() sets pin 14 low
   * led.on();
   */
  configure(options = {}) {
    options = normalizeDevice(options);

    if (typeof options.sink !== "undefined") {
      this.#state.sink = options.sink;
    }

    return this;
  }

  /**
   * The current value of the LED
   * @type {number}
   * @readonly
   */
  get value() {
    return this.#state.value;
  }

  /**
   * Wether the LED is on
   * @type {boolean}
   * @readonly
   */
  get isOn() {
    return !!this.#state.value;
  }

  /**
   * True if the LED is blinking, pulsing or animating
   * @type {boolean}
   * @readonly
   */
  get isRunning() {
    return this.#state.isRunning;
  };

  /**
   * Internal method that writes the current LED value to the IO
   * @access private
   */
  write() {
    let value = constrain(this.#state.value, 0, 1);
    if (this.#state.sink) {
      value = 1 - value;
    }
    value = map(value, 0, 1, this.LOW, this.HIGH);
    this.io.write(value);
  }

  /**
   * Turn an led on
   * @returns {LED} The instance on which the method was called
   * @example
   * import LED from "j5e/led";
   *
   * const led = await new LED(12);
   * led.on();
   */
  on() {
    this.#state.value = 1;
    this.write();
    return this;
  }

  /**
   * Turn an led off
   * @return {LED}
   * @example
   * import LED from "j5e/led";
   * import {timer} from "j5e/fn";
   *
   * const led = await new LED(12);
   * led.on();
   *
   * // Wait one second and turn the led off
   * timer.setTimeout(function() {
   *   led.off();
   * }, 1000);
   */
  off() {
    this.#state.value = 0;
    this.write();
    return this;
  }

  /**
   * Toggle the on/off state of an led
   * @return {LED}
   * @example
   * import LED from "j5e/led";
   * import {timer} from "j5e/fn";
   *
   * const led = await new LED(12);
   * led.toggle(); // It's on!
   *
   * // Wait one second and turn the led off
   * timer.setTimeout(function() {
   *   led.toggle(); // It's off!
   * }, 1000)
   */
  toggle() {
    return this[this.isOn ? "off" : "on"]();
  }

  /**
   * Blink the LED on a fixed interval
   * @param {Number} duration=100 - Time in ms on, time in ms off
   * @param {Function} callback - Method to call on blink
   * @return {LED}
   * @example
   * import LED from "j5e/led";
   *
   * const led = await new LED(12);
   * led.blink(1000);
   */
  blink(duration = 100, callback) {
    // Avoid traffic jams
    this.stop();

    if (typeof duration === "function") {
      callback = duration;
      duration = 100;
    }

    this.#state.isRunning = true;

    this.#state.interval = timer.setInterval(() => {
      this.toggle();
      if (typeof callback === "function") {
        callback();
      }
    }, duration);

    return this;
  }

  /**
   * Set the brightness of an led
   * @param {Number} value - Brightness value [0, 1]
   * @return {LED}
   * @example
   * import LED from "j5e/led";
   *
   * const led = await new LED(12, {
   *   pwm: true
   * });
   * led.brightness(0.5);
   */
  brightness(value) {
    this.#state.value = value;
    this.write();
    return this;
  }

  /**
   * Set the brightness of an led 0-100
   * @param {Integer} value - Brightness value [0, 100]
   * @return {LED}
   * @example
   * import LED from "j5e/led";
   *
   * const led = await new LED(12, {
   *   pwm: true
   * });
   * led.intensity(50);
   */
  intensity(value) {
    this.#state.value = map(value, 0, 100, 0, 1);
    this.io.write(value);
    return this;
  }

  /**
   * Fade an led from its current value to a new value (Requires ```pwm: true```)
   * @param {Number} val Target brightness value
   * @param {Number} [time=1000] Time in ms that a fade will take
   * @param {function} [callback] A function to run when the fade is complete
   * @return {LED}
   * @example
   * import LED from "j5e/led";
   *
   * const led = await new LED(12, {
   *   pwm: true
   * });
   * led.fade(512);
   */
  fade(val, time = 1000, callback) {

    this.stop();

    const options = {
      duration: typeof time === "number" ? time : 1000,
      keyFrames: [null, typeof val === "number" ? val : 1],
      easing: outSine,
      oncomplete: function() {
        this.stop();
        if (typeof callback === "function") {
          callback();
        }
      }
    };

    if (typeof val === "object") {
      Object.assign(options, val);
    }

    if (typeof val === "function") {
      callback = val;
    }

    if (typeof time === "object") {
      Object.assign(options, time);
    }

    if (typeof time === "function") {
      callback = time;
    }

    this.animate(options);

    return this;
  }

  /**
   * fadeIn Fade an led in to full brightness (Requires ```pwm: true```)
   * @param {Number} [time=1000] Time in ms that a fade will take
   * @param {function} [callback] A function to run when the fade is complete
   * @return {LED}
   * @example
   * <caption>Fade an LED to full brightness over half a second</caption>
   * import LED from "j5e/led";
   *
   * const led = await new LED(12, {
   *   pwm: true
   * });
   * led.fadeIn(500);
   */
  fadeIn(time = 1000, callback) {
    return this.fade(1, time, callback);
  }

  /**
   * fadeOut Fade an led out until it is off (Requires ```pwm: true```)
   * @param {Number} [time=1000] Time in ms that a fade will take
   * @param {function} [callback] A function to run when the fade is complete
   * @return {LED}
   * @example
   * <caption>Fade an LED out over half a second</caption>
   * import LED from "j5e/led";
   *
   * const led = await new LED(12, {
   *   pwm: true
   * });
   * led.on();
   * led.fadeOut(500);
   */
  fadeOut(time = 1000, callback) {
    return this.fade(0, time, callback);
  }

  /**
   * Pulse the LED in and out in a loop with specified time using ```inOutSine``` easing (Requires ```pwm: true```)
   * @param {number} [time=1000] Time in ms that a fade in/out will elapse
   * @param {function} [callback] A function to run each time the direction of pulse changes
   * @return {LED}
   * @example
   * <caption>Pulse an LED on a half second interval</caption>
   * import LED from "j5e/led";
   *
   * const led = await new LED(12, {
   *   pwm: true
   * });
   * led.pulse(500);
   */
  pulse(time = 1000, callback) {

    let options = {
      duration: typeof time === "number" ? time : 1000,
      keyFrames: [0, 1],
      metronomic: true,
      loop: true,
      easing: inOutSine,
      onloop: function() {
        /* istanbul ignore else */
        if (typeof callback === "function") {
          callback();
        }
      }
    };

    if (typeof time === "object") {
      Object.assign(options, time);
    }

    if (typeof time === "function") {
      callback = time;
    }

    return this.animate(options);

  }

  /**
   * Animate the LED by passing in a segment options object
   * @param {Object} options (See {@tutorial D-ANIMATING})
   * @return {LED}
   * @example
   * <caption>Animate an LED using an animation segment options object</caption>
   * import LED from "j5e/led";
   *
   * const led = await new LED(12, {
   *   pwm: true
   * });
   * led.animate({
   *   duration: 4000,
	 *   cuePoints: [0,  0.33, 0.66, 1],
	 *   keyFrames: [0, 0.75, 0.25, 1],
	 *   loop: true,
   *   metronomic: true
   * });
   */
  animate(options) {
    // Avoid traffic jams
    this.stop();

    this.#state.isRunning = true;

    this.#state.animation = this.#state.animation || new Animation(this);
    this.#state.animation.enqueue(options);
    return this;
  }

  /**
   * stop Stop the led from blinking, pulsing, fading, or animating
   * @return {LED}
   * @example
   * Pulse an LED and then stop after five seconds
   * import {timer} from "j5e/fn";
   * import LED from "j5e/led";
   *
   * const led = await new LED(12, {
   *   pwm: true
   * });
   * led.pulse(500);
   *
   * // Stop pulsing after five seconds
   * timer.setTimeout(function() {
   *   led.stop();
   * }, 5000);
   */
  stop() {

    if (this.#state.interval) {
      timer.clearInterval(this.#state.interval);
    }

    if (this.#state.animation) {
      this.#state.animation.stop();
    }

    this.#state.interval = null;
    this.#state.isRunning = false;

    return this;
  };

  /**
   * @param [number || object] keyFrames An array of step values or a keyFrame objects
   * @access private
   */
  normalize(keyFrames) {

    // If user passes null as the first element in keyFrames use current value
    /* istanbul ignore else */
    if (keyFrames[0] === null) {
      keyFrames[0] = {
        value: this.#state.value || 0
      };
    }

    return keyFrames.map(function(frame) {
      let value = frame;
      /* istanbul ignore else */
      if (frame !== null) {
        // frames that are just numbers represent values
        if (typeof frame === "number") {
          frame = {
            value: value,
          };
        } else {
          if (typeof frame.brightness === "number") {
            frame.value = frame.brightness;
            delete frame.brightness;
          }
          if (typeof frame.intensity === "number") {
            frame.value /= 100;
            delete frame.intensity;
          }
        }

      }
      return frame;
    });
  }

  /**
   * @position [number] value to set the led to
   * @access private
   */
  render(position) {
    this.#state.value = position[0];
    return this.write();
  };

};

export default LED;