var Emitter = require("events").EventEmitter;
var util = require("util");
var Board = require("./board");
var Fn = require("./fn");

var scale = Fn.scale;
var toFixed = Fn.toFixed;
var uint16 = Fn.uint16;

var priv = new Map();

var aliases = {
  down: ["down", "press", "tap", "impact", "hit", "touch"],
  up: ["up", "release"],
  hold: ["hold"]
};

function flatKeys(opts) {
  var keys = [];

  if (opts.keys && Array.isArray(opts.keys)) {
    keys = opts.keys.slice();

    if (keys.every(Array.isArray)) {
      keys = keys.reduce(function(accum, row) {
        return accum.concat(row);
      }, []);
    }
  }

  return keys;
}

// TODO:
//
// Provide a mechanism for explicitly naming aliases for buttons
//
//
var Controllers = {
  MPR121: {
    ADDRESSES: {
      value: [0x5A, 0x5B, 0x5C, 0x5D]
    },
    REGISTER: {
      value: require("./definitions/mpr121.js")
    },
    initialize: {
      value: function(opts, dataHandler) {

        var state = priv.get(this);
        var address = opts.address || this.ADDRESSES[0];
        var keys = flatKeys(opts);
        var keyMap = this.REGISTER.MAPS[opts.controller].KEYS;
        var targets = this.REGISTER.MAPS[opts.controller].TARGETS;
        var mapping = Object.keys(keyMap).reduce(function(accum, index) {
          accum[index] = keyMap[index];
          return accum;
        }, []);

        var length = mapping.length;
        opts.address = address;

        this.io.i2cConfig(opts);

        this.io.i2cWrite(address, this.REGISTER.MPR121_SOFTRESET, 0x63);

        this.io.i2cWrite(address, this.REGISTER.MHD_RISING, 0x01);
        this.io.i2cWrite(address, this.REGISTER.NHD_AMOUNT_RISING, 0x01);
        this.io.i2cWrite(address, this.REGISTER.NCL_RISING, 0x00);
        this.io.i2cWrite(address, this.REGISTER.FDL_RISING, 0x00);

        this.io.i2cWrite(address, this.REGISTER.MHD_FALLING, 0x01);
        this.io.i2cWrite(address, this.REGISTER.NHD_AMOUNT_FALLING, 0x01);
        this.io.i2cWrite(address, this.REGISTER.NCL_FALLING, 0xFF);
        this.io.i2cWrite(address, this.REGISTER.FDL_FALLING, 0x02);

        // https://www.sparkfun.com/datasheets/Components/MPR121.pdf
        //
        // p. 12
        //
        // 6. Touch and Release Threshold (0x41~0x5A)
        // The threshold is defined as a deviation value from the baseline value,
        // so it remains constant even baseline value changes. Typically the touch
        // threshold is a little bigger than the release threshold to touch debounce
        // and hysteresis. The range of the value is 0~255. For typical touch
        // application, the value can be in range 0x05~0x30 for example. The setting
        // of the threshold is depended on the actual application. For the operation
        // details and how to set the threshold refer to application note AN3892 and
        // MPR121 design guidelines.

        this.sensitivity = {
          // Inverted map approximately to 8 bit values:
          //
          // press: 12
          // release: 6
          //
          press: Array(12).fill(0.95),
          release: Array(12).fill(0.975),
          // These defaults as based on the defaults shown
          // in examples published by Adafruit
          // https://github.com/adafruit/Adafruit_MPR121/blob/master/Adafruit_MPR121.cpp#L43
        };

        // If keys were specified for a MPR121_SHIELD (adafruit shield),
        // then reverse the keys to align with the output of the.
        if (opts.keys && opts.controller === "MPR121_SHIELD") {
          keys = keys.reverse();
        }

        if (opts.sensitivity) {
          if (Array.isArray(opts.sensitivity)) {
            // Initialized as:
            //
            // new five.Keypad({
            //   controller: "MPR121",
            //   sensitivity: [
            //     { press: 0-1, release: 0-1, },
            //     { press: 0-1, release: 0-1, },
            //     { press: 0-1, release: 0-1, },
            //     ...
            //   ],
            // });
            //
            opts.sensitivity.forEach(function(sensitivity, index) {
              if (typeof sensitivity.press !== "undefined") {
                this.sensitivity.press[index] = sensitivity.press;
              }

              if (typeof sensitivity.release !== "undefined") {
                this.sensitivity.release[index] = sensitivity.release;
              }
            }, this);
          } else {
            // Initialized as:
            //
            // new five.Keypad({
            //   controller: "MPR121",
            //   sensitivity: {
            //     press: 0-1,
            //     release: 0-1,
            //   },
            // });
            //
            if (typeof opts.sensitivity.press !== "undefined") {
              this.sensitivity.press.fill(opts.sensitivity.press);
            }

            if (typeof opts.sensitivity.release !== "undefined") {
              this.sensitivity.release.fill(opts.sensitivity.release);
            }
          }
        }

        // The chip expects a LOWER value for a HIGHER sensitivity.
        // Most people don't think this way, so Johnny-Five aligns with
        // user/developer intuition, which we assume for this case is:
        //
        //  "Higher sensitivity value means greater touch sensitivity"
        //
        // This means that the value we received needs to be inverted
        // before it's written to the chip threshold configuration.
        //
        for (var i = 0; i < 12; i++) {
          this.io.i2cWrite(
            address,
            this.REGISTER.ELE0_TOUCH_THRESHOLD + (i << 1),
            scale(toFixed(1 - this.sensitivity.press[i], 3), 0, 1, 0, 255)
          );
          this.io.i2cWrite(
            address,
            this.REGISTER.ELE0_RELEASE_THRESHOLD + (i << 1),
            scale(toFixed(1 - this.sensitivity.release[i], 3), 0, 1, 0, 255)
          );
        }

        this.io.i2cWrite(address, this.REGISTER.FILTER_CONFIG, 0x13);
        this.io.i2cWrite(address, this.REGISTER.AFE_CONFIGURATION, 0x80);

        this.io.i2cWrite(address, this.REGISTER.AUTO_CONFIG_CONTROL_0, 0x8F);
        this.io.i2cWrite(address, this.REGISTER.AUTO_CONFIG_USL, 0xE4);
        this.io.i2cWrite(address, this.REGISTER.AUTO_CONFIG_LSL, 0x94);
        this.io.i2cWrite(address, this.REGISTER.AUTO_CONFIG_TARGET_LEVEL, 0xCD);

        this.io.i2cWrite(address, this.REGISTER.ELECTRODE_CONFIG, 0xCC);

        if (!keys.length) {
          keys = Array.from(Object.assign({}, keyMap, {
            length: length
          }));
        }

        state.length = length;
        state.touches = touches(length);
        state.keys = keys;
        state.mapping = mapping;
        state.targets = targets;
        state.isMultitouch = true;

        this.io.i2cRead(address, 0x00, 2, function(bytes) {
          dataHandler(uint16(bytes[1], bytes[0]));
        });
      }
    },
    toAlias: {
      value: function(index) {
        var state = priv.get(this);
        return state.keys[index];
      }
    },
    toIndices: {
      value: function(raw) {
        var state = priv.get(this);
        var indices = [];
        for (var i = 0; i < 12; i++) {
          if (raw & (1 << i)) {
            indices.push(state.targets[raw & (1 << i)]);
          }
        }
        return indices;
      }
    },
  },

  // https://learn.sparkfun.com/tutorials/vkey-voltage-keypad-hookup-guide
  VKEY: {
    initialize: {
      value: function(opts, dataHandler) {
        var state = priv.get(this);
        var aref = opts.aref || this.io.aref || 5;
        var use5V = Fn.inRange(aref, 4.5, 5.5);
        var keys = flatKeys(opts);
        var mapping = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
        var length = 0;

        if (!keys.length) {
          keys = mapping;
        }

        state.scale = [
          use5V ? 17 : 26,
          use5V ? 40 : 58,
          use5V ? 496 : 721,
        ];

        length = mapping.length;
        state.length = length;
        state.touches = touches(length);
        state.mapping = mapping;
        state.keys = keys;
        state.isMultitouch = false;

        this.io.pinMode(this.pin, this.io.MODES.ANALOG);
        this.io.analogRead(this.pin, function(adc) {
          dataHandler(adc);
        }.bind(this));
      },
    },
    toAlias: {
      value: function(index) {
        var state = priv.get(this);
        return state.keys[index];
      }
    },
    toIndices: {
      value: function(raw) {
        var state = priv.get(this);
        var length = state.length;
        var low = state.scale[0];
        var step = state.scale[1];
        var high = state.scale[2];

        if (raw < low || raw > high) {
          return [];
        }

        return [(length - ((raw - low) / step)) | 0];
      }
    }
  },

  // WaveShare AD
  //  - http://www.amazon.com/WaveShare-Accessory-buttons-controlled-keyboard/dp/B00KM6UXVS
  //  - http://www.wvshare.com/product/A_D-Keypad.htm
  //
  //  TODO: Create docs to show how to create a DIY keypad
  //        that works with this class.
  //
  ANALOG: {
    initialize: {
      value: function(opts, dataHandler) {

        var keys = flatKeys(opts);
        var mapping = [];
        var length = 0;

        if (opts.length && !keys.length) {
          keys = Array.from({
            length: opts.length
          }, function(_, key) {
            return key;
          });
        }

        if (!keys.length) {
          throw new Error(
            "Missing `keys`. Analog Keypad requires either a numeric `length` or a `keys` array."
          );
        }

        mapping = keys;
        length = mapping.length;

        var state = priv.get(this);
        // keys + Idle state == length + 1
        var total = length + 1;
        var vrange = Math.round(1023 / total);
        var ranges = Array.from({
          length: total
        }, function(_, index) {
          var start = vrange * index;
          return Array.from({
            length: vrange - 1
          }, function(_, index) {
            return start + index;
          });
        });

        state.length = length;
        state.ranges = ranges;
        state.touches = touches(length);
        state.mapping = mapping;
        state.keys = keys;
        state.isMultitouch = true;

        this.io.pinMode(this.pin, this.io.MODES.ANALOG);
        this.io.analogRead(this.pin, function(adc) {
          dataHandler(adc);
        });
      }
    },
    toAlias: {
      value: function(index) {
        var state = priv.get(this);
        return state.keys[index];
      }
    },
    toIndices: {
      value: function(raw) {
        var state = priv.get(this);
        var ranges = state.ranges;
        var index = ranges.findIndex(function(range) {
          return range.includes(raw);
        });

        if (index === state.length) {
          index--;
        }

        if (index < 0) {
          return [];
        }

        return [index];
      }
    }
  },
  AT42QT1070: {
    ADDRESSES: {
      value: [0x1B]
    },
    REGISTER: {
      value: {
        READ: 0x03
      }
    },
    initialize: {
      value: function(opts, dataHandler) {
        var state = priv.get(this);
        var address = opts.address || this.ADDRESSES[0];
        var keys = flatKeys(opts);
        var mapping = [0, 1, 2, 3, 4, 5, 6];
        var length = 0;

        if (!keys.length) {
          keys = mapping;
        }

        length = mapping.length;

        state.length = length;
        state.touches = touches(length);
        state.mapping = mapping;
        state.keys = keys;
        state.isMultitouch = true;

        this.io.i2cConfig(opts);
        this.io.i2cRead(address, this.REGISTER.READ, 1, function(data) {
          dataHandler(data[0]);
        });
      }
    },
    toAlias: {
      value: function(index) {
        var state = priv.get(this);
        return state.keys[index];
      }
    },
    toIndices: {
      value: function(raw) {
        var indices = [];
        for (var i = 0; i < 7; i++) {
          if (raw & (1 << i)) {
            indices.push(i);
          }
        }
        return indices;
      }
    }
  },

  "3X4_I2C_NANO_BACKPACK": {
    ADDRESSES: {
      value: [0x0A, 0x0B, 0x0C, 0x0D]
    },
    initialize: {
      value: function(opts, dataHandler) {
        var state = priv.get(this);
        var address = opts.address || this.ADDRESSES[0];
        var keys = flatKeys(opts);
        var mapping = [1, 2, 3, 4, 5, 6, 7, 8, 9, "*", 0, "#"];
        var length = 0;

        if (!keys.length) {
          keys = mapping;
        }

        length = mapping.length;

        state.length = length;
        state.touches = touches(length);
        state.mapping = mapping;
        state.keys = keys;
        state.isMultitouch = true;

        opts.address = address;

        this.io.i2cConfig(opts);
        this.io.i2cRead(address, 2, function(bytes) {
          dataHandler(uint16(bytes[0], bytes[1]));
        });
      }
    },
    toAlias: {
      value: function(index) {
        var state = priv.get(this);
        return state.keys[index];
      }
    },
    toIndices: {
      value: function(raw) {
        var state = priv.get(this);
        var indices = [];
        for (var i = 0; i < state.length; i++) {
          if (raw & (1 << i)) {
            indices.push(i);
          }
        }
        return indices;
      }
    }
  },
};


