/**
 * Description: Read a MIDI file in a given ArrayBuffer
 *
 * @package     midi-Synth
 * @category    class js
 * @author      https://github.com/nfroidure/midifile/tree/master/src
 * @copyright
 * @license     GPL v2 or later
 */

class MIDIFile {
  /**
   * Read (and soon edit) a MIDI file in a given ArrayBuffer
   * @param {arrayBuffer} buffer     [description]
   * @param {boolean}     strictMode [description]
   */
  constructor (buffer, strictMode) {
    this.MIDIEvents = new MIDIEvents();
    var track;
    var curIndex;
    var i;
    var j;

    // If not buffer given, creating a new MIDI file
    if (!buffer) {
      // Creating the content
      this.header = new MIDIFileHeader();
      this.tracks = [new MIDIFileTrack()];
      // if a buffer is provided, parsing him
    } else {
      buffer = this.ensureArrayBuffer(buffer);
      // Minimum MIDI file size is a headerChunk size (14bytes)
      // and an empty track (8+3bytes)
      if (25 > buffer.byteLength) {
        throw new Error(
          'A buffer of a valid MIDI file must have, at least, a' +
            ' size of 25bytes.'
        );
      }
      // Reading header
      this.header = new MIDIFileHeader(buffer, strictMode);
      this.tracks = [];
      curIndex = MIDIFileHeader.HEADER_LENGTH;
      // Reading tracks
      for (i = 0, j = this.header.getTracksCount(); i < j; i++) {
        // Testing the buffer length
        if (strictMode && curIndex >= buffer.byteLength - 1) {
          throw new Error(
            "Couldn't find datas corresponding to the track #" + i + '.'
          );
        }
        // Creating the track object
        track = new MIDIFileTrack(buffer, curIndex, strictMode);
        this.tracks.push(track);
        // Updating index to the track end
        curIndex += track.getTrackLength() + 8;
      }
      // Testing integrity : curIndex should be at the end of the buffer
      if (strictMode && curIndex !== buffer.byteLength) {
        throw new Error('It seems that the buffer contains too much datas.');
      }
    }
  }
  /**
   * [ensureArrayBuffer description]
   * @param  {[type]} buf [description]
   * @return {[type]}     [description]
   */
  ensureArrayBuffer(buf) {
    if (buf) {
      if (buf instanceof ArrayBuffer) {
        return buf;
      }
      if (buf instanceof Uint8Array) {
        // Copy/convert to standard Uint8Array, because derived classes like
        // node.js Buffers might have unexpected data in the .buffer property.
        return new Uint8Array(buf).buffer;
      }
    }
    throw new Error('Unsupported buffer type, need ArrayBuffer or Uint8Array');
  }
  /**
   * Remove a track
   * @param  {[type]} index [description]
   * @return {[type]}       [description]
   */
  delTrack(index) {
    if (index > this.tracks.length || 0 > index) {
      throw Error('Invalid track index (' + index + ')');
    }
    this.tracks.splice(index, 1);
    this.header.setTracksCount(this.tracks.length);
  }

