/*
  Reise nach Jerusalem
  Basierend auf: Quiz Buzzersystem für 8 Spieler

  Jeder Spieler hat eine LED und einen Button
  Wenn er an den Strom angeschlossen wird, dann soll die hier angezeigte Melodie fuer eine zufällige Zeit anfangen zu spielen.
  In der ersten Runde sollten 3 Spieler weiter kommen -
  das heisst 3 Button werden gedrueckt und dann geht die Melodie weiter und die LED von dem einen Spieler der als letztes gedrueckt hat geht aus.
  Das Spiel sollte dann enden sobald nur noch 1 Spieler da ist.
  https://forum.arduino.cc/index.php?topic=674983.0

  read buttons
  switch LEDs
  blink LED according "BlinkWithoutDelay"
  write non-blocking code

  copyright by noiasca

  Der Code kann genutzt oder verändert werden, solange der volle Copyright Vermerk und der Verweis auf 
  die ursprüngliche Download-Quelle: 
  https://werner.rothschopf.net/microcontroller/202103_arduino_reise_nach_jerusalem.htm
  als Kommentar erhalten bleibt.

  2021-05-29
*/

// define the GPIO
const uint8_t clearPin = A3;           // GPIO zum Zurücksetzen des Spiels (statt dem RESET Button)
const uint8_t walkAroundMin = 4;       // kürzeste Spielzeit der Musik (sec)
const uint8_t walkAroundMax = 10;      // längste Spielzeit der Musik (sec)
const uint8_t sitdownMax = 30;         // Beschränkung der Zeit für das "Niedersetzen" (sec)
const uint8_t song[] {49, 33, 37, 33, 49, 33, 37, 33, 37, 41, 44, 37, 41, 44};  // ein Song als Melodie

class Melody {
  private:
    uint32_t previousMillis;           // Zeitmanagement für die Melodie
    uint8_t nextTone = 0;              // welche Note im Song wird als nächstes gespielt
    bool state;                        // läuft der Song gerade
    const uint16_t interval = 500;     // wie viele MS wird jede Note gespielt
    const uint8_t peepPin;                // GPIO
    
  public:
    Melody(const uint8_t peepPin) : peepPin{peepPin} {}

    void start()
    {
      nextTone = 0;
      state = true;
      run();
    }

    void stop()
    {
      noTone(peepPin);
      state = false;
    }

    void run()                                             // Zeitsteuerung für die Melodie, muss laufend aufgerufen werden 
    {
      uint32_t currentMillis = millis();
      if (currentMillis - previousMillis > interval)       // Es ist Zeit für die nächste Note
      {
        previousMillis = currentMillis;
        tone(peepPin, song[nextTone]);
        nextTone++;
        if (nextTone >= sizeof(song) / sizeof(song[0])) nextTone = 0;
      }
    }
};

Melody melody(13);                     // ein Melodyobjekt mit Übergabe des GPIO

class Player {                         // ein Klasse für die Spieler
  private:
    bool state = true;                 // ist der Spieler noch im Spiel (true) oder bereits ausgeschieden (false)
    bool sits = false;                 // Sitzt der Spieler
    const uint8_t buzzer;              // GPIO für eine Buzzer / Taster gegen GND
    const uint8_t led;                 // GPIO für eine LED
    
  public:
    Player (const uint8_t buzzer, const uint8_t led) :
      buzzer(buzzer),
      led(led) {}

    void begin(void)
    {
      pinMode(buzzer, INPUT_PULLUP);   // Buzzer sind Input, auch hier - alle Button schließen gegen Masse
      pinMode(led, OUTPUT);            // LEDs sind Output
      digitalWrite(led, HIGH);         // Zum Spielanfang einschalten
    }

    bool getState()
    {
      return state;
    }

    void setState(bool newState)
    {
      state = newState;
      digitalWrite(led, newState);
    }

    void setSits(bool newSits)
    {
      sits = newSits;
    }

    bool pressed()                     // drückt der Spieler gerade den Button?
    {
      if (digitalRead(buzzer) == LOW)
        return true;
      else
        return false;
    }

    bool getSits()
    {
      return sits;
    }
};

Player player[] {                      // dann legen wir für unsere Spieler ein Array [] an und weisen die konkreten GPIOs zu
  {A0, 2},                             // Spieler 0: Buzzer GPIO, LED GPIO
  {A1, 3},                             // Spieler 1
  {A2, 4},                             // Spieler 2
  {10, 5}                              // Spieler 3: aufpassen, beim letzten kein komma mehr
};

constexpr size_t noOfPlayer = sizeof(player) / sizeof(player[0]);  // einmal die Anzahl der Spieler ermitteln

// eine Enumeration für die einzelnen Zustände der State Machine
enum class State {WALK_AROUND,         // Spiele Musik
                  SIT_DOWN,            // Musik aus - langsamsten Teilnehmer ermitteln
                  WAIT_FOR_CLEAR       // Warten auf clear durch Spielleiter
                 } state;              // eine Variable in der wir den aktuellen Status speichern

