/**
 * For working with buttons
 * @module j5e/button
 * @requires module:j5e/event
 * @requires module:j5e/fn
 */

import { Emitter } from "j5e/event";
import { debounce, normalizeDevice, normalizeIO, getProvider, timer } from "j5e/fn";

/**
 * Class representing a button
 * @classdesc The Button class allows for control of digital buttons
 * @async
 * @extends module:j5e/event.Emitter
 * @fires Button#open
 * @fires Button#close
 */
class Button extends Emitter {

  #state = {
    holdtime: null,
    last: null,
    isPullup: null,
    normallyClosed: null,
    interval: null
  };

  /**
   * Instantiate a button
   * @param {number|string|object} io - Pin identifier or IO Options (See {@tutorial C-INSTANTIATING})
   * @param {number|string} [io.mode=Input] - Device configuration options. If a number, a valid value based on the Provider's constants. If a string, one of "Input", "InputPullUp", or "InputPullDown"
   * @example
   * <caption>Use a button to control an LED</caption>
   * import Button from "j5e/button";
   * import LED from "j5e/led";
   *
   * const button = await new Button(12);
   * const led = await new LED(13);
   *
   * button.on("open", function() {
   *   led.off();
   * });
   *
   * button.on("close", function() {
   *   led.on();
   * });
   */
  constructor(io) {
    return (async() => {

      io = normalizeIO(io);
      super();

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

      let mode = Provider.Input;
      if (typeof io.mode !== "undefined") {
        if (typeof io.mode === "string") {
          mode = Provider[io.mode];
        } else {
          mode = io.mode;
        }
      }

      this.#state.isPullup = mode === Provider.InputPullUp;

      this.io = new Provider({
        pin: io.pin,
        mode,
        edge: Provider.Rising | Provider.Falling,
        onReadable: () => {
          this.trigger();
        }
      });

      this.configure({
        debounce: 7,
        holdtime: 500,
        normallyClosed: false
      });

      this.#state.last = this.upValue;

      return this;
    })();

  }

  /**
   * Configure a button
   * @returns {Button} The instance on which the method was called
   * @param {object} options - Device configuration options
   * @param {number} [options.holdtime=500] - The amount of time a button must be held down before emitting an hold event
   * @param {number} [options.debounce=7] - The amount of time in milliseconds to delay button events firing. Cleans up "noisy" state changes
   * @param {string} [options.type="NO"] - The type of button, "NO" for normally open, "NC" for normally closed
   * @example
   * import Button from "j5e/button";
   * import LED from "j5e/led";
   *
   * const button = await new Button(14);
   * button.configure({
   *   debounce: 20
   * });
   *
   * button.on("open", function() {
   *  led.off();
   * });
   *
   * button.on("close", function() {
   *  led.on();
   * });
   */
  configure(options) {
    options = normalizeDevice(options);

    if (typeof options.normallyClosed !== "undefined") {
      this.#state.normallyClosed = options.normallyClosed;
    }
    this.#state.holdtime = options.holdtime || this.#state.holdtime;
    this.#state.debounce = options.debounce || this.#state.debounce;

    this.trigger = debounce(this.processRead.bind(this), this.#state.debounce);

    return this;
  }

  /**
   * True if the button is being pressed
   * @type {boolean}
   * @readonly
   */
  get isClosed() {
    return this.io.read() === this.downValue;
  }

  /**
   * True if the button is not being pressed
   * @type {boolean}
   * @readonly
   */
  get isOpen() {
    return this.io.read() === this.upValue;
  }

  /**
   * Get the raw downValue (depends on type and io input mode)
   * @type {number}
   * @readonly
   */
  get downValue() {
    return 1 ^ this.#state.isPullup ^ this.#state.normallyClosed;
  }

  /**
   * Get the raw upValue (depends on type and io input mode)
   * @type {number}
   * @readonly
   */
  get upValue() {
    return 0 ^ this.#state.isPullup ^ this.#state.normallyClosed;
  }

  /**
   * The length of time a button must be held before firing a hold event (in ms)
   * @type {number}
   */
  get holdtime() {
    return this.#state.holdtime;
  }

  set holdtime(newHoldtime) {
    this.#state.holdtime = newHoldtime;
  }

  intialize() { }

  processRead() {
    if (this.isOpen) {
      this.emit("open");
      timer.clearTimeout(this.#state.interval);
    } else {
      this.emit("close");
      this.#state.interval = timer.setTimeout(() => {
        this.#state.interval = null;
        if (this.isClosed) {
          this.emit("hold");
        }
      }, this.#state.holdtime);
    }
  }
}

export default Button;