#include <Adafruit_NeoPixel.h>
#include <LiquidCrystal_I2C.h>   // https://github.com/johnrickman/LiquidCrystal_I2C
#include <Button_SL.hpp>         // https://github.com/DoImant/Button_SL
#include <Streaming.h>           // https://github.com/janelia-arduino/Streaming
Print &cout {Serial};

// If the search in an array was unsuccessful in terms of the search, -1 is returned.
constexpr int NO_VALID_INDEX {-1};
constexpr uint8_t ANALOG_INPUT_PINS[] = {A0, A1, A2, A3};   // Potentiometer-Eingänge
constexpr uint8_t NUM_ANALOG_VALUES {sizeof(ANALOG_INPUT_PINS) / sizeof(ANALOG_INPUT_PINS[0])};

// Namen der Potentiometer
const char *potiNames[NUM_ANALOG_VALUES] = {
    "Sound",     // Poti 1
    "Discord",   // Poti 2
    "YouTube",   // Poti 3
    "Game"       // Poti 4
};

// NeoPixel-Setup für 5 LEDs (5 LEDs, auch wenn es nur 4 Potis gibt)
Adafruit_NeoPixel strip(5, 6, NEO_GRB + NEO_KHZ800);   // 5 LEDs an Pin 6
LiquidCrystal_I2C lcd(0x27, 16, 2);                    // I2C-Adresse 0x27, 16 Zeichen, 2 Zeilen
Btn::ButtonSL btn {2};
int analogSliderValues[NUM_ANALOG_VALUES];

template <size_t N> int hasChanged(int (&oldVal)[N], int (&newVal)[N]) {
  int result {NO_VALID_INDEX};

  for (size_t index = 0; index < N; ++index) {
    if (oldVal[index] != newVal[index]) {
      oldVal[index] = newVal[index];
      if (result < 0) result = index;
    }
  }
  return result;
}

template <size_t N> int nextNonZero(int (&data)[N], size_t start) {
  // Search from actal index to end
  if (start < N - 1) {
    for (size_t index = start + 1; index < N; ++index) {
      if (data[index]) { return index; }
    }
  }

  // Search from begin to actal index
  for (size_t index = 0; index < start; ++index) {
    if (data[index]) { return index; }
  }
  return 0;
}

template <size_t N> void updateSliderValues(const uint8_t (&analogPins)[N], int (&data)[N]) {
  for (size_t i = 0; i < N; i++) {
    data[i] = analogRead(analogPins[i]);   // Potentiometerwert lesen
  }
}

template <size_t N> void setLeds(int (&data)[N]) {
  // Farben der LEDs festlegen
  int colors[5][3] = {
      {255, 255, 255}, // LED 5: Weiß für Poti 1 (Sound)
      {0,   0,   255}, // LED 4: Blau für Poti 2 (Discord)
      {255, 0,   0  }, // LED 3: Rot für Poti 3 (YouTube)
      {255, 255, 0  }, // LED 2: Gelb für Poti 4 (Game)
      {0,   255, 0  }  // LED 1: Grün (bleibt aktiv, aber ohne Poti)
  };

  // LEDs basierend auf Potentiometerwerten einstellen
  for (size_t i = 0; i < N; i++) {
    if (analogSliderValues[i] == 0) {
      strip.setPixelColor(N - i, 0, 0, 0);   // LED aus, wenn Potentiometer auf 0
    } else {
      strip.setPixelColor(N - i, colors[i][0], colors[i][1], colors[i][2]);   // Farbe einstellen
    }
  }
  // LED 5 (Grün) bleibt immer an, da sie keinen Poti hat
  strip.setPixelColor(0, colors[N][0], colors[N][1], colors[N][2]);
  strip.show();   // LEDs anzeigen
}

void displaySliderOnLCD(int analogValue, const char *const potiName) {
  char buffer[17] {0};
  // Zeigt den Wert des Potentiometers in der ersten Zeile
  lcd.setCursor(0, 0);   // Erste Zeile, erste Spalte
  snprintf(buffer, sizeof(buffer), "%-10s %3ld", potiName, map(analogValue, 0, 1023, 0, 100));
  lcd.print(buffer);
  // Zweiter Schritt: Balkenanzeige in der zweiten Zeile
  int barLength = map(analogValue, 0, 1023, 0, 16);   // Länge für LCD (16 Zeichen)
  lcd.setCursor(0, 1);                                // Zur zweiten Zeile wechseln
  memset(buffer, ' ', sizeof(buffer));                // fill (complete) Buffer with spaces
  memset(buffer, '|', barLength);                     // fill bar characters
  lcd.print(buffer);
}

template <size_t N> void sendSliderValues(int (&data)[N]) {
  size_t index {0};
  for (const auto &val : data) {
    // cout <<_WIDTH(val,4);
    cout << val;
    (++index < N) ? cout << '|' : cout << '\n';
  }
}

void setup() {
  // Potentiometer als Eingänge konfigurieren
  for (auto &pin : ANALOG_INPUT_PINS) { pinMode(pin, INPUT); }

  // Serielle Kommunikation initialisieren
  Serial.begin(9600);
  btn.begin();

  // NeoPixel-Strip initialisieren
  strip.begin();
  strip.show();
  strip.setBrightness(255);   // Helligkeit der LEDs (0-255)

  // Taster konfigurieren
  pinMode(2, INPUT_PULLUP);   // Taster an Pin 2 mit Pullup-Widerstand

  // LCD initialisieren
  lcd.init();        // LCD initialisieren
  lcd.backlight();   // LCD-Hintergrundbeleuchtung einschalten
}

void loop() {
  static int oldSliderValues[NUM_ANALOG_VALUES] {-1};
  static int lastSliderIndex {0};

  if (btn.tick() != Btn::ButtonState::notPressed) {
    lastSliderIndex = (lastSliderIndex + 1) % NUM_ANALOG_VALUES;
    displaySliderOnLCD(analogSliderValues[lastSliderIndex], potiNames[lastSliderIndex]);
  }

  updateSliderValues(ANALOG_INPUT_PINS, analogSliderValues);
  auto result = hasChanged(oldSliderValues, analogSliderValues);
  if (result != NO_VALID_INDEX) {
    // if a value becomes 0 show the next Slider value > 0
    lastSliderIndex = (!analogSliderValues[result]) ? nextNonZero(analogSliderValues, result) : result;
    // Potentiometer-Werte aktualisieren und LEDs einstellen
    setLeds(analogSliderValues);   
    // Den zuletzt angepassten Poti auf dem LCD anzeigen
    displaySliderOnLCD(analogSliderValues[lastSliderIndex], potiNames[lastSliderIndex]);
    // Potentiometerwerte über die serielle Schnittstelle senden
    sendSliderValues(analogSliderValues);   // Simuliert das Senden der Werte (deej nutzt serielle Daten)
  }
}