/**
 * Description: Read and edit events from various sources (ArrayBuffer, Stream)
 *
 * @package     midi-Synth
 * @category    class js
 * @author      https://github.com/nfroidure/midifile/tree/master/src
 * @copyright
 * @license     GPL v2 or later
 */

// Static constants
// Event types
const EVENT_META = 0xff;                        //255
const EVENT_SYSEX = 0xf0;                       //240
const EVENT_DIVSYSEX = 0xf7;                    //247
const EVENT_MIDI = 0x8;                         //8
// Meta event types
const EVENT_META_SEQUENCE_NUMBER = 0x00;        //0
const EVENT_META_TEXT = 0x01;                   //1
const EVENT_META_COPYRIGHT_NOTICE = 0x02;       //2
const EVENT_META_TRACK_NAME = 0x03;             //3
const EVENT_META_INSTRUMENT_NAME = 0x04;        //4
const EVENT_META_LYRICS = 0x05;                 //5
const EVENT_META_MARKER = 0x06;                 //6
const EVENT_META_CUE_POINT = 0x07;              //7
const EVENT_META_MIDI_CHANNEL_PREFIX = 0x20;    //32
const EVENT_META_END_OF_TRACK = 0x2f;           //47
const EVENT_META_SET_TEMPO = 0x51;              //81
const EVENT_META_SMTPE_OFFSET = 0x54;           //84
const EVENT_META_TIME_SIGNATURE = 0x58;         //88
const EVENT_META_KEY_SIGNATURE = 0x59;          //89
const EVENT_META_SEQUENCER_SPECIFIC = 0x7f;     //127
// MIDI event types
const EVENT_MIDI_NOTE_OFF = 0x8;                //8
const EVENT_MIDI_NOTE_ON = 0x9;                 //9
const EVENT_MIDI_NOTE_AFTERTOUCH = 0xa;         //10 ?
const EVENT_MIDI_CONTROLLER = 0xb;              //11
const EVENT_MIDI_PROGRAM_CHANGE = 0xc;          //12
const EVENT_MIDI_CHANNEL_AFTERTOUCH = 0xd;      //13
const EVENT_MIDI_PITCH_BEND = 0xe;              //14 +
const EVENT_MIDI_BANK_CHANGE = 0x20;            //32
//MIDI_CONTROLLER - param1 (8 == item.type && 11 == item.subtype)
//  0 = Bank Select (followed by CC32)
//  1 = Modulation wheel          +
//  2 = Breath Control
//  4 = FootMSB
//  7 = Volume                    +
//  8 = Balance                   +
// 10 = PanPot                    +
// 11 = Expression                +
// 32 = Bank Select
// 64 = Sustain (on/off)          +
// 65 = Portamento (on/off)
// 66 = Sostenuto (on/off)        +
// 67 = SoftPedal (on/off)        +
// 69 = Hold Pedal2 (on/off)
// 71 = Resonance (filter)
// 74 = Frequency Cutoff (filter)
// 92 = Tremolo
// 95 = Phaser

// MIDI event sizes
const MIDI_1PARAM_EVENTS = [
  EVENT_MIDI_PROGRAM_CHANGE,
  EVENT_MIDI_CHANNEL_AFTERTOUCH,
];
const MIDI_2PARAMS_EVENTS = [
  EVENT_MIDI_NOTE_OFF,
  EVENT_MIDI_NOTE_ON,
  EVENT_MIDI_NOTE_AFTERTOUCH,
  EVENT_MIDI_CONTROLLER,
  EVENT_MIDI_PITCH_BEND,
];

class MIDIEvents {
  /**
   * Read and edit events from various sources (ArrayBuffer, Stream)
   * @return {[type]}        [description]
   */
  constructor () {

  }

  static get EVENT_META() {
    return EVENT_META;
  }
  static get EVENT_SYSEX() {
    return EVENT_SYSEX;
  }
  static get EVENT_DIVSYSEX() {
    return EVENT_DIVSYSEX;
  }
  static get EVENT_MIDI() {
    return EVENT_MIDI;
  }

