#include <Button_SL.hpp>
#include <Streaming.h>
#include <LiquidCrystal_I2C.h>

Print &cout {Serial};
// #define SERIAL_OUT   // Remove comment character if an output is to be made on the serial console.

using namespace Btn;

//////////////////////////////////////////////////////////////////////////////
/// @brief Define an object that assigns an LED and a sound to a switch.
///        This class is derived from a button object.
///        The button object queries switches/buttons (debounced).
//////////////////////////////////////////////////////////////////////////////
class Select : public Button {
public:
  // Constructor
  Select(byte switchPin, byte ledPin, unsigned int note) : Button {switchPin}, ledPin {ledPin}, note {note} {}

  // Methodes
  void begin() {
    pinMode(ledPin, OUTPUT);
    Button::begin();
  }
  void ledOn() {
    if (LOW == digitalRead(ledPin)) {
      digitalWrite(ledPin, HIGH);
      ++counter;
    }
  }
  void ledOff() const {
    if (HIGH == digitalRead(ledPin)) { digitalWrite(ledPin, LOW); }
  }
  bool isLedOn() const { return digitalRead(ledPin) ? true : false; }
  unsigned int getNote() const { return note; }
  unsigned int getCount() const { return counter; }

  // Attributes
private:
  byte ledPin;
  unsigned int note;
  unsigned int counter {0};
};

constexpr char const *COLORS[4] {"gruen", "blau", "orange", "rot"};
// Define the frequencies for the desired note
constexpr unsigned int NOTE_G3 {196};
constexpr unsigned int NOTE_C4 {262};
constexpr unsigned int NOTE_E4 {330};
constexpr unsigned int NOTE_G5 {784};
constexpr unsigned int SOUND_LENGTH_MS {200};   // in milliseconds

constexpr byte BUZZER_PIN {8};

constexpr byte I2C_ADDR {0x27};
constexpr byte LCD_COLUMNS {16};
constexpr byte LCD_LINES {2};

LiquidCrystal_I2C lcd(I2C_ADDR, LCD_COLUMNS, LCD_LINES);   // Create display object
Print &lcdOut {lcd};                                       // Use Printstream for the output

// Create an array of selection objects with the appropriate initialization values
Select selections[] {
    {4, 12, NOTE_G3}, // Switch Pin Nr., LED Pin Nr, Note value
    {5, 11, NOTE_C4},
    {6, 10, NOTE_E4},
    {7, 9,  NOTE_G5}
};

//////////////////////////////////////////////////////////////////////////////
/// @brief Check which of the connected switches are activated/pressed.
///
/// @tparam (&sel)[N]   Array with objects of type Select
/// @return byte        Bit mask that corresponds to the activated switches.
//////////////////////////////////////////////////////////////////////////////
template <size_t N> byte checkSwitch(Select (&sel)[N]) {
  byte bitMask {0};
  for (byte i = 0; i < N; ++i) {
    if (true == sel[i].tick()) { bitMask |= (1 << i); }   // For instance: 0000 1111 = four switchs are active
  }
  return bitMask;
}

//////////////////////////////////////////////////////////////////////////////
/// @brief Switches an LED assigned to the switch on/off and plays a sound
///        when it is switched on.
///
/// @tparam (&sel)[N]   Array with objects of type Select
/// @param buzzerPin    The buzzer is connected to this pin
/// @param mask         Bit mask of all active switches
/// @param isOnIdx      Index on the selected object whose LED is switched on
/// @return byte        Return the index to the active selection object
//////////////////////////////////////////////////////////////////////////////
template <size_t N> byte toggleSwitch(Select (&sel)[N], byte buzzerPin, const byte mask, byte isOnIdx) {
  for (byte i = 0; i < N; ++i) {
    if (isOnIdx != i && mask == (1 << i)) {   // Search for the first active switch using the bit mask
      if (isOnIdx != 0xFF) {                  // isOnIndex = 0xFF => no led is On
        sel[isOnIdx].ledOff();
        isOnIdx = 0xFF;
      }
      sel[i].ledOn();
      playSound(buzzerPin, sel[i].getNote(), SOUND_LENGTH_MS);
      isOnIdx = i;
    }
  }

  // If a bit assigned to a switch is not set, then switch off the corresponding LED
  if (not(mask & (1 << isOnIdx))) {
    sel[isOnIdx].ledOff();
    isOnIdx = 0xFF;
  }
  return isOnIdx;
}

