import {EventEmitter} from "../node_modules/djipevents/dist/djipevents.esm.min.js";
import {WebMidi} from "./WebMidi.js";

/**
 * The `InputChannel` class represents a single input MIDI channel (1-16) from a single input
 * device. This object is derived from the host's MIDI subsystem and cannot be instantiated
 * directly.
 *
 * All 16 `InputChannel` objects can be found inside the input's [channels]{@link Input#channels}
 * property.
 *
 * The `InputChannel` class extends the
 * [EventEmitter](https://djipco.github.io/djipevents/EventEmitter.html) class from the
 * [djipevents]{@link https://djipco.github.io/djipevents/index.html} module. This means
 * it also includes methods such as
 * [addListener()](https://djipco.github.io/djipevents/EventEmitter.html#addListener),
 * [removeListener()](https://djipco.github.io/djipevents/EventEmitter.html#removeListener),
 * [hasListener()](https://djipco.github.io/djipevents/EventEmitter.html#hasListener) and several
 * others. Check out the
 * [documentation for EventEmitter](https://djipco.github.io/djipevents/EventEmitter.html) for more
 * details.
 *
 * @param {Input} input The `Input` this channel belongs to
 * @param {number} number The MIDI channel's number (1-16)
 *
 * @fires InputChannel#midimessage
 * @fires InputChannel#noteoff
 * @fires InputChannel#noteon
 * @fires InputChannel#keyaftertouch
 * @fires InputChannel#controlchange
 * @fires InputChannel#channelmode
 * @fires InputChannel#programchange
 * @fires InputChannel#channelaftertouch
 * @fires InputChannel#pitchbend
 *
 * @fires InputChannel#nrpn
 *
 * @fires InputChannel#allnotesoff
 * @fires InputChannel#allsoundoff
 * @fires InputChannel#localcontrol
 * @fires InputChannel#monomode
 * @fires InputChannel#omnimode
 * @fires InputChannel#resetallcontrollers
 *
 * @since 3.0.0
 */
export class InputChannel extends EventEmitter {

  constructor(input, number) {

    super();

    /**
     * The {@link Input} this channel belongs to
     * @type {Input}
     */
    this.input = input;

    /**
     * This channel's number (1-16)
     * @type {number}
     */
    this.number = number;

    /**
     * An array of the current NRPNs being constructed for this channel
     *
     * @private
     *
     * @type {string[]}
     */
    this._nrpnBuffer = [];

    // Enable NRPN events by default
    this.nrpnEventsEnabled = true;

  }

  destroy() {
    this.input = null;
    this.number = null;
    this._nrpnBuffer = null;
    this._nrpnEventsEnabled = false;
    this.removeListener();
  }

  /**
   * @param e Event
   * @protected
   */
  _parseEvent(e) {

    // Extract data bytes (unless it's a sysex message)
    let dataBytes = null;
    if (e.data[0] !== WebMidi.MIDI_SYSTEM_MESSAGES.sysex) dataBytes = e.data.slice(1);

    /**
     * Event emitted when a MIDI message of any kind is received by the `InputChannel`.
     *
     * @event InputChannel#midimessage
     * @type {Object}
     * @property {InputChannel} target The `InputChannel` that triggered the event.
     * @property {Array} event.data The MIDI message as an array of 8 bit values.
     * @property {Uint8Array} event.rawData The raw MIDI message as a Uint8Array.
     * @property {number} event.statusByte The message's status byte.
     * @property {?number[]} event.dataBytes The message's data bytes as an array of 0, 1 or 2
     * integers. This will be null for `sysex` messages.
     * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
     * milliseconds since the navigation start of the document).
     * @property {string} type `"midimessage"`
     */
    let midiMessageEvent = {
      target: this,
      data: Array.from(e.data),
      rawData: e.data,
      statusByte: e.data[0],
      dataBytes: dataBytes,
      timestamp: e.timeStamp,
      type: "midimessage"
    };

    this.emit("midimessage", midiMessageEvent);

    // Parse the event to see if its part of an NRPN sequence
    this._parseEventForNrpnMessage(e);

    // Parse the inbound event for regular messages
    this._parseEventForStandardMessages(e);

  }

  getStructuredMidiMessage(data) {

    return {
      command: data[0] >> 4,
      data1: data.length > 1 ? data[1] : undefined,
      data2: data.length > 2 ? data[2] : undefined
    };

  }