  /**
   * Add a track
   * @param {[type]} index [description]
   */
  addTrack(index) {
    var track;

    if (index > this.tracks.length || 0 > index) {
      throw Error('Invalid track index (' + index + ')');
    }
    track = new MIDIFileTrack();
    if (index === this.tracks.length) {
      this.tracks.push(track);
    } else {
      this.tracks.splice(index, 0, track);
    }
    this.header.setTracksCount(this.tracks.length);
  }
  /**
   * Events reading helpers
   * @param  {[type]} type    [description]
   * @param  {[type]} subtype [description]
   * @return {[type]}         [description]
   */
  getEvents(type, subtype) {
    var events;
    var event;
    var playTime = 0;
    var filteredEvents = [];
    var format = this.header.getFormat();
    var tickResolution = this.header.getTickResolution();
    var trackParsers;
    var smallestDelta;
    var i;
    var j;

    // Reading events
    // if the read is sequential
    if (1 !== format || 1 === this.tracks.length) {
      for (i = 0, j = this.tracks.length; i < j; i++) {
        // reset playtime if format is 2
        playTime = 2 === format && playTime ? playTime : 0;
        events = this.MIDIEvents.createParser(
          this.tracks[i].getTrackContent(),
          0,
          false
        );
        // loooping through events
        event = events.next();
        while (event) {
          playTime += event.delta ? event.delta * tickResolution / 1000 : 0;
          if (event.type === MIDIEvents.EVENT_META) {
            // tempo change events
            if (event.subtype === MIDIEvents.EVENT_META_SET_TEMPO) {
              tickResolution = this.header.getTickResolution(event.tempo);
            }
          }
          // push the asked events
          if (
            (!type || event.type === type) &&
            (!subtype || (event.subtype && event.subtype === subtype))
          ) {
            event.playTime = playTime;
            filteredEvents.push(event);
          }
          event = events.next();
        }
      }
      // the read is concurrent
    } else {
      trackParsers = [];
      smallestDelta = -1;

      // Creating parsers
      for (i = 0, j = this.tracks.length; i < j; i++) {
        trackParsers[i] = {};
        trackParsers[i].parser = this.MIDIEvents.createParser(
          this.tracks[i].getTrackContent(),
          0,
          false
        );
        trackParsers[i].curEvent = trackParsers[i].parser.next();
      }
      // Filling events
      do {
        smallestDelta = -1;
        // finding the smallest event
        for (i = 0, j = trackParsers.length; i < j; i++) {
          if (trackParsers[i].curEvent) {
            if (
              -1 === smallestDelta ||
              trackParsers[i].curEvent.delta <
                trackParsers[smallestDelta].curEvent.delta
            ) {
              smallestDelta = i;
            }
          }
        }
        if (-1 !== smallestDelta) {
          // removing the delta of previous events
          for (i = 0, j = trackParsers.length; i < j; i++) {
            if (i !== smallestDelta && trackParsers[i].curEvent) {
              trackParsers[i].curEvent.delta -=
                trackParsers[smallestDelta].curEvent.delta;
            }
          }
          // filling values
          event = trackParsers[smallestDelta].curEvent;
          playTime += event.delta ? event.delta * tickResolution / 1000 : 0;
          if (event.type === MIDIEvents.EVENT_META) {
            // tempo change events
            if (event.subtype === MIDIEvents.EVENT_META_SET_TEMPO) {
              tickResolution = this.header.getTickResolution(event.tempo);
            }
          }
          // push midi events
          if (
            (!type || event.type === type) &&
            (!subtype || (event.subtype && event.subtype === subtype))
          ) {
            event.playTime = playTime;
            event.track = smallestDelta;
            filteredEvents.push(event);
          }
          // getting next event
          trackParsers[smallestDelta].curEvent = trackParsers[
            smallestDelta
          ].parser.next();
        }
      } while (-1 !== smallestDelta);
    }
    return filteredEvents;
  }
  /**
   * [getMidiEvents description]
   * @return {[type]} [description]
   */
  getMidiEvents() {
    return this.getEvents(MIDIEvents.EVENT_MIDI);
  }
  /**
   * [getLyrics description]
   * @return {[type]} [description]
   */
  getLyrics() {
    var events = this.getEvents(MIDIEvents.EVENT_META);
    var texts = [];
    var lyrics = [];
    var event;
    var i;
    var j;

    for (i = 0, j = events.length; i < j; i++) {
      event = events[i];
      // Lyrics
      if (event.subtype === MIDIEvents.EVENT_META_LYRICS) {
        lyrics.push(event);
        // Texts
      } else if (event.subtype === MIDIEvents.EVENT_META_TEXT) {
        // Ignore special texts
        if ('@' === String.fromCharCode(event.data[0])) {
          if ('T' === String.fromCharCode(event.data[1])) {
            // console.log('Title : ' + event.text.substring(2));
          } else if ('I' === String.fromCharCode(event.data[1])) {
            // console.log('Info : ' + event.text.substring(2));
          } else if ('L' === String.fromCharCode(event.data[1])) {
            // console.log('Lang : ' + event.text.substring(2));
          }
          // karaoke text follows, remove all previous text
        } else if (
          0 === String.fromCharCode.apply(String, event.data).indexOf('words')
        ) {
          texts.length = 0;
          // console.log('Word marker found');
          // Karaoke texts
          // If playtime is greater than 0
        } else if (0 !== event.playTime) {
          texts.push(event);
        }
      }
    }
    // Choosing the right lyrics
    if (2 < lyrics.length) {
      texts = lyrics;
    } else if (!texts.length) {
      texts = [];
    }
    // Convert texts and detect encoding
    try {
      texts.forEach(function(event) {
        event.text = UTF8.getStringFromBytes(event.data, 0, event.length, true);
      });
    } catch (e) {
      texts.forEach(function(event) {
        event.text = event.data
          .map(function(c) {
            return String.fromCharCode(c);
          })
          .join('');
      });
    }
    return texts;
  }
  /**
   * Basic events reading
   * @param  {[type]} index [description]
   * @return {[type]}       [description]
   */
  getTrackEvents(index) {
    var event;
    var events = [];
    var parser;
    if (index > this.tracks.length || 0 > index) {
      throw Error('Invalid track index (' + index + ')');
    }
    parser = this.MIDIEvents.createParser(
      this.tracks[index].getTrackContent(),
      0,
      false
    );
    event = parser.next();
    do {
      events.push(event);
      event = parser.next();
    } while (event);
    return events;
  }
  /**
   * Retrieve the content in a buffer
   * @return {[type]} [description]
   */
  getContent() {
    var bufferLength;
    var destination;
    var origin;
    var i;
    var j;
    var k;
    var l;
    var m;
    var n;

    // Calculating the buffer content
    // - initialize with the header length
    bufferLength = MIDIFileHeader.HEADER_LENGTH;
    // - add tracks length
    for (i = 0, j = this.tracks.length; i < j; i++) {
      bufferLength += this.tracks[i].getTrackLength() + 8;
    }
    // Creating the destination buffer
    destination = new Uint8Array(bufferLength);
    // Adding header
    origin = new Uint8Array(
      this.header.datas.buffer,
      this.header.datas.byteOffset,
      MIDIFileHeader.HEADER_LENGTH
    );
    for (i = 0, j = MIDIFileHeader.HEADER_LENGTH; i < j; i++) {
      destination[i] = origin[i];
    }
    // Adding tracks
    for (k = 0, l = this.tracks.length; k < l; k++) {
      origin = new Uint8Array(
        this.tracks[k].datas.buffer,
        this.tracks[k].datas.byteOffset,
        this.tracks[k].datas.byteLength
      );
      for (m = 0, n = this.tracks[k].datas.byteLength; m < n; m++) {
        destination[i++] = origin[m];
      }
    }
    return destination.buffer;
  }
  /**
   * Basic events writting
   * @param {[type]} index  [description]
   * @param {[type]} events [description]
   */
  setTrackEvents(index, events) {
    var bufferLength;
    var destination;

    if (index > this.tracks.length || 0 > index) {
      throw Error('Invalid track index (' + index + ')');
    }
    if (!events || !events.length) {
      throw Error('A track must contain at least one event, none given.');
    }
    bufferLength = this.MIDIEvents.getRequiredBufferLength(events);
    destination = new Uint8Array(bufferLength);
    this.MIDIEvents.writeToTrack(events, destination);
    this.tracks[index].setTrackContent(destination);
  }
}
