/////////////////////////////////////////////
/////////////////////////////////////////////
///// HOLOCRON MICROCOSM MIDI MODULE ////////
/////// Antoine Laloux - Fev 2026 ///////////
/////////////////////////////////////////////
/////////////////////////////////////////////

#include <MIDI.h>
#include <Wire.h>
#include <U8g2lib.h>

MIDI_CREATE_DEFAULT_INSTANCE();

const byte MICROCOSM_CHANNEL = 4;

// 74HC4067 control pins
const byte MUX_SIG_PIN = A0;
const byte MUX_S0_PIN = 2;
const byte MUX_S1_PIN = 3;
const byte MUX_S2_PIN = 4;
const byte MUX_S3_PIN = 5;
const byte NUM_POTS = 16;
const byte LOOPER_BUTTON_PIN = 6; // push button to GND, uses INPUT_PULLUP

U8G2_SH1106_128X64_NONAME_1_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);

const byte POT_CC[NUM_POTS] = {
  9, 8, 15, 7, 0, 14, 12, 21,
  20, 11, 6, 17, 18, 13, 16, 19
};

const char *POT_NAMES[NUM_POTS] = {
  "MIX",
  "FILTER",
  "RESONANCE",
  "SHAPE",
  "N/C",
  "MOD FREQ",
  "SPACE",
  "FADE TIME",
  "REVERB TIME",
  "REPEATS",
  "ACTIVITY",
  "LOOP SPD+DIR",
  "LOOP STEP",
  "LOOP LEVEL",
  "FX VOLUME",
  "MOD DEPTH"
};

const char *LOOP_STEP_LABELS[6] = {"1/4", "1/2", "TAP", "2X", "4X", "8X"};
const char *SHAPE_NAMES[4] = {"SQUARE", "RAMP UP", "TRIANGLE", "RAMP DOWN"};
const byte SHAPE_CC_OFFSET = 0;

byte lastMidiValue[NUM_POTS];
byte lastLoopSpeed = 255;
byte lastLoopDirection = 255;
byte lastLoopStep = 255;
bool looperEngaged = false;
byte looperButtonStableState = HIGH;
byte looperButtonLastRead = HIGH;
unsigned long looperButtonDebounceMs = 0;
const byte MIDI_DEADBAND = 3;
const byte CROSSTALK_DEADBAND = 7;
const byte CROSSTALK_TAKEOVER_DELTA = 14;
const unsigned long CROSSTALK_LOCK_MS = 120;
const unsigned long BUTTON_DEBOUNCE_MS = 25;
byte crosstalkActivePot = 255;
unsigned long crosstalkLockUntil = 0;

void drawCenteredText(const uint8_t *font, int y, const char *text);
void updateLooperButton();

void setMuxChannel(byte channel) {
  digitalWrite(MUX_S0_PIN, bitRead(channel, 0));
  digitalWrite(MUX_S1_PIN, bitRead(channel, 1));
  digitalWrite(MUX_S2_PIN, bitRead(channel, 2));
  digitalWrite(MUX_S3_PIN, bitRead(channel, 3));
}

byte readPotMidiValue(byte channel) {
  setMuxChannel(channel);

  bool noisyChannel = (channel == 0 || channel == 3);
  delayMicroseconds(noisyChannel ? 220 : 150);
  analogRead(MUX_SIG_PIN);

  int raw;
  if (noisyChannel) {
    int raw1 = analogRead(MUX_SIG_PIN);
    int raw2 = analogRead(MUX_SIG_PIN);
    int raw3 = analogRead(MUX_SIG_PIN);
    int raw4 = analogRead(MUX_SIG_PIN);
    int raw5 = analogRead(MUX_SIG_PIN);
    raw = (raw1 + raw2 + raw3 + raw4 + raw5) / 5;
  } else {
    int raw1 = analogRead(MUX_SIG_PIN);
    int raw2 = analogRead(MUX_SIG_PIN);
    int raw3 = analogRead(MUX_SIG_PIN);
    raw = (raw1 + raw2 + raw3) / 3;
  }
  return (byte)(raw >> 3);          
}