  /**
   * Parses channel events for standard (non-NRPN) events.
   * @param e Event
   * @private
   */
  _parseEventForStandardMessages(e) {

    let {command, data1, data2} = this.getStructuredMidiMessage(e.data);

    // Returned event
    let event = {
      target: this,
      data: Array.from(e.data),
      rawData: e.data,
      timestamp: e.timeStamp
    };

    if (
      command === WebMidi.MIDI_CHANNEL_VOICE_MESSAGES.noteoff ||
      (command === WebMidi.MIDI_CHANNEL_VOICE_MESSAGES.noteon && data2 === 0)
    ) {

      /**
       * Event emitted when a **note off** MIDI message has been received.
       *
       * @event InputChannel#noteoff
       * @type {Object}
       * @property {InputChannel} target The `InputChannel` that triggered the event.
       * @property {Array} event.data The MIDI message as an array of 8 bit values.
       * @property {Uint8Array} event.rawData The raw MIDI message as a `Uint8Array`.
       * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
       * milliseconds since the navigation start of the document).
       * @property {string} type `"noteoff"`
       * @property {Object} note A {@link Note} object containing information such as note number,
       * note name and octave.
       * @property {number} release The release velocity expressed as a float between 0 and 1.
       * @property {number} rawRelease The release velocity expressed as an integer (between 0 and
       * 127).
       */
      event.type = "noteoff";
      event.note = new Note(data1, {rawRelease: data2});
      event.release = event.note.release;
      event.rawRelease = event.note.rawRelease;

    } else if (command === WebMidi.MIDI_CHANNEL_VOICE_MESSAGES.noteon) {

      /**
       * Event emitted when a **note on** MIDI message has been received.
       *
       * @event InputChannel#noteon
       * @type {Object}
       * @property {InputChannel} target The `InputChannel` that triggered the event.
       * @property {Array} event.data The MIDI message as an array of 8 bit values.
       * @property {Uint8Array} event.rawData The raw MIDI message as a Uint8Array.
       * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
       * milliseconds since the navigation start of the document).
       * @property {string} type `"noteon"`
       * @property {Object} note A {@link Note} object containing information such as note number,
       * note name and octave.
       * @property {number} attack The attack velocity expressed as a float between 0 and 1.
       * @property {number} rawAttack The attack velocity expressed as an integer (between 0 and
       * 127).
       */
      event.type = "noteon";
      event.note = new Note(data1, {rawAttack: data2});
      event.attack = event.note.attack;
      event.rawAttack = event.note.rawAttack;

    } else if (command === WebMidi.MIDI_CHANNEL_VOICE_MESSAGES.keyaftertouch) {

      /**
       * Event emitted when a **key-specific aftertouch** MIDI message has been received.
       *
       * @event InputChannel#keyaftertouch
       * @type {Object}
       * @property {InputChannel} target The `InputChannel` that triggered the event.
       * @property {Array} event.data The MIDI message as an array of 8 bit values.
       * @property {Uint8Array} event.rawData The raw MIDI message as a `Uint8Array`.
       * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
       * milliseconds since the navigation start of the document).
       * @property {string} type `"keyaftertouch"`
       * @property {Object} note A {@link Note} object containing information such as note number,
       * note name and octave.
       * @property {number} value The aftertouch amount expressed as a float between 0 and 1.
       * @property {number} rawValue The aftertouch amount expressed as an integer (between 0 and
       * 127).
       */
      event.type = "keyaftertouch";
      event.note = new Note(data1);
      event.value = data2 / 127;
      event.rawValue = data2;

    } else if (
      command === WebMidi.MIDI_CHANNEL_VOICE_MESSAGES.controlchange &&
      data1 >= 0 && data1 <= 119
    ) {

      /**
       * Event emitted when a **control change** MIDI message has been received.
       *
       * @event InputChannel#controlchange
       * @type {Object}
       * @property {InputChannel} target The `InputChannel` that triggered the event.
       * @property {Array} event.data The MIDI message as an array of 8 bit values.
       * @property {Uint8Array} event.rawData The raw MIDI message as a Uint8Array.
       * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
       * milliseconds since the navigation start of the document).
       * @property {string} type `"controlchange"`
       * @property {Object} controller
       * @property {Object} controller.number The number of the controller.
       * @property {Object} controller.name The usual name or function of the controller.
       * @property {number} value The value expressed as a float between 0 and 1.
       * @property {number} rawValue The value expressed as an integer (between 0 and 127).
       */
      event.type = "controlchange";
      event.controller = {
        number: data1,
        name: this.getCcNameByNumber(data1)
      };
      event.value = data2 / 127;
      event.rawValue = data2;

    } else if (
      command === WebMidi.MIDI_CHANNEL_VOICE_MESSAGES.channelmode &&
      data1 >= 120 && data1 <= 127
    ) {

      /**
       * Event emitted when a **channel mode** MIDI message has been received.
       *
       * @event InputChannel#channelmode
       * @type {Object}
       * @property {InputChannel} target The `InputChannel` that triggered the event.
       * @property {Array} event.data The MIDI message as an array of 8 bit values.
       * @property {Uint8Array} event.rawData The raw MIDI message as a Uint8Array.
       * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
       * milliseconds since the navigation start of the document).
       * @property {string} type `"channelmode"`
       * @property {Object} controller
       * @property {Object} controller.number The number of the controller.
       * @property {Object} controller.name The usual name or function of the controller.
       * @property {number} value The value expressed as a float between 0 and 1.
       * @property {number} rawValue The value expressed as an integer (between 0 and 127).
       */
      event.type = "channelmode";
      event.controller = {
        number: data1,
        name: this.getChannelModeByNumber(data1)
      };
      event.value = data2;

      // Also dispatch specific channel mode events
      this._parseChannelModeMessage(e);

    } else if (command === WebMidi.MIDI_CHANNEL_VOICE_MESSAGES.programchange) {

      /**
       * Event emitted when a **program change** MIDI message has been received.
       *
       * @event InputChannel#programchange
       * @type {Object}
       * @property {InputChannel} target The `InputChannel` that triggered the event.
       * @property {Array} event.data The MIDI message as an array of 8 bit values.
       * @property {Uint8Array} event.rawData The raw MIDI message as a Uint8Array.
       * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
       * milliseconds since the navigation start of the document).
       * @property {string} type `"programchange"`
       * @property {number} value The value expressed as an integer between 1 and 128.
       * @property {number} rawValue The value expressed as an integer between 0 and 127.
       */
      event.type = "programchange";
      event.value = data1 + 1;
      event.rawValue = data1;

    } else if (command === WebMidi.MIDI_CHANNEL_VOICE_MESSAGES.channelaftertouch) {

      /**
       * Event emitted when a control change MIDI message has been received.
       *
       * @event InputChannel#channelaftertouch
       * @type {Object}
       * @property {InputChannel} target The `InputChannel` that triggered the event.
       * @property {Array} event.data The MIDI message as an array of 8 bit values.
       * @property {Uint8Array} event.rawData The raw MIDI message as a Uint8Array.
       * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
       * milliseconds since the navigation start of the document).
       * @property {string} type `"channelaftertouch"`
       * @property {number} value The value expressed as a float between 0 and 1.
       * @property {number} rawValue The value expressed as an integer (between 0 and 127).
       */
      event.type = "channelaftertouch";
      event.value = data1 / 127;
      event.rawValue = data1;

    } else if (command === WebMidi.MIDI_CHANNEL_VOICE_MESSAGES.pitchbend) {

      /**
       * Event emitted when a pitch bend MIDI message has been received.
       *
       * @event InputChannel#pitchbend
       * @type {Object}
       * @property {InputChannel} target The `InputChannel` that triggered the event.
       * @property {Array} event.data The MIDI message as an array of 8 bit values.
       * @property {Uint8Array} event.rawData The raw MIDI message as a Uint8Array.
       * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
       * milliseconds since the navigation start of the document).
       * @property {string} type `"pitchbend"`
       * @property {number} value The value expressed as a float between 0 and 1.
       * @property {number} rawValue The value expressed as an integer (between 0 and 16383).
       */
      event.type = "pitchbend";
      event.value = ((data2 << 7) + data1 - 8192) / 8192;
      event.rawValue = (data2 << 7) + data1;

    } else {
      event.type = "unknownmessage";
    }

    this.emit(event.type, event);

  }