  static get EVENT_META_SEQUENCE_NUMBER() {
    return EVENT_META_SEQUENCE_NUMBER;
  }
  static get EVENT_META_TEXT() {
    return EVENT_META_TEXT;
  }
  static get EVENT_META_COPYRIGHT_NOTICE() {
    return EVENT_META_COPYRIGHT_NOTICE;
  }
  static get EVENT_META_TRACK_NAME() {
    return EVENT_META_TRACK_NAME;
  }
  static get EVENT_META_INSTRUMENT_NAME() {
    return EVENT_META_INSTRUMENT_NAME;
  }
  static get EVENT_META_LYRICS() {
    return EVENT_META_LYRICS;
  }
  static get EVENT_META_MARKER() {
    return EVENT_META_MARKER;
  }
  static get EVENT_META_CUE_POINT() {
    return EVENT_META_CUE_POINT;
  }
  static get EVENT_META_MIDI_CHANNEL_PREFIX() {
    return EVENT_META_MIDI_CHANNEL_PREFIX;
  }
  static get EVENT_META_END_OF_TRACK() {
    return EVENT_META_END_OF_TRACK;
  }
  static get EVENT_META_SET_TEMPO() {
    return EVENT_META_SET_TEMPO;
  }
  static get EVENT_META_SMTPE_OFFSET() {
    return EVENT_META_SMTPE_OFFSET;
  }
  static get EVENT_META_TIME_SIGNATURE() {
    return EVENT_META_TIME_SIGNATURE;
  }
  static get EVENT_META_KEY_SIGNATURE() {
    return EVENT_META_KEY_SIGNATURE;
  }
  static get EVENT_META_SEQUENCER_SPECIFIC() {
    return EVENT_META_SEQUENCER_SPECIFIC;
  }

  static get EVENT_MIDI_NOTE_OFF() {
    return EVENT_MIDI_NOTE_OFF;
  }
  static get EVENT_MIDI_NOTE_ON() {
    return EVENT_MIDI_NOTE_ON;
  }
  static get EVENT_MIDI_NOTE_AFTERTOUCH() {
    return EVENT_MIDI_NOTE_AFTERTOUCH;
  }
  static get EVENT_MIDI_CONTROLLER() {
    return EVENT_MIDI_CONTROLLER;
  }
  static get EVENT_MIDI_PROGRAM_CHANGE() {
    return EVENT_MIDI_PROGRAM_CHANGE;
  }
  static get EVENT_MIDI_CHANNEL_AFTERTOUCH() {
    return EVENT_MIDI_CHANNEL_AFTERTOUCH;
  }
  static get EVENT_MIDI_PITCH_BEND() {
    return EVENT_MIDI_PITCH_BEND;
  }

  static get MIDI_1PARAM_EVENTS() {
    return MIDI_1PARAM_EVENTS;
  }
  static get MIDI_2PARAMS_EVENTS() {
    return MIDI_2PARAMS_EVENTS;
  }

