/*
 *  VERSIONS LOG
 *
 *  0.1   - display of noteOn, noteOff, and ControlChange MIDI messages
 *
 *  0.2   - generation of the note CV (DAC0)
 *        - generation of the velocity CV (DAC1)
 *        - generation of the GATE signal (D24)
 *
 *  0.3   - push buttons handling for future features
 *
 *  0.4   - changes to NoteOn and NoteOff handling to keep into account
 *          the key priority: lower key has precedence to high key
 *          (monophonic synth pitch priority)
 *        - generation of the trigger signal (D26)
 *
 * 	0.5   - fix key priority in handleNoteOn(); previously, priority was
 * 			    given to higher pitch note, now it is given to lower pitch
 * 			    note.
 *
 *  0.6   - Add the capability of generating analog output (PWM) for
 *          8 control change channels/devices.
 *        - Minor cosmetic changes
 *
 * 	1.0	  - Added pin mode initialization on PWM pins
 * 		    - changed PWM8 pin fro 9 to 11, because of clock interaction
 * 			    issues (pins 9 and 10 cannot be used)
 * 		    - Name change for first official release
 */


#include <UHS2-MIDI.h>
#include <LiquidCrystal.h>
#include <AnalogButtons.h>

#define VERSION_NUMBER  "1.0"

#define DEBUG 1     // serial monitor active
//#define DEBUG 0   // serial monitor disabled

////////////////////////////////////////////////////
// Push Buttons Handling
////////////////////////////////////////////////////
// constant definitions
#define PB_PIN    A11
#define PB_MARGIN 35
#define PB_UP     354
#define PB_DOWN   566
#define PB_LEFT   692
#define PB_RIGHT  853
#define PB_ENTER  932
// the buttons object
AnalogButtons push_buttons(PB_PIN, INPUT);
// button handlers declaration
void pb_up_handler();
void pb_down_handler();
void pb_left_handler();
void pb_right_handler();
void pb_enter_handler();
// buttons list
Button pb_up = Button(PB_UP, pb_up_handler);
Button pb_down = Button(PB_DOWN, pb_down_handler);
Button pb_left = Button(PB_LEFT, pb_left_handler);
Button pb_right = Button(PB_RIGHT, pb_right_handler);
Button pb_enter = Button(PB_ENTER, pb_enter_handler);

// Conversion table
// MIDI Note number -> 12 bit DAC input value
const int MIDI2DAC[]
{
  0x0000, 0x0020, 0x0040, 0x0060, 0x0080, 0x00A1, 0x00C1, 0x00E1, 0x0101, 0x0122, 0x0142, 0x0162,  // octave -1
  0x0182, 0x01A3, 0x01C3, 0x01E3, 0x0203, 0x0224, 0x0244, 0x0264, 0x0284, 0x02A5, 0x02C5, 0x02E5,  // octave 0
  0x0305, 0x0326, 0x0346, 0x0366, 0x0386, 0x03A7, 0x03C7, 0x03E7, 0x0407, 0x0428, 0x0448, 0x0468,  // octave 1
  0x0488, 0x04A9, 0x04C9, 0x04E9, 0x0509, 0x052A, 0x054A, 0x056A, 0x058A, 0x05AA, 0x05CB, 0x05EB,  // octave 2
  0x060B, 0x062B, 0x064C, 0x066C, 0x068C, 0x06AC, 0x06CD, 0x06ED, 0x070D, 0x072D, 0x074E, 0x076E,  // octave 3
  0x078E, 0x07AE, 0x07CF, 0x07EF, 0x080F, 0x082F, 0x0850, 0x0870, 0x0890, 0x08B0, 0x08D1, 0x08F1,  // octave 4
  0x0911, 0x0931, 0x0952, 0x0972, 0x0992, 0x09B2, 0x09D3, 0x09F3, 0x0A13, 0x0A33, 0x0A54, 0x0A74,  // octave 5
  0x0A94, 0x0AB4, 0x0AD4, 0x0AF5, 0x0B15, 0x0B35, 0x0B55, 0x0B76, 0x0B96, 0x0BB6, 0x0BD6, 0x0BF7,  // octave 6
  0x0C17, 0x0C37, 0x0C57, 0x0C78, 0x0C98, 0x0CB8, 0x0CD8, 0x0CF9, 0x0D19, 0x0D39, 0x0D59, 0x0D7A,  // octave 7
  0x0D9A, 0x0DBA, 0x0DDA, 0x0DFB, 0x0E1B, 0x0E3B, 0x0E5B, 0x0E7C, 0x0E9C, 0x0EBC, 0x0EDC, 0x0EFD,  // octave 8
  0x0F1D, 0x0F3D, 0x0F5D, 0x0F7E, 0x0F9E, 0x0FBE, 0x0FDE, 0x0FFF                                   // octave 9
};