byte valueToPercent(byte value) {
  return (byte)((value * 100UL + 63UL) / 127UL);
}

void drawProgressBar(byte percent) {
  const int x = 4;
  const int y = 44;
  const int w = 120;
  const int h = 14;
  int fill = (percent * (w - 2)) / 100;

  u8g2.drawFrame(x, y, w, h);
  if (fill > 0) u8g2.drawBox(x + 1, y + 1, fill, h - 2);
}

void drawShapeIcon(byte shapeIndex) {
  const int x = 8;
  const int y = 44;
  const int w = 112;
  const int h = 18;
  const int yTop = y + 1;
  const int yBot = y + h - 1;
  const int step = w / 2;
  const int x0 = x;
  const int x1 = x + step;
  const int x2 = x + w;

  switch (shapeIndex) {
    case 0: // SQUARE
      u8g2.drawLine(x0, yBot, x0, yTop);
      u8g2.drawLine(x0, yTop, x0 + step / 2, yTop);
      u8g2.drawLine(x0 + step / 2, yTop, x0 + step / 2, yBot);
      u8g2.drawLine(x0 + step / 2, yBot, x1, yBot);
      u8g2.drawLine(x1, yBot, x1, yTop);
      u8g2.drawLine(x1, yTop, x1 + step / 2, yTop);
      u8g2.drawLine(x1 + step / 2, yTop, x1 + step / 2, yBot);
      u8g2.drawLine(x1 + step / 2, yBot, x2, yBot);
      break;
    case 1: // RAMP UP 
      u8g2.drawLine(x0, yBot, x1 - 2, yTop);
      u8g2.drawLine(x1 - 2, yTop, x1 - 2, yBot);
      u8g2.drawLine(x1, yBot, x2 - 2, yTop);
      u8g2.drawLine(x2 - 2, yTop, x2 - 2, yBot);
      break;
    case 2: // TRIANGLE
      u8g2.drawLine(x0, yBot, x0 + step / 2, yTop);
      u8g2.drawLine(x0 + step / 2, yTop, x1, yBot);
      u8g2.drawLine(x1, yBot, x1 + step / 2, yTop);
      u8g2.drawLine(x1 + step / 2, yTop, x2, yBot);
      break;
    default: // RAMP DOWN
      u8g2.drawLine(x0, yTop, x1 - 2, yBot);
      u8g2.drawLine(x1 - 2, yBot, x1 - 2, yTop);
      u8g2.drawLine(x1, yTop, x2 - 2, yBot);
      u8g2.drawLine(x2 - 2, yBot, x2 - 2, yTop);
      break;
  }
}

void showShapeOnOled(byte potIndex, byte cc, byte value) {
  byte shapeIndex = value / 32;
  if (shapeIndex > 3) shapeIndex = 3;
  char top[22];
  snprintf(top, sizeof(top), "CC%03u", potIndex + 1, cc);

  u8g2.firstPage();
  do {
    drawCenteredText(u8g2_font_6x12_tf, 9, top);
    drawCenteredText(u8g2_font_10x20_tf, 30, "SHAPE");
    drawCenteredText(u8g2_font_6x12_tf, 41, SHAPE_NAMES[shapeIndex]);
    drawShapeIcon(shapeIndex);
  } while (u8g2.nextPage());
}

void drawCenteredText(const uint8_t *font, int y, const char *text) {
  u8g2.setFont(font);
  int w = u8g2.getStrWidth(text);
  int x = (128 - w) / 2;
  if (x < 0) x = 0;
  u8g2.drawStr(x, y, text);
}