  /**
   * Create an event stream parser
   * @param  {[type]} stream     [description]
   * @param  {[type]} startAt    [description]
   * @param  {[type]} strictMode [description]
   * @return {[type]}            [description]
   */
  createParser(stream, startAt, strictMode ) {
    // Private vars
    // Common vars
    var eventTypeByte;
    var event;
    // MIDI events vars
    var MIDIEventType;
    var MIDIEventChannel;
    var MIDIEventParam1;

    // Wrap DataView into a data stream
    if (stream instanceof DataView) {
      stream = {
        position: startAt || 0,
        buffer: stream,
        readUint8: function() {
          return this.buffer.getUint8(this.position++);
        },
        readUint16: function() {
          var v = this.buffer.getUint16(this.position);
          this.position = this.position + 2;
          return v;
        },
        readVarInt: function() {
          var v = 0;
          var i = 0;
          var b;

          while (4 > i++) {
            b = this.readUint8();

            if (b & 0x80) {
              v += b & 0x7f;
              v <<= 7;
            } else {
              return v + b;
            }
          }
          throw new Error(
            '0x' +
              this.position.toString(16) +
              ':' +
              ' Variable integer length cannot exceed 4 bytes'
          );
        },
        readBytes: function(length) {
          var bytes = [];

          for (; 0 < length; length--) {
            bytes.push(this.readUint8());
          }
          return bytes;
        },
        pos: function() {
          return '0x' + (this.buffer.byteOffset + this.position).toString(16);
        },
        end: function() {
          return this.position === this.buffer.byteLength;
        },
      };
      startAt = 0;
    }
    // Consume stream till not at start index
    if (0 < startAt) {
      while (startAt--) {
        stream.readUint8();
      }
    }
    // creating the parser object
    return {
      // Read the next event
      next: function() {
        if (stream){
          // Check available datas
          if (stream.end()) {
            return null;
          }
          // Creating the event
          event = {
            // Memoize the event index
            index: stream.pos(),
            // Read the delta time
            delta: stream.readVarInt(),
          };
          // Read the eventTypeByte
          eventTypeByte = stream.readUint8();
          if (0xf0 === (eventTypeByte & 0xf0)) {
            // Meta events
            if (eventTypeByte === MIDIEvents.EVENT_META) {
              event.type = MIDIEvents.EVENT_META;
              event.subtype = stream.readUint8();
              event.length = stream.readVarInt();
              switch (event.subtype) {
                case MIDIEvents.EVENT_META_SEQUENCE_NUMBER:
                  if (strictMode && 2 !== event.length) {
                    throw new Error(stream.pos() + ' Bad metaevent length.');
                  }
                  event.msb = stream.readUint8();
                  event.lsb = stream.readUint8();
                  return event;
                case MIDIEvents.EVENT_META_TEXT:
                case MIDIEvents.EVENT_META_COPYRIGHT_NOTICE:
                case MIDIEvents.EVENT_META_TRACK_NAME:
                case MIDIEvents.EVENT_META_INSTRUMENT_NAME:
                case MIDIEvents.EVENT_META_LYRICS:
                case MIDIEvents.EVENT_META_MARKER:
                case MIDIEvents.EVENT_META_CUE_POINT:
                  event.data = stream.readBytes(event.length);
                  return event;
                case MIDIEvents.EVENT_META_MIDI_CHANNEL_PREFIX:
                  if (strictMode && 1 !== event.length) {
                    throw new Error(stream.pos() + ' Bad metaevent length.');
                  }
                  event.prefix = stream.readUint8();
                  return event;
                case MIDIEvents.EVENT_META_END_OF_TRACK:
                  if (strictMode && 0 !== event.length) {
                    throw new Error(stream.pos() + ' Bad metaevent length.');
                  }
                  return event;
                case MIDIEvents.EVENT_META_SET_TEMPO:
                  if (strictMode && 3 !== event.length) {
                    throw new Error(
                      stream.pos() + ' Tempo meta event length must be 3.'
                    );
                  }
                  event.tempo =
                    (stream.readUint8() << 16) +
                    (stream.readUint8() << 8) +
                    stream.readUint8();
                  event.tempoBPM = 60000000 / event.tempo;
                  return event;
                case MIDIEvents.EVENT_META_SMTPE_OFFSET:
                  if (strictMode && 5 !== event.length) {
                    throw new Error(stream.pos() + ' Bad metaevent length.');
                  }
                  event.hour = stream.readUint8();
                  if (strictMode && 23 < event.hour) {
                    throw new Error(
                      stream.pos() +
                        ' SMTPE offset hour value must' +
                        ' be part of 0-23.'
                    );
                  }
                  event.minutes = stream.readUint8();
                  if (strictMode && 59 < event.minutes) {
                    throw new Error(
                      stream.pos() +
                        ' SMTPE offset minutes value' +
                        ' must be part of 0-59.'
                    );
                  }
                  event.seconds = stream.readUint8();
                  if (strictMode && 59 < event.seconds) {
                    throw new Error(
                      stream.pos() +
                        ' SMTPE offset seconds value' +
                        ' must be part of 0-59.'
                    );
                  }
                  event.frames = stream.readUint8();
                  if (strictMode && 30 < event.frames) {
                    throw new Error(
                      stream.pos() +
                        ' SMTPE offset frames value must' +
                        ' be part of 0-30.'
                    );
                  }
                  event.subframes = stream.readUint8();
                  if (strictMode && 99 < event.subframes) {
                    throw new Error(
                      stream.pos() +
                        ' SMTPE offset subframes value' +
                        ' must be part of 0-99.'
                    );
                  }
                  return event;
                case MIDIEvents.EVENT_META_KEY_SIGNATURE:
                  if (strictMode && 2 !== event.length) {
                    throw new Error(stream.pos() + ' Bad metaevent length.');
                  }
                  event.key = stream.readUint8();
                  if (strictMode && (-7 > event.key || 7 < event.key)) {
                    throw new Error(stream.pos() + ' Bad metaevent length.');
                  }
                  event.scale = stream.readUint8();
                  if (strictMode && 0 !== event.scale && 1 !== event.scale) {
                    throw new Error(
                      stream.pos() +
                        ' Key signature scale value must' +
                        ' be 0 or 1.'
                    );
                  }
                  return event;
                case MIDIEvents.EVENT_META_TIME_SIGNATURE:
                  if (strictMode && 4 !== event.length) {
                    throw new Error(stream.pos() + ' Bad metaevent length.');
                  }
                  event.data = stream.readBytes(event.length);
                  event.param1 = event.data[0];
                  event.param2 = event.data[1];
                  event.param3 = event.data[2];
                  event.param4 = event.data[3];
                  return event;
                case MIDIEvents.EVENT_META_SEQUENCER_SPECIFIC:
                  event.data = stream.readBytes(event.length);
                  return event;
                default:
                  if (strictMode) {
                    throw new Error(
                      stream.pos() +
                        ' Unknown meta event type ' +
                        '(' +
                        event.subtype.toString(16) +
                        ').'
                    );
                  }
                  event.data = stream.readBytes(event.length);
                  return event;
              }
              // System events
            } else if (
              eventTypeByte === MIDIEvents.EVENT_SYSEX ||
              eventTypeByte === MIDIEvents.EVENT_DIVSYSEX
            ) {
              event.type = eventTypeByte;
              event.length = stream.readVarInt();
              event.data = stream.readBytes(event.length);
              return event;
              // Unknown event, assuming it's system like event
            } else {
              if (strictMode) {
                throw new Error(
                  stream.pos() +
                    ' Unknown event type ' +
                    eventTypeByte.toString(16) +
                    ', Delta: ' +
                    event.delta +
                    '.'
                );
              }
              event.type = eventTypeByte;
              event.badsubtype = stream.readVarInt();
              event.length = stream.readUint8();
              event.data = stream.readBytes(event.length);
              return event;
            }
            // MIDI eventsdestination[index++]
          } else {
            // running status
            if (0 === (eventTypeByte & 0x80)) {
              if (!MIDIEventType) {
                throw new Error(
                  stream.pos() + ' Running status without previous event'
                );
              }
              MIDIEventParam1 = eventTypeByte;
            } else {
              MIDIEventType = eventTypeByte >> 4;
              MIDIEventChannel = eventTypeByte & 0x0f;
              MIDIEventParam1 = stream.readUint8();
            }
            event.type = MIDIEvents.EVENT_MIDI;
            event.subtype = MIDIEventType;
            event.channel = MIDIEventChannel;
            event.param1 = MIDIEventParam1;
            switch (MIDIEventType) {
              case MIDIEvents.EVENT_MIDI_NOTE_OFF:
                event.param2 = stream.readUint8();
                return event;
              case MIDIEvents.EVENT_MIDI_NOTE_ON:
                event.param2 = stream.readUint8();

                // If velocity is 0, it's a note off event in fact
                if (!event.param2) {
                  event.subtype = MIDIEvents.EVENT_MIDI_NOTE_OFF;
                  event.param2 = 127; // Find a standard telling what to do here
                }
                return event;
              case MIDIEvents.EVENT_MIDI_NOTE_AFTERTOUCH:
                event.param2 = stream.readUint8();
                return event;
              case MIDIEvents.EVENT_MIDI_CONTROLLER:
                event.param2 = stream.readUint8();
                return event;
              case MIDIEvents.EVENT_MIDI_PROGRAM_CHANGE:
                return event;
              case MIDIEvents.EVENT_MIDI_CHANNEL_AFTERTOUCH:
                return event;
              case MIDIEvents.EVENT_MIDI_PITCH_BEND:
                event.param2 = stream.readUint8();
                return event;
              default:
                if (strictMode) {
                  throw new Error(
                    stream.pos() +
                      ' Unknown MIDI event type ' +
                      '(' +
                      MIDIEventType.toString(16) +
                      ').'
                  );
                }
                return event;
            }
          }
        }
      },
    };
  }
  /**
   * Return the buffer length needed to encode the given events
   * @param  {[type]} events      [description]
   * @param  {[type]} destination [description]
   * @param  {[type]} strictMode  [description]
   * @return {[type]}             [description]
   */
  writeToTrack(events, destination, strictMode) {
    var index = 0;
    var i;
    var j;
    var k;
    var l;

    // Converting each event to binary MIDI datas
    for (i = 0, j = events.length; i < j; i++) {
      // Writing delta value
      if (events[i].delta >>> 28) {
        throw Error(
          'Event #' +
            i +
            ': Maximum delta time value reached (' +
            events[i].delta +
            '/134217728 max)'
        );
      }
      if (events[i].delta >>> 21) {
        destination[index++] = ((events[i].delta >>> 21) & 0x7f) | 0x80;
      }
      if (events[i].delta >>> 14) {
        destination[index++] = ((events[i].delta >>> 14) & 0x7f) | 0x80;
      }
      if (events[i].delta >>> 7) {
        destination[index++] = ((events[i].delta >>> 7) & 0x7f) | 0x80;
      }
      destination[index++] = events[i].delta & 0x7f;
      // MIDI Events encoding
      if (events[i].type === MIDIEvents.EVENT_MIDI) {
        // Adding the byte of subtype + channel
        destination[index++] = (events[i].subtype << 4) + events[i].channel;
        // Adding the byte of the first params
        destination[index++] = events[i].param1;
        // Adding a byte for the optionnal second param
        if (-1 !== MIDIEvents.MIDI_2PARAMS_EVENTS.indexOf(events[i].subtype)) {
          destination[index++] = events[i].param2;
        }
        // META / SYSEX events encoding
      } else {
        // Adding the event type byte
        destination[index++] = events[i].type;
        // Adding the META event subtype byte
        if (events[i].type === MIDIEvents.EVENT_META) {
          destination[index++] = events[i].subtype;
        }
        // Writing the event length bytes
        if (events[i].length >>> 28) {
          throw Error(
            'Event #' +
              i +
              ': Maximum length reached (' +
              events[i].length +
              '/134217728 max)'
          );
        }
        if (events[i].length >>> 21) {
          destination[index++] = ((events[i].length >>> 21) & 0x7f) | 0x80;
        }
        if (events[i].length >>> 14) {
          destination[index++] = ((events[i].length >>> 14) & 0x7f) | 0x80;
        }
        if (events[i].length >>> 7) {
          destination[index++] = ((events[i].length >>> 7) & 0x7f) | 0x80;
        }
        destination[index++] = events[i].length & 0x7f;
        if (events[i].type === MIDIEvents.EVENT_META) {
          switch (events[i].subtype) {
            case MIDIEvents.EVENT_META_SEQUENCE_NUMBER:
              destination[index++] = events[i].msb;
              destination[index++] = events[i].lsb;
              break;
            case MIDIEvents.EVENT_META_TEXT:
            case MIDIEvents.EVENT_META_COPYRIGHT_NOTICE:
            case MIDIEvents.EVENT_META_TRACK_NAME:
            case MIDIEvents.EVENT_META_INSTRUMENT_NAME:
            case MIDIEvents.EVENT_META_LYRICS:
            case MIDIEvents.EVENT_META_MARKER:
            case MIDIEvents.EVENT_META_CUE_POINT:
              for (k = 0, l = events[i].length; k < l; k++) {
                destination[index++] = events[i].data[k];
              }
              break;
            case MIDIEvents.EVENT_META_MIDI_CHANNEL_PREFIX:
              destination[index++] = events[i].prefix;
              break;
            case MIDIEvents.EVENT_META_END_OF_TRACK:
              break;
            case MIDIEvents.EVENT_META_SET_TEMPO:
              destination[index++] = events[i].tempo >> 16;
              destination[index++] = (events[i].tempo >> 8) & 0xff;
              destination[index++] = events[i].tempo & 0xff;
              break;
            case MIDIEvents.EVENT_META_SMTPE_OFFSET:
              if (strictMode && 23 < events[i].hour) {
                throw new Error(
                  'Event #' +
                    i +
                    ': SMTPE offset hour value must be' +
                    ' part of 0-23.'
                );
              }
              destination[index++] = events[i].hour;
              if (strictMode && 59 < events[i].minutes) {
                throw new Error(
                  'Event #' +
                    i +
                    ': SMTPE offset minutes value must' +
                    ' be part of 0-59.'
                );
              }
              destination[index++] = events[i].minutes;
              if (strictMode && 59 < events[i].seconds) {
                throw new Error(
                  'Event #' +
                    i +
                    ': SMTPE offset seconds value must' +
                    ' be part of 0-59.'
                );
              }
              destination[index++] = events[i].seconds;
              if (strictMode && 30 < events[i].frames) {
                throw new Error(
                  'Event #' +
                    i +
                    ': SMTPE offset frames amount must' +
                    ' be part of 0-30.'
                );
              }
              destination[index++] = events[i].frames;
              if (strictMode && 99 < events[i].subframes) {
                throw new Error(
                  'Event #' +
                    i +
                    ': SMTPE offset subframes amount' +
                    ' must be part of 0-99.'
                );
              }
              destination[index++] = events[i].subframes;
              break;
            case MIDIEvents.EVENT_META_KEY_SIGNATURE:
              if (
                'number' != typeof events[i].key ||
                -7 > events[i].key ||
                7 < events[i].scale
              ) {
                throw new Error(
                  'Event #' +
                    i +
                    ':The key signature key must be' +
                    ' between -7 and 7'
                );
              }
              if (
                'number' !== typeof events[i].scale ||
                0 > events[i].scale ||
                1 < events[i].scale
              ) {
                throw new Error(
                  'Event #' + i + ':' + 'The key signature scale must be 0 or 1'
                );
              }
              destination[index++] = events[i].key;
              destination[index++] = events[i].scale;
              break;
            // Not implemented
            case MIDIEvents.EVENT_META_TIME_SIGNATURE:
            case MIDIEvents.EVENT_META_SEQUENCER_SPECIFIC:
            default:
              for (k = 0, l = events[i].length; k < l; k++) {
                destination[index++] = events[i].data[k];
              }
              break;
          }
          // Adding bytes corresponding to the sysex event datas
        } else {
          for (k = 0, l = events[i].length; k < l; k++) {
            destination[index++] = events[i].data[k];
          }
        }
      }
    }
  }
  /**
   * Return the buffer length needed to encode the given events
   * @param  {[type]} events [description]
   * @return {[type]}        [description]
   */
  getRequiredBufferLength(events) {
    var bufferLength = 0;
    var i = 0;
    var j;

    // Calculating the track size by adding events lengths
    for (i = 0, j = events.length; i < j; i++) {
      // Computing necessary bytes to encode the delta value
      bufferLength +=
        events[i].delta >>> 21
          ? 4
          : events[i].delta >>> 14
            ? 3
            : events[i].delta >>> 7
              ? 2
              : 1;
      // MIDI Events have various fixed lengths
      if (events[i].type === MIDIEvents.EVENT_MIDI) {
        // Adding a byte for subtype + channel
        bufferLength++;
        // Adding a byte for the first params
        bufferLength++;
        // Adding a byte for the optionnal second param
        if (-1 !== MIDIEvents.MIDI_2PARAMS_EVENTS.indexOf(events[i].subtype)) {
          bufferLength++;
        }
        // META / SYSEX events lengths are self defined
      } else {
        // Adding a byte for the event type
        bufferLength++;
        // Adding a byte for META events subtype
        if (events[i].type === MIDIEvents.EVENT_META) {
          bufferLength++;
        }
        // Adding necessary bytes to encode the length
        bufferLength +=
          events[i].length >>> 21
            ? 4
            : events[i].length >>> 14
              ? 3
              : events[i].length >>> 7
                ? 2
                : 1;
        // Adding bytes corresponding to the event length
        bufferLength += events[i].length;
      }
    }
    return bufferLength;
  }
}