  /**
   * Returns the channel mode name matching the specified number. If no match is found, the function
   * returns `false`.
   *
   * @param {number} number An integer representing the channel mode message.
   * @returns {string|false} The name of the matching channel mode or `false` if not match could be
   * found.
   *
   * @since 2.0.0
   */
  getChannelModeByNumber(number) {

    if (WebMidi.validation) {
      number = Math.floor(number);
    }

    if ( !(number >= 120 && number <= 127) ) return false;

    for (let cm in WebMidi.MIDI_CHANNEL_MODE_MESSAGES) {

      if (
        WebMidi.MIDI_CHANNEL_MODE_MESSAGES.hasOwnProperty(cm) &&
        number === WebMidi.MIDI_CHANNEL_MODE_MESSAGES[cm]
      ) {
        return cm;
      }

    }

  }

  _parseChannelModeMessage(e) {

    let data1, data2;

    if (e.data.length > 1) {
      data1 = e.data[1];
      data2 = e.data.length > 2 ? e.data[2] : undefined;
    }

    // Basis for the returned event
    let event = {
      target: this,
      data: Array.from(e.data),
      rawData: e.data,
      timestamp: e.timeStamp,
      type: this.getChannelModeByNumber(data1)
    };

    /**
     * Event emitted when an "all sound off" channel-mode MIDI message has been received.
     *
     * @event InputChannel#allsoundoff
     * @type {Object}
     * @property {InputChannel} target The `InputChannel` that triggered the event.
     * @property {Array} event.data The MIDI message as an array of 8 bit values.
     * @property {Uint8Array} event.rawData The raw MIDI message as a Uint8Array.
     * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
     * milliseconds since the navigation start of the document).
     * @property {string} type `"allsoundoff"`
     */

    /**
     * Event emitted when a "reset all controllers" channel-mode MIDI message has been received.
     *
     * @event InputChannel#resetallcontrollers
     * @type {Object}
     * @property {InputChannel} target The `InputChannel` that triggered the event.
     * @property {Array} event.data The MIDI message as an array of 8 bit values.
     * @property {Uint8Array} event.rawData The raw MIDI message as a Uint8Array.
     * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
     * milliseconds since the navigation start of the document).
     * @property {string} type `"resetallcontrollers"`
     */

    /**
     * Event emitted when a "local control" channel-mode MIDI message has been received. The value
     * property of the event is set to either `true` (local control on) of `false` (local control
     * off).
     *
     * @event InputChannel#localcontrol
     * @type {Object}
     * @property {InputChannel} target The `InputChannel` that triggered the event.
     * @property {Array} event.data The MIDI message as an array of 8 bit values.
     * @property {Uint8Array} event.rawData The raw MIDI message as a Uint8Array.
     * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
     * milliseconds since the navigation start of the document).
     * @property {string} type `"localcontrol"`
     * @property {boolean} value For local control on, the value is `true`. For local control off,
     * the value is `false`.
     */
    if (event.type === "localcontrol") {
      event.value = data2 === 127 ? true : false;
    }

    /**
     * Event emitted when an "all notes off" channel-mode MIDI message has been received.
     *
     * @event InputChannel#allnotesoff
     * @type {Object}
     * @property {InputChannel} target The `InputChannel` that triggered the event.
     * @property {Array} event.data The MIDI message as an array of 8 bit values.
     * @property {Uint8Array} event.rawData The raw MIDI message as a Uint8Array.
     * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
     * milliseconds since the navigation start of the document).
     * @property {string} type `"allnotesoff"`
     */

    /**
     * Event emitted when an "omni mode" channel-mode MIDI message has been received. The value
     * property of the event is set to either `true` (omni mode on) of `false` (omni mode off).
     *
     * @event InputChannel#omnimode
     * @type {Object}
     * @property {InputChannel} target The `InputChannel` that triggered the event.
     * @property {Array} event.data The MIDI message as an array of 8 bit values.
     * @property {Uint8Array} event.rawData The raw MIDI message as a Uint8Array.
     * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
     * milliseconds since the navigation start of the document).
     * @property {string} type `"omnimode"`
     * @property {boolean} value The value is `true` for omni mode on and false for omni mode off.
     */
    if (event.type === "omnimodeon") {
      event.type = "omnimode";
      event.value = true;
    } else if (event.type === "omnimodeoff") {
      event.type = "omnimode";
      event.value = false;
    }

    /**
     * Event emitted when a "mono/poly mode" MIDI message has been received. The value property of
     * the event is set to either `true` (mono mode on / poly mode off) or `false` (mono mode off /
     * poly mode on).
     *
     * @event InputChannel#monomode
     * @type {Object}
     * @property {InputChannel} target The `InputChannel` that triggered the event.
     * @property {Array} event.data The MIDI message as an array of 8 bit values.
     * @property {Uint8Array} event.rawData The raw MIDI message as a Uint8Array.
     * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
     * milliseconds since the navigation start of the document).
     * @property {string} type `"monomode"`
     * @property {boolean} value The value is `true` for omni mode on and false for omni mode off.
     */
    if (event.type === "monomodeon") {
      event.type = "monomode";
      event.value = true;
    } else if (event.type === "polymodeon") {
      event.type = "monomode";
      event.value = false;
    }

    this.emit(event.type, event);

  }

