#define FW_VER 1.02
#define USE_OLED true // OLED displays
#define USE_ENCODER true
#define USE_PANIC true
#define EEPROM_MIDI_CHANNEL_ADDR 0 // Address to save MIDI channel in EEPROM

// include libraries
#include <EEPROM.h>
#include <Adafruit_MCP23X17.h>
#include <MIDI.h>

#if USE_OLED
  #include <Wire.h>
  #include <Adafruit_GFX.h>
  #include <Adafruit_SSD1306.h>
  #include "font.h"
  #include "gfx.h"
  #define SCREEN_WIDTH 128
  #define SCREEN_HEIGHT 32
  Adafruit_SSD1306 oled(SCREEN_WIDTH,SCREEN_HEIGHT,&Wire,-1);
#endif

#if USE_ENCODER
  #define ENCODER_CLK 2
  #define ENCODER_DT 3
  int lastClk = HIGH;
#endif

#if USE_PANIC
  #define PANIC_BUTTON 4
  bool midiPanic = false;
#endif

unsigned long lastChannelChangeTime = 0;
bool channelChanged = false;
const unsigned long saveDelay = 2000; // 2 seconds

bool showSavedMessage = false;
unsigned long savedMessageStart = 0;
const unsigned long savedMessageDuration = 3000; // 3 seconds

// Instantiate libraries
Adafruit_MCP23X17 mcp1; // First MCP23017
Adafruit_MCP23X17 mcp2; // Second MCP23017

// Default hardware MIDI. SoftwareSerial is too slow...
MIDI_CREATE_DEFAULT_INSTANCE();

// Default MIDI channel
byte currentMidiChannel = MIDI_CHANNEL_OMNI; //8; // Default MIDI channel
byte savedChannel = EEPROM.read(EEPROM_MIDI_CHANNEL_ADDR);

// Struct for 2 MCP chips. 
struct MCPOutput {
  uint8_t chip;    // 0 = mcp1, 1 = mcp2
  uint8_t pin;     // 0–15
};

// LED shows MIDI activity
unsigned long ledOnTime = 0;
bool ledActive = false;
const unsigned long ledDuration = 50; // flash for 50ms

// Note to pin mapping. {chip, pin}
// One would be able to add up to 8 MCPs, but 2 is plenty for a Meowsic.
MCPOutput noteToMCP[32] = {
  // MCP1 – MIDI 57 to 72
  {0,  0}, // 57 A3
  {0,  1}, // 58 A#3
  {0,  2}, // 59 B3
  {0,  3}, // 60 C4
  {0,  4}, // 61 C#4
  {0,  5}, // 62 D4
  {0,  6}, // 63 D#4
  {0,  7}, // 64 E4
  {0,  8}, // 65 F4
  {0,  9}, // 66 F#4
  {0, 10}, // 67 G4
  {0, 11}, // 68 G#4
  {0, 12}, // 69 A4
  {0, 13}, // 70 A#4
  {0, 14}, // 71 B4
  {0, 15}, // 72 C5

  // MCP2 – MIDI 73 to 88
  {1,  0}, // 73 C#5
  {1,  1}, // 74 D5
  {1,  2}, // 75 D#5
  {1,  3}, // 76 E5
  {1,  4}, // 77 F5
  {1,  5}, // 78 F#5
  {1,  6}, // 79 G5
  {1,  7}, // 80 G#5
  {1,  8}, // 81 A5
  {1,  9}, // 82 A#5
  {1, 10}, // 83 B5
  {1, 11}, // 84 C6
  {1, 12}, // 85 C#6
  {1, 13}, // 86 D6
  {1, 14}, // 87 D#6
  {1, 15}  // 88 E6
};


void updateOLED() {
#if USE_OLED
    oled.clearDisplay();
    oled.drawBitmap(0, 12, midich, MIDICH_WIDTH, MIDICH_HEIGHT, 1);
    oled.setCursor(86, 32);
    oled.setTextColor(SSD1306_WHITE);
  if (currentMidiChannel == MIDI_CHANNEL_OMNI) {
    oled.print("ALL");
  } else {
    oled.print(currentMidiChannel);
  }

  if (showSavedMessage) {
    oled.drawBitmap(88, 0, savetorom, SAVETOROM_WIDTH, SAVETOROM_HEIGHT, 1);
  }

  #if USE_PANIC
  if (midiPanic) {
    oled.drawBitmap(1, 0, iconpanic, ICONPANIC_WIDTH, ICONPANIC_HEIGHT, 1);
  }
  #endif

  oled.display();
#endif
}

void checkEncoder() {
  #if USE_ENCODER
    int newClk = digitalRead(ENCODER_CLK);
    if (newClk != lastClk && newClk == LOW) {
      int newChannel = currentMidiChannel;

      if (digitalRead(ENCODER_DT) == LOW) {
        newChannel++;
      } else {
        newChannel--;
      }

      if (newChannel > 16) newChannel = 0;
      if (newChannel < 0)  newChannel = 16;

      if (newChannel != currentMidiChannel) {
        currentMidiChannel = newChannel;
        MIDI.begin(currentMidiChannel); // Reset MIDI, begin on current channel
        #if USE_OLED
          updateOLED();
        #endif

        // Mark that channel changed and restart delay timer
        lastChannelChangeTime = millis();
        channelChanged = true;
      }
    }
    lastClk = newClk;
  #endif
}