// Define Usb object, able to read on all channels
USB Usb;
UHS2MIDI_CREATE_DEFAULT_INSTANCE(&Usb);

// Define 2x16 LCD unit using the following pins:
//
// LCD RS => digital pin 23
// LCD EN => digital pin 25     (Enable)
// LCD D4 => 27
// LCD D5 => 29
// LCD D6 => 31
// LCD D7 => 33
// LCD R/W => GND               (ground)
// LCD VSS => GND               (ground)
// LCD VDD => +5V
// LCD contrast: 10k lin pot:
//               end pin 1 => GND
//               end pin 2 => +5V
//               center pin => LCD pin 3  (cursor/wiper)
const int lcd_rows = 2;     // number of lcd rows
const int lcd_cols = 16;    // number of lcd columns
const int lcd_RS = 23;
const int lcd_EN = 25;
const int lcd_D4 = 27;
const int lcd_D5 = 29;
const int lcd_D6 = 31;
const int lcd_D7 = 33;
LiquidCrystal lcd(lcd_RS, lcd_EN, lcd_D4, lcd_D5, lcd_D6, lcd_D7);


///////////////////////////////////////
// PWM output pins
///////////////////////////////////////
const int PWM1 = 2;
const int PWM2 = 3;
const int PWM3 = 4;
const int PWM4 = 5;
const int PWM5 = 6;
const int PWM6 = 7;
const int PWM7 = 8;
const int PWM8 = 11;

///////////////////////////////////////
// general constants
///////////////////////////////////////
const int DACS_RESOLUTION = 12;         	  // DACs resolution
const int NOTE_CV  = DAC0;              	  // DAC number for note CV
const int VELOCITY_CV = DAC1;           	  // DAC number for velocity CV
const int GATE_PIN = 24;                	  // Digital pin used to generate the gate signal
const int TRIGGER_PIN = 26;                 // Digital pin used to generate the trigger signal
const long VELOCITY_MULTIPLIER = 4097/127;	// conversion factor for velocity input on DAC1 (0x0FFF/0x7F)
const int TRIGGER_WIDTH = 10;               // trigger signal length = 10 msec

///////////////////////////////////////
// global variables
///////////////////////////////////////
int active_notes;                 // how many notes are currently active
int current_note;                 // note currently playing
int current_velocity;             // velocity of note currently playing
int future_note;                  // future note to play
int future_velocity;              // velocity of future note to play
bool trigger_done;                // whether the trigger signal has been processed
bool trigger_flag;                // need to process a trigger signal
unsigned long startTriggerTime;   // time when last trigger started

//////////////////////////
// Push Button Handlers
//////////////////////////
void pb_up_handler()
{
  if (DEBUG ==1)
  {
    Serial.println("Push Button UP");
  }
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("Push Button UP");
}

void pb_down_handler()
{
if (DEBUG ==1)
  {
    Serial.println("Push Button DOWN");
  }
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("Push Button DOWN");
}

void pb_left_handler()
{
if (DEBUG ==1)
  {
    Serial.println("Push Button LEFT");
  }
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("Push Button LEFT");
}

void pb_right_handler()
{
if (DEBUG ==1)
  {
    Serial.println("Push Button RIGHT");
  }
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("Push Button RIGHT");
}

void pb_enter_handler()
{
if (DEBUG ==1)
  {
    Serial.println("Push Button ENT");
  }
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("Push Button ENT");
}

//////////////////////////////////////
// USB MIDI handlers and subroutines
//////////////////////////////////////
void play_current_note()
{
  analogWrite(NOTE_CV, MIDI2DAC[current_note]);
  analogWrite(VELOCITY_CV, current_velocity);
  digitalWrite(GATE_PIN, HIGH);
  fire_trigger();
}

void stop_notes()
{
  analogWrite(NOTE_CV, MIDI2DAC[0]);
  analogWrite(VELOCITY_CV, 0);
  digitalWrite(GATE_PIN, LOW);
  current_note = 0;
  current_velocity = 0;
  future_note = 0;
  future_velocity = 0;
}

void trigger_handling()
{
  if (trigger_flag == HIGH)
  {
    digitalWrite(TRIGGER_PIN, HIGH);
    startTriggerTime = millis();
    trigger_flag = LOW;
    trigger_done = LOW;
  }
  else
  {
    if (trigger_done == LOW)
    {
      if (millis() - startTriggerTime >= TRIGGER_WIDTH)
      {
        digitalWrite(TRIGGER_PIN, LOW);
        trigger_done = HIGH;
      }
    }
  }
}