void showOnOled(byte potIndex, byte cc, byte value, const char *extraLabel, bool showProgress) {
  byte percent = valueToPercent(value);
  char top[22];
  char valText[10];

  snprintf(top, sizeof(top), "CC%03u", potIndex + 1, cc);
  snprintf(valText, sizeof(valText), "%3u%%", percent);

  u8g2.firstPage();
  do {
    drawCenteredText(u8g2_font_6x12_tf, 9, top);
    drawCenteredText(u8g2_font_10x20_tf, 30, POT_NAMES[potIndex]);

    if (showProgress) {
      if (extraLabel != nullptr) {
        drawCenteredText(u8g2_font_6x12_tf, 41, extraLabel);
      }
      u8g2.setFont(u8g2_font_6x12_tf);
      u8g2.drawStr(98, 41, valText);
      drawProgressBar(percent);
    } else if (extraLabel != nullptr) {
      // LOOP STEP mode: replace bar with a very large centered step label.
      drawCenteredText(u8g2_font_10x20_tf, 62, extraLabel);
    }
  } while (u8g2.nextPage());
}

void showLoopSpeedDirOnOled(byte potIndex, byte speedValue, byte directionValue) {
  char top[22];
  snprintf(top, sizeof(top), "CC017+023", potIndex + 1);

  const int barX = 8;
  const int barY = 50;
  const int barW = 112;
  const int barH = 10;
  const int midX = barX + (barW / 2);
  const int halfW = (barW - 2) / 2;
  int fill = (int)map((long)speedValue, 0L, 127L, 0L, halfW);
  if (fill < 0) fill = 0;
  if (fill > halfW) fill = halfW;

  u8g2.firstPage();
  do {
    drawCenteredText(u8g2_font_6x12_tf, 9, top);
    drawCenteredText(u8g2_font_10x20_tf, 30, "LOOP SPEED");
    drawCenteredText(u8g2_font_6x12_tf, 43, "-2   I   0   I   +2");
    u8g2.drawFrame(barX, barY, barW, barH);
    u8g2.drawLine(midX, barY, midX, barY + barH - 1);
    if (fill > 0) {
      if (directionValue >= 64) {
        u8g2.drawBox(midX - fill, barY + 1, fill, barH - 2);
      } else {
        u8g2.drawBox(midX + 1, barY + 1, fill, barH - 2);
      }
    }
  } while (u8g2.nextPage());
}

void sendLoopSpeedDirectionFromPot11(byte potIndex, byte potValue) {
  byte speedValue;
  byte directionValue;

  if (potValue <= 63) {
    speedValue = (byte)map((long)potValue, 0L, 63L, 127L, 0L);
    directionValue = 127;
  } else {
    speedValue = (byte)map((long)potValue, 64L, 127L, 0L, 127L);
    directionValue = 0;
  }

  if (directionValue != lastLoopDirection) {
    MIDI.sendControlChange(23, directionValue, MICROCOSM_CHANNEL);
    lastLoopDirection = directionValue;
  }

  if (speedValue != lastLoopSpeed) {
    MIDI.sendControlChange(17, speedValue, MICROCOSM_CHANNEL);
    lastLoopSpeed = speedValue;
  }

  showLoopSpeedDirOnOled(potIndex, speedValue, directionValue);
}

void sendLoopStepFromPot16(byte potIndex, byte potValue) {
  // CC18 stepped values from Microcosm MIDI chart:
  // 0=1/4, 1=1/2, 2=TAP, 3=2X, 4=4X, 5=8X
  byte step = (byte)map((long)potValue, 0L, 127L, 0L, 5L);
  if (step > 5) step = 5;
  if (step == lastLoopStep) return;

  MIDI.sendControlChange(18, step, MICROCOSM_CHANNEL);
  lastLoopStep = step;

  showOnOled(potIndex, 18, potValue, LOOP_STEP_LABELS[step], false);
}

void updateLooperButton() {
  byte reading = digitalRead(LOOPER_BUTTON_PIN);

  if (reading != looperButtonLastRead) {
    looperButtonDebounceMs = millis();
    looperButtonLastRead = reading;
  }

  if ((millis() - looperButtonDebounceMs) < BUTTON_DEBOUNCE_MS) return;
  if (reading == looperButtonStableState) return;

  looperButtonStableState = reading;

  if (looperButtonStableState == LOW) {
    looperEngaged = !looperEngaged;
    MIDI.sendControlChange(22, looperEngaged ? 127 : 0, MICROCOSM_CHANNEL);
  }
}