void setup() {
  Serial.begin(115200);                          // die Serielle aktivieren, damit man sieht was passiert
  Serial.println(F("\nMusical chairs, also known as Trip to Jerusalem"));
  Serial.println(F("press start to play music ..."));
  pinMode(clearPin, INPUT_PULLUP);               // internen Pullup verwenden, Taster gegen Masse schalten
  for (auto &i : player) i.begin();              // range based for ... i ist nun eine Referenz auf den jeweiligen player
  state = State::WAIT_FOR_CLEAR;                 // Spielbegin: auf eine Freigabe durch den Spielleiter warten
}

void loop() {
  static uint8_t currentRound = 1;               // current Round
  static uint32_t currentPlayTimeMS = 42;        // current planed playtime in MS
  static uint8_t currentPresses = 0;             // wie viele Spieler haben in dieser Runde schon gedrückt
  static uint32_t previousMillis;                // letzter Aufruf
  uint32_t currentMillis = millis();             // aktuellen Zeitstempel merken
                                                 
  switch (state)                                 
  {                                              
    case State::WALK_AROUND :                    // Warten auf die Tastendrücke
      melody.run();                              // dafür sorgen, dass die Musik gegebenenfalls die nächste Note spielt
      
      for (size_t i = 0; i < noOfPlayer; i++)    // prüfen ob ein aktiver Spieler zu früh drückt
      {
        if (player[i].getState() == true && player[i].pressed() == true)
        {
          Serial.print(F("pressed to early index=")); Serial.println(i);
          player[i].setState(false);
          melody.stop();                         // Musik spielt ja noch, müssen wir also stoppen
          state = State::WAIT_FOR_CLEAR;
          Serial.println(F("--> WAIT_FOR_CLEAR"));
        }
      }
      
      if (currentMillis - previousMillis > currentPlayTimeMS)  // prüfen auf Zeitablauf
      {
        melody.stop();                           
        state = State::SIT_DOWN;
        Serial.println(F("--> SIT_DOWN"));
        previousMillis = currentMillis;          // verwenden wir auch für den Timeout zum Niedersetzen
      }

      break;

    case State::SIT_DOWN :                      // Musik ist aus, die Spielen sollen sich auf freien Plätzen niedersetzen
      for (size_t i = 0; i < noOfPlayer; i++)   // prüfen ob ein aktiver Spieler gedrückt hat
      {
        if (player[i].getState() == true && player[i].getSits() == false && player[i].pressed() == true)
        {
          Serial.print(F("player has seated, player=")); Serial.println(i);
          player[i].setSits(true);
          currentPresses++;
        }
      }

      if (currentPresses == currentRound)       // prüfen ob bis auf einen alle Spieler gedrückt haben
      {
        Serial.println(F("enough players have pressed"));
        // wer steht noch rum?
        for (size_t i = 0; i < noOfPlayer; i++)
        {
          if (player[i].getState() == true && player[i].getSits() == false)
          {
            player[i].setState(false); // Spieler rauswerfen
          }
        }
        state = State::WAIT_FOR_CLEAR;
        Serial.println(F("--> WAIT_FOR_CLEAR"));
      }

      if (currentMillis - previousMillis > sitdownMax * 1000UL)   // Zeit ist abgelaufen
      {
        Serial.println(F("timeout"));
        state = State::WAIT_FOR_CLEAR;
        Serial.println(F("--> WAIT_FOR_CLEAR"));
      }
      break;

    case State::WAIT_FOR_CLEAR :                           // warten, bis Clear gedrückt wird
      if (digitalRead(clearPin) == LOW)                    // checken ob Neustart gedrückt wird
      {
        currentRound--;                                    // wir zählen um eine Runde runter
        if (currentRound == 0)                             // sollten wir in Runde 0 angelangt sein, fangen wir ein neues Spiel an
        {
          Serial.println(F("start new game"));
          currentRound = noOfPlayer - 1;                   // minus 1 weil wir einen Spieler je Runde rauswerfen
          for (auto &i : player) i.setState(true);         // alle Spieler wieder aktivieren
        }
        else
        {
          Serial.println(F("start new round"));
        }
        Serial.print(F("currentRound=")); Serial.println(currentRound);
        for (auto &i : player) i.setSits(false);           // alle Spieler sollen auf stehen (= sie sitzen nicht mehr)
        currentPresses = 0;                                // noch hat keiner gedrückt
        currentPlayTimeMS = random(walkAroundMin * 1000UL, walkAroundMax * 1000UL); // Laufzeitzeit für Melodie festlegen
        previousMillis = currentMillis;                    // aktuelle Zeit zurücksetzen
        melody.start();                                    // Melodie einschalten
        state = State::WALK_AROUND;
        Serial.println(F("--> WALK_AROUND"));
      }
      break;
  }
}