// Otherwise known as...
Controllers.MPR121QR2 = Controllers.MPR121;
Controllers.MPR121QR2_SHIELD = Controllers.MPR121;
Controllers.MPR121_KEYPAD = Controllers.MPR121;
Controllers.MPR121_SHIELD = Controllers.MPR121;
Controllers.QTOUCH = Controllers.AT42QT1070;

function touches(length) {
  return Array.from({
    length: length
  }, function() {
    return {
      timeout: null,
      value: 0,
    };
  });
}

function Keypad(opts) {

  if (!(this instanceof Keypad)) {
    return new Keypad(opts);
  }

  // Initialize a Device instance on a Board
  Board.Component.call(
    this, opts = Board.Options(opts)
  );

  var raw = null;
  var controller = null;
  var state = {
    touches: null,
    timeout: null,
    length: null,
    keys: null,
    mapping: null,
    holdtime: null,
  };

  var trigger = Fn.debounce(function(type, value) {
    var event = {
      type: type,
      which: value,
      timestamp: Date.now()
    };
    aliases[type].forEach(function(type) {
      this.emit(type, event);
    }, this);

    this.emit("change", Object.assign({}, event));
  }, 5);


  if (opts.controller && typeof opts.controller === "string") {
    controller = Controllers[opts.controller.toUpperCase()];
  } else {
    controller = opts.controller;
  }

  if (controller == null) {
    controller = Controllers.ANALOG;
  }

  Board.Controller.call(this, controller, opts);

  state.holdtime = opts.holdtime ? opts.holdtime : 500;

  priv.set(this, state);

  if (typeof this.initialize === "function") {
    this.initialize(opts, function(data) {

      raw = data;

      var now = Date.now();
      var indices = this.toIndices(data);
      var kLength = state.length;

      var lists = {
        down: [],
        hold: [],
        up: [],
      };

      var target = null;
      var alias = null;

      for (var k = 0; k < kLength; k++) {
        alias = this.toAlias(k);

        if (indices.includes(k)) {
          if (state.touches[k].value === 0) {

            state.touches[k].timeout = now + state.holdtime;
            lists.down.push(alias);

          } else if (state.touches[k].value === 1) {
            if (state.touches[k].timeout !== null && now > state.touches[k].timeout) {
              state.touches[k].timeout = now + state.holdtime;
              lists.hold.push(alias);
            }
          }

          state.touches[k].value = 1;
        } else {
          if (state.touches[k].value === 1) {
            state.touches[k].timeout = null;
            lists.up.push(alias);
          }
          state.touches[k].value = 0;
        }
        target = null;
        alias = null;
      }

      Object.keys(lists).forEach(function(key) {
        var list = lists[key];

        if (list.length) {
          trigger.call(this, key, list);
        }
      }, this);
    }.bind(this));
  }

  Object.defineProperties(this, {
    isMultitouch: {
      get: function() {
        return state.isMultitouch;
      }
    },
    value: {
      get: function() {
        return raw;
      }
    },
  });
}

util.inherits(Keypad, Emitter);

module.exports = Keypad;