  /**
   * Parses channel events and constructs NRPN message parts in valid sequences.
   * Keeps a separate NRPN buffer for each channel.
   * Emits an event after it receives the final CC parts msb 127 lsb 127.
   * If a message is incomplete and other messages are received before
   * the final 127 bytes, the incomplete message is cleared.
   * @param e Event
   * @private
   */
  _parseEventForNrpnMessage(e) {

    if (!this.nrpnEventsEnabled) return;

    // Extract basic data
    let command = e.data[0] >> 4;
    let channel = (e.data[0] & 0xf) + 1;
    let data1;
    let data2;

    if (e.data.length > 1) {
      data1 = e.data[1];
      data2 = e.data.length > 2 ? e.data[2] : undefined;
    }

    // Message not valid for NRPN
    if (
      !(
        command === WebMidi.MIDI_CHANNEL_VOICE_MESSAGES.controlchange &&
        (
          (
            data1 >= WebMidi.MIDI_NRPN_MESSAGES.increment &&
            data1 <= WebMidi.MIDI_NRPN_MESSAGES.parammsb
          ) ||
          data1 === WebMidi.MIDI_NRPN_MESSAGES.entrymsb ||
          data1 === WebMidi.MIDI_NRPN_MESSAGES.entrylsb
        )
      )
    ) {
      return;
    }

    // set up a CC event to parse as NRPN part
    let ccEvent = {
      target: this,
      type: "controlchange",
      data: Array.from(e.data),
      rawData: e.data,
      timestamp: e.timeStamp,
      channel: channel,
      controller: {
        number: data1,
        name: this.getCcNameByNumber(data1)
      },
      value: data2
    };

    if (
      // if we get a starting MSB (CC99 - 0-126) vs an end MSB (CC99 - 127), destroy incomplete NRPN
      // and begin building again
      ccEvent.controller.number === WebMidi.MIDI_NRPN_MESSAGES.parammsb &&
      ccEvent.value != WebMidi.MIDI_NRPN_MESSAGES.nullactiveparameter
    ) {
      this._nrpnBuffer = [];
      this._nrpnBuffer[0] = ccEvent;
    } else if(
      // add the param LSB
      this._nrpnBuffer.length === 1 &&
      ccEvent.controller.number === WebMidi.MIDI_NRPN_MESSAGES.paramlsb
    ) {
      this._nrpnBuffer.push(ccEvent);

    } else if(
      // add data inc/dec or value MSB for 14bit
      this._nrpnBuffer.length === 2 &&
      (ccEvent.controller.number === WebMidi.MIDI_NRPN_MESSAGES.increment ||
        ccEvent.controller.number === WebMidi.MIDI_NRPN_MESSAGES.decrement ||
        ccEvent.controller.number === WebMidi.MIDI_NRPN_MESSAGES.entrymsb)
    ) {
      this._nrpnBuffer.push(ccEvent);
    } else if(
      // if we have a value MSB, only add an LSB to pair with that
      this._nrpnBuffer.length === 3 &&
      this._nrpnBuffer[2].number === WebMidi.MIDI_NRPN_MESSAGES.entrymsb &&
      ccEvent.controller.number === WebMidi.MIDI_NRPN_MESSAGES.entrylsb
    ) {
      this._nrpnBuffer.push(ccEvent);

    } else if(
      // add an end MSB (CC99 - 127)
      this._nrpnBuffer.length >= 3 &&
      this._nrpnBuffer.length <= 4 &&
      ccEvent.controller.number === WebMidi.MIDI_NRPN_MESSAGES.parammsb &&
      ccEvent.value === WebMidi.MIDI_NRPN_MESSAGES.nullactiveparameter
    ) {
      this._nrpnBuffer.push(ccEvent);
    } else if(
      // add an end LSB (CC99 - 127)
      this._nrpnBuffer.length >= 4 &&
      this._nrpnBuffer.length <= 5 &&
      ccEvent.controller.number === WebMidi.MIDI_NRPN_MESSAGES.paramlsb &&
      ccEvent.value === WebMidi.MIDI_NRPN_MESSAGES.nullactiveparameter
    ) {
      this._nrpnBuffer.push(ccEvent);
      // now we have a full inc or dec NRPN message, lets create that event!

      let rawData = [];

      this._nrpnBuffer.forEach(ev => rawData.push(ev.data));

      let nrpnNumber = (this._nrpnBuffer[0].value<<7) | (this._nrpnBuffer[1].value);
      let nrpnValue = this._nrpnBuffer[2].value;
      if (this._nrpnBuffer.length === 6) {
        nrpnValue = (this._nrpnBuffer[2].value<<7) | (this._nrpnBuffer[3].value);
      }

      let nrpnControllerType = "";

      switch (this._nrpnBuffer[2].controller.number) {
      case WebMidi.MIDI_NRPN_MESSAGES.entrymsb:
        nrpnControllerType = InputChannel.NRPN_TYPES[0];
        break;
      case WebMidi.MIDI_NRPN_MESSAGES.increment:
        nrpnControllerType = InputChannel.NRPN_TYPES[1];
        break;
      case WebMidi.MIDI_NRPN_MESSAGES.decrement:
        nrpnControllerType = InputChannel.NRPN_TYPES[2];
        break;
      default:
        throw new Error("The NPRN type was unidentifiable.");
      }

      // now we are done building an NRPN, so clear the NRPN buffer
      this._nrpnBuffer = [];

      /**
       * Event emitted when a valid NRPN message sequence has been received.
       *
       * @event InputChannel#nrpn
       * @type {Object}
       * @property {InputChannel} target The `InputChannel` that triggered the event.
       * @property {Array} event.data The MIDI message as an array of 8 bit values.
       * @property {Uint8Array} event.rawData The raw MIDI message as a Uint8Array.
       * @property {number} timestamp The moment (DOMHighResTimeStamp) when the event occurred (in
       * milliseconds since the navigation start of the document).
       * @property {string} type `"nrpn"`
       * @property {Object} controller
       * @property {Object} controller.number The number of the NRPN.
       * @property {Object} controller.name The usual name or function of the controller.
       * @property {number} value The aftertouch amount expressed as a float between 0 and 1.
       * @property {number} rawValue The aftertouch amount expressed as an integer (between 0 and
       * 65535).
       */
      let nrpnEvent = {
        timestamp: ccEvent.timestamp,
        channel: ccEvent.channel,
        type: "nrpn",
        data: Array.from(rawData),
        rawData: rawData,
        controller: {
          number: nrpnNumber,
          type: nrpnControllerType,
          name: "Non-Registered Parameter " + nrpnNumber
        },
        value: nrpnValue / 65535,
        rawValue: nrpnValue
      };

      this.emit(nrpnEvent.type, nrpnEvent);

    } else {
      // something didn't match, clear the incomplete NRPN message buffer
      this._nrpnBuffer = [];
    }
  }

