var Board = require("./board");
var Timer = require("nanotimer");

var MICROSECONDS_PER_SECOND = 1000000;
var priv = new Map();
var defaultOctave = 4;

function clearTimer() {
  if (!this.timer) {
    return;
  }

  this.timer.clearInterval();
  delete this.timer;
}

var Controllers = {
  /**
   * Timer-based tone generator using digital high/low piezo.
   */
  DEFAULT: {
    initialize: {
      writable: true,
      value: function() {
        this.io.pinMode(this.pin, this.io.MODES.OUTPUT);
      },
    },
    tone: {
      writable: true,
      value: function(tone, duration) {
        if (isNaN(tone) || isNaN(duration)) {
          // Very Bad Things happen if one tries to play a NaN tone
          throw new Error(
            "Piezo.tone: invalid tone or duration"
          );
        }

        clearTimer.call(this);

        var timer = this.timer = new Timer();
        var value = 1;

        timer.setInterval(function() {
          value = value === 1 ? 0 : 1;
          this.io.digitalWrite(this.pin, value);

          if ((timer.difTime / 1000000) > duration) {
            clearTimer.call(this);
          }
        }.bind(this), null, tone + "u", function() {});

        return this;
      },
    },
    noTone: {
      writable: true,
      value: function() {
        this.io.digitalWrite(this.pin, 0);
        clearTimer.call(this);

        return this;
      },
    },
  },

  I2C_BACKPACK: {
    ADDRESSES: {
      value: [0x0A]
    },
    REGISTER: {
      value: {
        NO_TONE: 0x00,
        TONE: 0x01,
      },
    },
    initialize: {
      value: function(opts) {
        var state = priv.get(this);
        var address = opts.address || this.ADDRESSES[0];

        state.address = opts.address = address;

        this.io.i2cConfig(opts);
      }
    },
    tone: {
      value: function(tone, duration) {
        var state = priv.get(this);

        if (isNaN(tone) || isNaN(duration)) {
          throw new Error(
            "Piezo.tone: invalid tone or duration"
          );
        }

        var data = [
          this.REGISTER.TONE,
          this.pin,
          (tone >> 8) & 0xff,
          tone & 0xff,
          (duration >> 24) & 0xff,
          (duration >> 16) & 0xff,
          (duration >> 8) & 0xff,
          duration & 0xff,
        ];

        this.io.i2cWrite(state.address, data);

        return this;
      },
    },
    noTone: {
      value: function() {
        var state = priv.get(this);

        var data = [
          this.REGISTER.NO_TONE,
          this.pin,
        ];

        this.io.i2cWrite(state.address, data);

        return this;
      },
    },
  },
};

function Piezo(opts) {

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

  Board.Component.call(
    this, opts = Board.Options(opts)
  );

  var controller = null;

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

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

  Object.defineProperties(this, controller);

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

  // Piezo instance properties
  var state = {
    isPlaying: false,
    timeout: null,
    address: null,
  };

  priv.set(this, state);

  Object.defineProperties(this, {
    isPlaying: {
      get: function() {
        return state.isPlaying;
      }
    }
  });

  if (typeof this.initialize === "function") {
    this.initialize(opts);
  }
}

// These notes are rounded up at .5 otherwise down.
Piezo.Notes = {
  "c0": 16,
  "c#0": 17,
  "d0": 18,
  "d#0": 19,
  "e0": 21,
  "f0": 22,
  "f#0": 23,
  "g0": 25,
  "g#0": 26,
  "a0": 28,
  "a#0": 29,
  "b0": 31,
  "c1": 33,
  "c#1": 35,
  "d1": 37,
  "d#1": 39,
  "e1": 41,
  "f1": 44,
  "f#1": 47,
  "g1": 49,
  "g#1": 52,
  "a1": 55,
  "a#1": 58,
  "b1": 62,
  "c2": 65,
  "c#2": 69,
  "d2": 73,
  "d#2": 78,
  "e2": 82,
  "f2": 87,
  "f#2": 93,
  "g2": 98,
  "g#2": 104,
  "a2": 110,
  "a#2": 117,
  "b2": 124,
  "c3": 131,
  "c#3": 139,
  "d3": 147,
  "d#3": 156,
  "e3": 165,
  "f3": 175,
  "f#3": 185,
  "g3": 196,
  "g#3": 208,
  "a3": 220,
  "a#3": 233,
  "b3": 247,
  "c4": 262,
  "c#4": 277,
  "d4": 294,
  "d#4": 311,
  "e4": 330,
  "f4": 349,
  "f#4": 370,
  "g4": 392,
  "g#4": 415,
  "a4": 440,
  "a#4": 466,
  "b4": 494,
  "c5": 523,
  "c#5": 554,
  "d5": 587,
  "d#5": 622,
  "e5": 659,
  "f5": 698,
  "f#5": 740,
  "g5": 784,
  "g#5": 831,
  "a5": 880,
  "a#5": 932,
  "b5": 988,
  "c6": 1047,
  "c#6": 1109,
  "d6": 1175,
  "d#6": 1245,
  "e6": 1319,
  "f6": 1397,
  "f#6": 1480,
  "g6": 1568,
  "g#6": 1661,
  "a6": 1760,
  "a#6": 1865,
  "b6": 1976,
  "c7": 2093,
  "c#7": 2217,
  "d7": 2349,
  "d#7": 2489,
  "e7": 2637,
  "f7": 2794,
  "f#7": 2960,
  "g7": 3136,
  "g#7": 3322,
  "a7": 3520,
  "a#7": 3729,
  "b7": 3951,
  "c8": 4186,
  "c#8": 4435,
  "d8": 4699,
  "d#8": 4978,
  "e8": 5274,
  "f8": 5588,
  "f#8": 5920,
  "g8": 6272,
  "g#8": 6645,
  "a8": 7040,
  "a#8": 7459,
  "b8": 7902,
};