void fire_trigger()
{
  trigger_flag = HIGH;
}

void handleNoteOff(byte channel, byte pitch, byte velocity)
{
  if (DEBUG == 1)
  {
    Serial.print("Received Note Off: ");
    Serial.print(channel, HEX);
    Serial.print(" ");
    Serial.print(pitch, HEX);
    Serial.print(" ");
    Serial.println(velocity, HEX);
  }

  // print received info on LCD
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("Note Off - ch ");
  lcd.print(channel, HEX);
  lcd.setCursor(0,1);
  lcd.print("pitch ");
  lcd.print(pitch, HEX);
  lcd.print(" vel.");
  lcd.print(velocity, HEX);

  if (active_notes > 0)
  {
    active_notes--;

    if (active_notes == 0)
    {
      stop_notes();
    }
    else
    {
      if (pitch == current_note)
      {
        current_note = future_note;
        current_velocity = future_velocity;
        future_note = 0;
        future_velocity = 0;
        play_current_note();
      }
      else
      {
        future_note = 0;
        future_velocity = 0;
      }
    }
  }
  else
  {
    stop_notes();
  }
}

void handleNoteOn(byte channel, byte pitch, byte velocity)
{
  if (DEBUG ==1)
  {
    Serial.print("Received Note On: ");
    Serial.print(channel, HEX);
    Serial.print(" ");
    Serial.print(pitch, HEX);
    Serial.print(" ");
    Serial.println(velocity, HEX);
  }

  // print received info on LCD
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("Note On  - ch ");
  lcd.print(channel, HEX);
  lcd.setCursor(0,1);
  lcd.print("pitch ");
  lcd.print(pitch, HEX);
  lcd.print(" vel.");
  lcd.print(velocity, HEX);

  // generate note, velocity, gate
  active_notes++;

  if (active_notes <= 1)
  {
    current_note = pitch;
    current_velocity = velocity * VELOCITY_MULTIPLIER;
    play_current_note();
  }
  else
  {
    if (current_note > pitch)
    {
      future_note = current_note;
      future_velocity = current_velocity;
      current_note = pitch;
      current_velocity = velocity * VELOCITY_MULTIPLIER;
      play_current_note();
    }
    else
    {
      future_note = pitch;
      future_velocity = velocity * VELOCITY_MULTIPLIER;
    }
  }
}

// this function associates the device information received
// with a CC MIDI message and converts it into the pin
// number where the control voltage needs to be generated
int cc_convert(byte device)
{
  switch(device)
  {
    case 0x46:
      return PWM1;

    case 0x47:
      return PWM2;

    case 0x48:
      return PWM3;

    case 0x49:
      return PWM4;

    case 0x4A:
      return PWM5;

    case 0x4B:
      return PWM6;

    case 0x4C:
      return PWM7;

    case 0x4D:
      return PWM8;

    default:
      lcd.clear();
      lcd.setCursor(0, 0);
      lcd.print("CC - device not ");
      lcd.setCursor(0,1);
      lcd.print("recognized ");
      lcd.setCursor(11, 1);
      lcd.print(device, HEX);
      return 0;
  }
}

void handleControlChange(byte channel, byte data1, byte data2)
{
  int pin;

  if (DEBUG == 1)
  {
    Serial.print("Received Control change message: ");
    Serial.print(channel, HEX);
    Serial.print(" ");
    Serial.print(data1, HEX);
    Serial.print(" ");
    Serial.println(data2, HEX);
  }

  // print received info on LCD
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("Ctrl Chg - ch ");
  lcd.print(channel, HEX);
  lcd.setCursor(0,1);
  lcd.print("dat1 ");
  lcd.print(data1, HEX);
  lcd.print(" dat2 ");
  lcd.print(data2, HEX);

  pin = cc_convert(data1);
  // if pin == 0 there was a conversion error
  if (pin != 0)
  {
    // data2 has 7 significant bits while
    // the analog output has resolution of 12
    analogWrite(pin, data2 << 5);
  }
}


///////////////////////////
// Main Arduino Functions
///////////////////////////