void sendPotIfChanged(byte potIndex, byte newValue) {
  byte oldValue = lastMidiValue[potIndex];
  int delta = (int)newValue - (int)oldValue;
  if (delta < 0) delta = -delta;
  bool crosstalkChannel = (potIndex == 0 || potIndex == 3);
  byte effectiveDeadband = crosstalkChannel ? CROSSTALK_DEADBAND : MIDI_DEADBAND;
  if (delta <= effectiveDeadband) return;

  if (crosstalkChannel) {
    unsigned long now = millis();
    if (crosstalkActivePot != 255 && now < crosstalkLockUntil && potIndex != crosstalkActivePot) {
      if (delta < CROSSTALK_TAKEOVER_DELTA) return;
    }
    crosstalkActivePot = potIndex;
    crosstalkLockUntil = now + CROSSTALK_LOCK_MS;
  }

  if (potIndex == 11) {
    sendLoopSpeedDirectionFromPot11(potIndex, newValue);
    lastMidiValue[potIndex] = newValue;
    return;
  }

  if (potIndex == 12) {
    sendLoopStepFromPot16(potIndex, newValue);
    lastMidiValue[potIndex] = newValue;
    return;
  }

  // C4 not connected.
  if (potIndex == 4) {
    return;
  }

  if (potIndex == 3) {
    byte shapeValue = (byte)((newValue + SHAPE_CC_OFFSET) & 0x7F); // modulo 128
    MIDI.sendControlChange(7, shapeValue, MICROCOSM_CHANNEL);
    lastMidiValue[potIndex] = newValue;
    showShapeOnOled(potIndex, 7, shapeValue);
  } else {
    byte cc = POT_CC[potIndex];
    MIDI.sendControlChange(cc, newValue, MICROCOSM_CHANNEL);
    lastMidiValue[potIndex] = newValue;
    showOnOled(potIndex, cc, newValue, nullptr, true);
  }
}

void setup() {
  pinMode(MUX_S0_PIN, OUTPUT);
  pinMode(MUX_S1_PIN, OUTPUT);
  pinMode(MUX_S2_PIN, OUTPUT);
  pinMode(MUX_S3_PIN, OUTPUT);
  pinMode(LOOPER_BUTTON_PIN, INPUT_PULLUP);

  MIDI.begin(MIDI_CHANNEL_OFF);
  Wire.begin();
  u8g2.begin();

  u8g2.firstPage();
  do {
    u8g2.setFont(u8g2_font_6x12_tf);
    u8g2.drawStr(0, 12, "MICROCOSM STATION");
    u8g2.drawStr(0, 26, "UNO + OLED READY");
    u8g2.drawStr(0, 40, "CALIBRATING...");
  } while (u8g2.nextPage());

  for (byte i = 0; i < NUM_POTS; i++) {
    lastMidiValue[i] = readPotMidiValue(i);
  }

  u8g2.firstPage();
  do {
    u8g2.setFont(u8g2_font_6x12_tf);
    drawCenteredText(u8g2_font_6x12_tf, 12, "HOLOGRAM");
    drawCenteredText(u8g2_font_6x12_tf, 26, "MICROCOSM");
    drawCenteredText(u8g2_font_6x12_tf, 40, "CONTROLLER");
    drawCenteredText(u8g2_font_6x12_tf, 54, "V1.2");
  } while (u8g2.nextPage());

  delay(2000);

  u8g2.firstPage();
  do {
    drawCenteredText(u8g2_font_6x12_tf, 32, "Made by");
    drawCenteredText(u8g2_font_6x12_tf, 42, "Antoine Laloux");
  } while (u8g2.nextPage());

  delay(1500);
}

void loop() {
  updateLooperButton();
  for (byte i = 0; i < NUM_POTS; i++) {
    if (i == 4) continue; // C4 not connected
    byte value = readPotMidiValue(i);
    sendPotIfChanged(i, value);
  }
}