  /**
   * Returns the name of a control change message matching the specified number. Some valid control
   * change numbers do not have a specific name or purpose assigned in the MIDI
   * [spec](https://midi.org/specifications-old/item/table-3-control-change-messages-data-bytes-2).
   * In this case, the method returns `false`.
   *
   * @param {number} number An integer representing the control change message
   * @returns {string|false} The matching control change name or `false` if not match was found
   *
   * @throws {RangeError} Invalid control change number.
   *
   * @since 2.0.0
   */
  getCcNameByNumber(number) {

    if (WebMidi.validation) {
      number = parseInt(number);
      if ( !(number >= 0 && number <= 119) ) {
        throw new RangeError("Invalid control change number.");
      }
    }

    for (let cc in WebMidi.MIDI_CONTROL_CHANGE_MESSAGES) {

      if (
        WebMidi.MIDI_CONTROL_CHANGE_MESSAGES.hasOwnProperty(cc) &&
        number === WebMidi.MIDI_CONTROL_CHANGE_MESSAGES[cc]
      ) {
        return cc;
      }

    }

    return false;

  }


  /**
   * Indicates whether events for **Non-Registered Parameter Number** should be dispatched. NRPNs
   * are composed of a sequence of specific **control change** messages. When a valid sequence of
   * such control change messages is received, an `nrpn` event will fire. If an invalid or out of
   * order control change message is received, it will fall through the collector logic and all
   * buffered control change messages will be discarded as incomplete.
   *
   * @type Boolean
   */
  get nrpnEventsEnabled() {
    return this._nrpnEventsEnabled;
  }
  set nrpnEventsEnabled(enabled) {
    this._nrpnEventsEnabled = !!enabled;
  }

  /**
   * Array of valid **non-registered parameter number** (NRPNs) types.
   *
   * @type {string[]}
   * @readonly
   */
  static get NRPN_TYPES() {
    return ["entry", "increment", "decrement"];
  }

}