void setup()
{
  // Serial communications initialization
  if (DEBUG == 1)
  {
    Serial.begin(9600);
  }

  // LCD initialization
  lcd.begin(lcd_cols,lcd_rows);
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("Initializing...");
  lcd.setCursor(0, 1);
  lcd.print("SW Vers.: ");
  lcd.setCursor(10,1);
  lcd.print(VERSION_NUMBER);

  delay(1000);

  // MIDI callback functions definition
  MIDI.setHandleNoteOff(handleNoteOff);
  MIDI.setHandleNoteOn(handleNoteOn);
  MIDI.setHandleControlChange(handleControlChange);

  // MIDI initialization
  MIDI.begin(MIDI_CHANNEL_OMNI);  // use omni channel

  // USB Host interface initialization
  if (Usb.Init() == -1)
  {
    if (DEBUG == 1)
    {
      Serial.println("Failed to initialize USB HOST");
    }

      lcd.clear();
      lcd.print("Init Error");

    while(1);
  }

  // set DACs resolution
  analogWriteResolution(DACS_RESOLUTION);

  // pin settings
  pinMode(GATE_PIN, OUTPUT);
  pinMode(NOTE_CV, OUTPUT);
  pinMode(VELOCITY_CV, OUTPUT);
  pinMode(TRIGGER_PIN, OUTPUT);
  pinMode(PWM1, OUTPUT);
  pinMode(PWM2, OUTPUT);
  pinMode(PWM3, OUTPUT);
  pinMode(PWM4, OUTPUT);
  pinMode(PWM5, OUTPUT);
  pinMode(PWM6, OUTPUT);
  pinMode(PWM7, OUTPUT);
  pinMode(PWM8, OUTPUT);

  // initialize outputs to 0V
  analogWrite(NOTE_CV, 0);
  analogWrite(VELOCITY_CV, 0);
  digitalWrite(TRIGGER_PIN, 0);
  analogWrite(PWM1, 0);
  analogWrite(PWM2, 0);
  analogWrite(PWM3, 0);
  analogWrite(PWM4, 0);
  analogWrite(PWM5, 0);
  analogWrite(PWM6, 0);
  analogWrite(PWM7, 0);
  analogWrite(PWM8, 0);

  // buttons initialization
  push_buttons.add(pb_up);
  push_buttons.add(pb_down);
  push_buttons.add(pb_left);
  push_buttons.add(pb_right);
  push_buttons.add(pb_enter);

  // global variables intialization
  active_notes = 0;
  current_note = 0;
  current_velocity = 0;
  future_note = 0;
  future_velocity = 0;
  trigger_done = HIGH;
  startTriggerTime = 0;
  trigger_flag = LOW;

  // give some time to the attached devices
  // to complete the initialization
  delay(1000);

  if (DEBUG == 1)
  {
    Serial.println("Initialization completed");
  }

  // tell user that initialization is completed
  lcd.clear();
  lcd.setCursor(0,0);
  lcd.print("  MIDI Module");
  lcd.setCursor(0, 1);
  lcd.print("  ready. v");
  lcd.setCursor(10,1);
  lcd.print(VERSION_NUMBER);
}

void loop()
{
  Usb.Task();           // USB Host main task
  MIDI.read();          // Polling MIDI inputs
  push_buttons.check(); // polling push buttons
  trigger_handling();   // trigger signal start/stop (not connected in HW)

  // To verify/reconfigure the code numbers associated to
  // each push button (PB_UP, PB_DOWN, PB_LEFT, PB_RIGHT, PB_ENTER),
  // uncomment the following line and make sure DEBUG = 1.
  // Then hold down each button noting the corresponding code number
  // in the serial monitor
  // push_buttons_configuration();
}

void push_buttons_configuration()
{
  if (DEBUG == 1)
  {
    unsigned int value = analogRead(PB_PIN);
    Serial.println(value);
    delay(250);
  }
}


// CALLBACKS REFERENCE (doc)
/*
ErrorCallback                = void (*)(int8_t);
NoteOffCallback              = void (*)(Channel channel, byte note, byte velocity);
NoteOnCallback               = void (*)(Channel channel, byte note, byte velocity);
AfterTouchPolyCallback       = void (*)(Channel channel, byte note, byte velocity);
ControlChangeCallback        = void (*)(Channel channel, byte, byte);
ProgramChangeCallback        = void (*)(Channel channel, byte);
AfterTouchChannelCallback    = void (*)(Channel channel, byte);
PitchBendCallback            = void (*)(Channel channel, int);
SystemExclusiveCallback      = void (*)(byte * array, unsigned size);
TimeCodeQuarterFrameCallback = void (*)(byte data);
SongPositionCallback         = void (*)(unsigned beats);
SongSelectCallback           = void (*)(byte songnumber);
TuneRequestCallback          = void (*)(void);
ClockCallback                = void (*)(void);
StartCallback                = void (*)(void);
TickCallback                 = void (*)(void);
ContinueCallback             = void (*)(void);
StopCallback                 = void (*)(void);
ActiveSensingCallback        = void (*)(void);
SystemResetCallback          = void (*)(void);
*/