void handleNoteOn(byte channel, byte pitch, byte velocity) {
  #if USE_PANIC
  if (midiPanic) return;  // suppress all notes during panic
  #endif

  if (currentMidiChannel == MIDI_CHANNEL_OMNI || channel == currentMidiChannel) {

    if (pitch >= 57 && pitch <= 88) {
      MCPOutput out = noteToMCP[pitch - 57];
      if (out.chip == 0) {
        mcp1.digitalWrite(out.pin, HIGH);
      } else if (out.chip == 1) {
        mcp2.digitalWrite(out.pin, HIGH);
      }
   }
  }

  // Flash LED on Note On
  digitalWrite(LED_BUILTIN, HIGH);
  ledOnTime = millis();
  ledActive = true;
}

void handleNoteOff(byte channel, byte pitch, byte velocity) {
  #if USE_PANIC
  if (midiPanic) return;  // suppress all notes during panic
  #endif

  if (currentMidiChannel == MIDI_CHANNEL_OMNI || channel == currentMidiChannel) {
    if (pitch >= 57 && pitch <= 88) {
      MCPOutput out = noteToMCP[pitch - 57];
      if (out.chip == 0) {
        mcp1.digitalWrite(out.pin, LOW);
      } else if (out.chip == 1) {
        mcp2.digitalWrite(out.pin, LOW);
      }
   }
  }
}

void checkMidiPanic() {
  #if USE_PANIC
  static bool lastButtonState = HIGH;
  static unsigned long lastToggleTime = 0;
  const unsigned long debounceDelay = 50;

  bool currentButtonState = digitalRead(PANIC_BUTTON);

  if (lastButtonState == HIGH && currentButtonState == LOW) {
    unsigned long now = millis();
    if (now - lastToggleTime > debounceDelay) {
      midiPanic = !midiPanic;
      lastToggleTime = now;

      if (midiPanic) {
        // Turn off all notes immediately
        for (int i = 0; i < 32; i++) {
          if (noteToMCP[i].chip == 0) {
            mcp1.digitalWrite(noteToMCP[i].pin, LOW);
          } else {
            mcp2.digitalWrite(noteToMCP[i].pin, LOW);
          }
        }
      }

      #if USE_OLED
        updateOLED();  // Only update display when state changes
      #endif
    }
  }

  lastButtonState = currentButtonState;
  #endif
}




void setup() {
  // Validate saved midi channel in EEPROM
  if (savedChannel <= 16) {
    currentMidiChannel = savedChannel;
  } else {
    currentMidiChannel = MIDI_CHANNEL_OMNI;
  }

  #if USE_OLED
    if (!oled.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // I2C addr 0x3C
      // Don't halt if missing, just skip display
    } else {
      oled.clearDisplay();
      oled.drawBitmap(0, 0, bootuplogo, BOOTLOGO_WIDTH, BOOTLOGO_HEIGHT, 1);
      oled.setCursor(54, 18);
      oled.setTextColor(SSD1306_WHITE);
      oled.print("FW ver. " + String(FW_VER));
      oled.display();
      delay(2500);
      oled.setFont(&UiFont);
    }
  #endif

  #if USE_ENCODER
    pinMode(ENCODER_CLK, INPUT_PULLUP);
    pinMode(ENCODER_DT, INPUT_PULLUP);
  #endif

  #if USE_PANIC
    pinMode(PANIC_BUTTON, INPUT_PULLUP);
  #endif
  // Midi Activity LED on Pin 13 (aka LED_BUILTIN)
  pinMode(LED_BUILTIN, OUTPUT);

  // Initialize MCP23017s
  mcp1.begin_I2C(0x20);
  mcp2.begin_I2C(0x21);
  // Initialize MIDI
  MIDI.begin(currentMidiChannel);
  MIDI.setHandleNoteOn(handleNoteOn);
  MIDI.setHandleNoteOff(handleNoteOff);

  for (int i = 0; i < 16; i++) {
    mcp1.pinMode(i, OUTPUT);
    mcp1.digitalWrite(i, LOW);

    mcp2.pinMode(i, OUTPUT);
    mcp2.digitalWrite(i, LOW);
  }
  updateOLED();
}



void loop() {
  MIDI.read();

  #if USE_ENCODER
    checkEncoder();
  #endif

  #if USE_PANIC
    checkMidiPanic();
  #endif
  
  // Save channel if change timeout has passed
  if (channelChanged && (millis() - lastChannelChangeTime >= saveDelay)) {
    EEPROM.update(EEPROM_MIDI_CHANNEL_ADDR, currentMidiChannel);
    channelChanged = false;

    // Show "Saved" message
    showSavedMessage = true;
    savedMessageStart = millis();
    #if USE_OLED
      updateOLED();
    #endif
  }

  if (showSavedMessage && (millis() - savedMessageStart >= savedMessageDuration)) {
    showSavedMessage = false;
    #if USE_OLED
      updateOLED();  // Return to normal channel display
    #endif
  }

  // LED feedback
  if (ledActive && millis() - ledOnTime >= ledDuration) {
    digitalWrite(LED_BUILTIN, LOW);
    ledActive = false;
  }
}