Piezo.Frequencies = Object.keys(Piezo.Notes).reduce(function(accum, note) {
  accum[Piezo.Notes[note]] = note;
  return accum;
}, {});

Piezo.Parsers = {};
/**
 * Get the tone from the current note. note
 * could be an int, string, array or null.
 * If int or null, leave alone. Otherwise,
 * derive what the tone should be.
 * @return int | null
 */
Piezo.Parsers.hzFromInput = function(input) {
  var output = input;

  if (Array.isArray(input)) {
    output = input[0];
  }

  // Is it a valid frequency?
  if (typeof output === "number" &&
      Piezo.Frequencies[output]) {
    return output;
  }

  // See above: Piezo.Notes { ... }
  if (typeof output === "string") {
    output = output.toLowerCase().trim();

    // Example: c#, c
    if (output.endsWith("#") || output.length === 1) {
      output += defaultOctave;
    }

    // There will never be a 0 tone
    output = Piezo.Notes[output] || null;
  }

  // Normalize NaN, null & undefined to null
  if (isNaN(output)) {
    output = null;
  }

  return output;
};

/**
 * Obtain the beat/duration count from the current
 * note. This is either an int or undefined. Default
 * to 1.
 * @return int (default 1)
 */
Piezo.Parsers.beatFromNote = function(note) {
  var beat = 1;
  if (Array.isArray(note) && note[1] !== undefined) {
    // If extant, beat will be second element of note
    beat = note[1];
  }
  return beat;
};

/**
 * Validate the octave provided to ensure the value is
 * supported and won't crash the board.
 * @return bool
 */
Piezo.isValidOctave = function(octave) {
  return typeof octave === "number" && (octave >= 0 && octave <= 8);
};

/**
 * Set or get a default octave for all notes
 * @return number
 */
Piezo.defaultOctave = function(octave) {
  if (Piezo.isValidOctave(octave)) {
    defaultOctave = octave;
  }

  return defaultOctave;
};

Piezo.ToFrequency = function(tone) {
  var toneSeconds = tone / MICROSECONDS_PER_SECOND;
  var period = toneSeconds * 2;
  return Math.round(1 / period);
};

Piezo.ToTone = function(frequency) {
  var period = 1 / frequency;
  var duty = period / 2;
  return Math.round(duty * MICROSECONDS_PER_SECOND);
};

Piezo.ToSong = function(stringSong, beats) {
  beats = beats || 1;
  var notes = stringSong.split(" ");
  var song = [];
  var note, lastNote;
  while (notes.length) {
    note = notes.shift();
    if (/^[0-9]+$/.test(note)) {
      note = parseInt(note, 10);
    }
    lastNote = song[song.length - 1];
    if (lastNote && lastNote[0] === note) {
      lastNote[1] += beats;
    } else {
      song.push([note, beats]);
    }
  }
  return song;
};

Piezo.prototype.note = function(note, duration) {
  var tone = Piezo.Parsers.hzFromInput(note);

  return this.tone(tone, duration);
};

Piezo.prototype.tone = function(tone, duration) {
  return this.frequency(Piezo.ToFrequency(tone), duration);
};

Piezo.prototype.frequency = function(frequency, duration) {
  return this.tone(Piezo.ToTone(frequency), duration);
};


Piezo.prototype.play = function(tune, callback) {
  if (typeof tune !== "object") {
    tune = {
      song: tune
    };
  }

  if (typeof tune.song === "string") {
    tune.song = Piezo.ToSong(tune.song, tune.beats);
  }

  if (tune.song && !Array.isArray(tune.song)) {
    /*
      If `tune.song` was present and not falsy,
      but also is not a string (above), or an array
      (presently), then it is likely a Hz value, so
      normalize song to the appropriate array format:
     */
    tune.song = [tune.song];
    /*
      Note: This path is taken for calls that look
      like this:

      piezo.play({
        song: 262,
      }, ...)

      Where 262 is a frequency in Hz
     */
  }

  var state = priv.get(this);
  var tempo = tune.tempo || 250;
  // Length for a single beat in ms
  var beatDuration = Math.round(60000 / tempo);
  var song = tune.song || [];
  var duration;
  var nextNoteIndex = 0;

  var next = function() {
    if (nextNoteIndex === song.length) {
      // No more notes in song:
      // Song is over
      state.isPlaying = false;
      if (typeof callback === "function") {
        callback(tune);
      }
      return;
    }

    var note = song[nextNoteIndex];
    var hz = Piezo.Parsers.hzFromInput(note);
    var beat = Piezo.Parsers.beatFromNote(note);

    duration = beat * beatDuration;
    nextNoteIndex++;

    if (hz === null) {
      this.noTone();
    } else {
      this.frequency(hz, duration);
    }

    state.timeout = setTimeout(next, duration);
  }.bind(this);

  // We are playing a song
  state.isPlaying = true;

  next();

  return this;
};

Piezo.prototype.off = function() {
  return this.noTone();
};

Piezo.prototype.stop = function() {
  var state = priv.get(this);

  /* istanbul ignore else */
  if (state.timeout) {
    clearTimeout(state.timeout);
    state.timeout = null;
  }

  return this;
};


module.exports = Piezo;