//////////////////////////////////////////////////////////////////////////////
/// @brief Plays a short sound.
///
/// @param pin    Pin to which the buzzer is connected
/// @param note   Tone frequency (corresponds to a specific note)
/// @param length Playing duration of the note in ms
//////////////////////////////////////////////////////////////////////////////
void playSound(byte pin, unsigned int note, unsigned int length) {
  tone(pin, note);
  delay(length);
  noTone(pin);
}

//////////////////////////////////////////////////////////////////////////////
/// @brief Displays which LED has been switched on how often (Serial console)
///
/// @tparam (&sel)[N]   Array with objects of type Select
//////////////////////////////////////////////////////////////////////////////
template <size_t N> void printCount(Select (&sel)[N]) {
  byte idx = 0;

  for (auto &sel : selections) {
    cout << _FMT("LED %", COLORS[idx]) << _PAD(6 - strlen(COLORS[idx]), ' ')
         << _FMT(": % mal angeschaltet\n", _WIDTH(sel.getCount(), 3));
    ++idx;
  }
}

//////////////////////////////////////////////////////////////////////////////
/// @brief Display of color abbreviations (lcd)
///
//////////////////////////////////////////////////////////////////////////////
void lcdMainScreen() {
  lcd.setCursor(0, 0);
  lcdOut << F("Gr:");
  lcd.setCursor(8, 0);
  lcdOut << F("Bl:");
  lcd.setCursor(0, 1);
  lcdOut << F("Or:");
  lcd.setCursor(8, 1);
  lcdOut << F("Re:");
}

//////////////////////////////////////////////////////////////////////////////
/// @brief Display of the number of times an LED has been switched on (lcd)
///
/// @param sel    Pointer to the array with the selection objects
//////////////////////////////////////////////////////////////////////////////
void lcdPrintCount(Select *sel) {
  lcd.setCursor(4, 0);
  lcdOut << _WIDTH(sel[0].getCount(), 3);
  lcd.setCursor(12, 0);
  lcdOut << _WIDTH(sel[1].getCount(), 3);
  lcd.setCursor(4, 1);
  lcdOut << _WIDTH(sel[2].getCount(), 3);
  lcd.setCursor(12, 1);
  lcdOut << _WIDTH(sel[3].getCount(), 3);
}

void setup() {
#ifdef SERIAL_OUT
  Serial.begin(115200);
  cout << "Program started: " << __FILE__ << endl;
#endif
  for (auto &sel : selections) { sel.begin(); }   // Initialize the selection objects
  lcd.init();
  lcd.backlight();
  lcdMainScreen();
  lcdPrintCount(selections);
}

void loop() {
  static byte lastSwitchMask {0};
  static byte isOnIdx {0xFF};

  auto switchMask = checkSwitch(selections);
  if (switchMask != lastSwitchMask) {   // Only carry out the switching process if a switch position has changed
    // cout << "Mask: " << _WIDTHZ(_BIN(switchMask), 4) << endl;
    isOnIdx = toggleSwitch(selections, BUZZER_PIN, switchMask, isOnIdx);
    lastSwitchMask = switchMask;
    if (isOnIdx != 0xFF) {
      lcdPrintCount(selections);
#ifdef SERIAL_OUT
      printCount(selections);
      cout << _PAD(32, '-') << endl;
#endif
    }
  }
}