/* 
DARTS SCORER by AleDre
  Thanks a lot to Anon
  Press "A" for...
  Press "B" for delete last input
  Press "C" for Reset Game
  Press "D" for next leg
  Press "#" for insert score
  Press "*" for correct INPUT
*/
#include <MD_Parola.h>
#include <MD_MAX72xx.h>
#include <SPI.h>
#include <Keypad.h>
#include <Wire.h>
#define HARDWARE_TYPE MD_MAX72XX::FC16_HW
#define MAX_DEVICES 8
#define NUM_ZONES 4
#define CLK_PIN 14
#define DATA_PIN 13
#define CS_PIN 12
#define SPEED_TIME 50
#define PAUSE_TIME 500
#define ROTARY_PIN1 26
#define ROTARY_PIN2 33  //27 on my esp
#define ROTARY_BUTTON 34 //25 on my esp
#define BUZZ_PIN 20
#define DEBOUNCE_DELAY 200
#define RESET_DEBOUNCE_DELAY 100
// Set PWM properties
const int freq = 30000;
const int pwmChannel = 0;
const int resolution = 8;
MD_Parola P = MD_Parola(HARDWARE_TYPE, DATA_PIN, CLK_PIN, CS_PIN, MAX_DEVICES);
// Keypad config
const byte ROWS = 4; 
const byte COLS = 4;
char hexaKeys[ROWS][COLS] = {
  {'1', '2', '3', 'A'},
  {'4', '5', '6', 'B'},
  {'7', '8', '9', 'C'},
  {'*', '0', '#', 'D'}
};
byte rowPins[ROWS] = {21, 19, 18, 5}; // Pin Rows
byte colPins[COLS] = {17, 16, 4, 0}; // Pin Columns
// Keypad Init
Keypad customKeypad = Keypad(makeKeymap(hexaKeys), rowPins, colPins, ROWS, COLS);
enum GameState { SELECT_GAME,
                 ENTER_LEGS,
                 PLAY_GAME,
                 GAME_OVER };
GameState gameState = SELECT_GAME;
int lastEncoded = 0;
unsigned long lastDebounceTime = 0;
int availableScores[] = { 180, 301, 501, 701, 901 };
int playerScores[2] = { 0, 0 };
int legsToWin[2] = { 0, 0 };
int legsWon[2] = { 0, 0 };
int dartsThrown[2] = { 0, 0 };
int scoreIndex = 3;
bool scoreLocked = false;
int currentPlayer = 1;
String currentInput = "";
int previousScore = 0;
int lastPlayer = 0;
bool legFinished = false;
int startingPlayer = 1;
int lastScoreInput = 0;
int prevPlayerScores[2] = { 0, 0 };
int prevLegsWon[2] = { 0, 0 };
int prevDartsThrown[2] = { 0, 0 };
int prevCurrentPlayer = 1;
bool isValidScore(int score) {
  int invalidScores[] = { 179, 178, 176, 175, 173, 172, 169, 166, 163 };
  if (score > playerScores[currentPlayer - 1]) return false;
  if (score > 180) return false;
  for (int i = 0; i < sizeof(invalidScores) / sizeof(invalidScores[0]); i++) {
    if (score == invalidScores[i]) return false;
  }
  return true;
}
void handleRotaryEncoder() {
  int MSB = digitalRead(ROTARY_PIN1);
  int LSB = digitalRead(ROTARY_PIN2);
  int currentEncoded = (MSB << 1) | LSB;
  int sum = (lastEncoded << 2) | currentEncoded;
  unsigned long currentTime = millis();
  char avScores[10];
  char legToWin[10];
  if (gameState == SELECT_GAME) {
    if (currentTime - lastDebounceTime > DEBOUNCE_DELAY) {
      if (sum == 0b1101 || sum == 0b0100 || sum == 0b0010 || sum == 0b1011) {
        scoreIndex = min(scoreIndex + 1, (int)(sizeof(availableScores) / sizeof(availableScores[0]) - 1));
        P.displayZoneText(5, "Game?", PA_CENTER, 0, 0, PA_NO_EFFECT);
        itoa(availableScores[scoreIndex], avScores, 10);
        P.displayZoneText(6, avScores, PA_CENTER, 0, 0, PA_NO_EFFECT);
        P.displayAnimate();
        tone(BUZZ_PIN, 300, 10);
        lastDebounceTime = currentTime;
      } else if (sum == 0b1110 || sum == 0b0111 || sum == 0b0001 || sum == 0b1000) {
        scoreIndex = max(scoreIndex - 1, 0);
        P.displayZoneText(5, "Game?", PA_CENTER, 0, 0, PA_NO_EFFECT);
        itoa(availableScores[scoreIndex], avScores, 10);
        P.displayZoneText(6, avScores, PA_CENTER, 0, 0, PA_NO_EFFECT);
        P.displayAnimate();
        tone(BUZZ_PIN, 300, 10);
        lastDebounceTime = currentTime;
      }
    }
  } else if (gameState == ENTER_LEGS) {
    if (currentTime - lastDebounceTime > DEBOUNCE_DELAY) {
      if (sum == 0b1101 || sum == 0b0100 || sum == 0b0010 || sum == 0b1011) {
        legsToWin[0] = legsToWin[1] = min(legsToWin[0] + 1, 9);
        itoa(legsToWin[1], legToWin, 10);
        P.displayZoneText(6, legToWin, PA_CENTER, 0, 0, PA_NO_EFFECT);
        P.displayZoneText(5, "Legs?", PA_CENTER, 0, 0, PA_NO_EFFECT);
        P.displayAnimate();
        tone(BUZZ_PIN, 300, 10);
        lastDebounceTime = currentTime;
      } else if (sum == 0b1110 || sum == 0b0111 || sum == 0b0001 || sum == 0b1000) {
        legsToWin[0] = legsToWin[1] = max(legsToWin[0] - 1, 1);
        itoa(legsToWin[0], legToWin, 10);
        P.displayZoneText(6, legToWin, PA_CENTER, 0, 0, PA_NO_EFFECT);
        P.displayZoneText(5, "Legs?", PA_CENTER, 0, 0, PA_NO_EFFECT);
        P.displayAnimate();
        tone(BUZZ_PIN, 300, 10);
        lastDebounceTime = currentTime;
      }
    }
  }
  lastEncoded = currentEncoded;
}
void handleButtonPress() {
  bool buttonState = digitalRead(ROTARY_BUTTON) == LOW;
  if (buttonState && millis() - lastDebounceTime > DEBOUNCE_DELAY) {
    if (gameState == SELECT_GAME) {
      gameState = ENTER_LEGS;
      currentInput = "";
      P.displayZoneText(5, "Legs?", PA_CENTER, 0, 0, PA_PRINT);
      P.displayZoneText(6, "#", PA_CENTER, 0, 0, PA_PRINT);
      P.displayAnimate();
      tone(BUZZ_PIN, 400, 10);
    } else if (gameState == ENTER_LEGS && legsToWin[0] > 0) {
      gameState = PLAY_GAME;
      legsWon[0] = legsWon[1] = 0;
      dartsThrown[0] = dartsThrown[1] = 0;
      playerScores[0] = playerScores[1] = availableScores[scoreIndex];
      currentPlayer = 1;
      updateDisplays();
      tone(BUZZ_PIN, 500, 10);
    }
    lastDebounceTime = millis();
  }
}
void handleScore() {
  String ultimoPlayer = String(lastPlayer);
  if (playerScores[currentPlayer - 1] == 0) {
    legsWon[lastPlayer - 1]++;
    legFinished = true;
    int darts = dartsThrown[lastPlayer - 1];
    dartsThrown[0] = dartsThrown[1] = 0;
    char dartsStr[10];
    itoa(darts, dartsStr, 10);
    P.displayClear();
    P.displayZoneText(0, "", PA_CENTER, 0, 0, PA_NO_EFFECT);
    P.displayZoneText(1, ultimoPlayer.c_str(), PA_CENTER, 0, 0, PA_NO_EFFECT);
    P.displayAnimate();
  }
}
void playMelody() {
  int melody[] = {262, 294, 330, 349, 392, 440, 494}; // Frequenze delle note
  int noteDurations[] = {100, 50, 50, 50, 50, 50, 50}; // Durata delle note in ms
  int pauseBetweenNotes = 50; // Pausa tra le note in ms
  for (int i = 0; i < 7; i++) {
    tone(BUZZ_PIN, melody[i], noteDurations[i]);
    delay(noteDurations[i] - pauseBetweenNotes); // Pausa tra le note
    noTone(BUZZ_PIN); // Arresta la riproduzione della nota prima di riprodurne un'altra
    delay(pauseBetweenNotes); // Pausa tra le note
  }
}
void playMelodyLeg() {
  int melody[] = {262, 392, 262, 392, 262, 393, 494}; // Frequenze delle note
  int noteDurations[] = {100, 50, 50, 50, 50, 50, 50}; // Durata delle note in ms
  int pauseBetweenNotes = 50; // Pausa tra le note in ms
  for (int i = 0; i < 7; i++) {
    tone(BUZZ_PIN, melody[i], noteDurations[i]);
    delay(noteDurations[i] - pauseBetweenNotes); // Pausa tra le note
    noTone(BUZZ_PIN); // Arresta la riproduzione della nota prima di riprodurne un'altra
    delay(pauseBetweenNotes); // Pausa tra le note
  }
}
void playMelodySet() {
  int melody[] = {262, 392, 262, 392, 262, 393, 494}; // Frequenze delle note
  int noteDurations[] = {100, 50, 50, 50, 50, 50, 50}; // Durata delle note in ms
  int pauseBetweenNotes = 50; // Pausa tra le note in ms
  for (int i = 0; i < 7; i++) {
    tone(BUZZ_PIN, melody[i], noteDurations[i]);
    delay(noteDurations[i] - pauseBetweenNotes); // Pausa tra le note
    noTone(BUZZ_PIN); // Arresta la riproduzione della nota prima di riprodurne un'altra
    delay(pauseBetweenNotes); // Pausa tra le note
  }
}
void handleKeypad() {
  char key = customKeypad.getKey();
  String correntePlayer = String(currentPlayer);
  if (key) {
    tone(BUZZ_PIN, 500, 10);
    if (key == 'D') {
      if (gameState == PLAY_GAME && legFinished) {
        legFinished = false;
        playerScores[0] = playerScores[1] = availableScores[scoreIndex];
        dartsThrown[0] = dartsThrown[1] = 0;
        startingPlayer = 3 - startingPlayer;
        currentPlayer = startingPlayer;
        updateDisplays();
      }
    } else if (key == '*') {
      if (gameState == PLAY_GAME) {
        currentInput = "";
        displayCurrentInput();
        updateDisplays();
      }
    } else if (key == '#') {
      if (gameState == PLAY_GAME && currentInput.length() > 0) {
        int score = currentInput.toInt();
        if (isValidScore(score)) {
          // Memorizza lo stato precedente
          prevPlayerScores[0] = playerScores[0];
          prevPlayerScores[1] = playerScores[1];
          prevLegsWon[0] = legsWon[0];
          prevLegsWon[1] = legsWon[1];
          prevDartsThrown[0] = dartsThrown[0];
          prevDartsThrown[1] = dartsThrown[1];
          prevCurrentPlayer = currentPlayer;
          previousScore = score;
          lastPlayer = currentPlayer;
          playerScores[currentPlayer - 1] -= score;
          dartsThrown[currentPlayer - 1] += 3;
          lastScoreInput = currentInput.toInt();
          currentInput = "";
          currentPlayer = currentPlayer % 2 + 1;
          updateDisplays();
          if (playerScores[lastPlayer - 1] == 0) {
            legsWon[lastPlayer - 1]++;
            legFinished = true;
            if (legsWon[lastPlayer - 1] == legsToWin[0]) {
              gameState = GAME_OVER;
              P.displayClear();
              int darts = dartsThrown[lastPlayer - 1];
              dartsThrown[0] = dartsThrown[1] = 0; // Reset for the next leg
              char dartsStr[10];
              char lastScoreStr[10];
              itoa(darts, dartsStr, 10);
              itoa(lastScoreInput, lastScoreStr, 10); 
              char displayStr[30]; // Buffer to hold the combined string
              snprintf(displayStr, sizeof(displayStr), "%s:%s", dartsStr, lastScoreStr);
              P.displayZoneText(1, correntePlayer.c_str(), PA_CENTER, 0, 0, PA_PRINT);
              P.displayZoneText(0, "SET!", PA_CENTER, 0, 0, PA_PRINT);
              P.displayZoneText(6, displayStr, PA_CENTER, 0, 0, PA_PRINT);
              P.displayAnimate();
              playMelodySet();
            } else {
              P.displayClear();
              int darts = dartsThrown[lastPlayer - 1];
              dartsThrown[0] = dartsThrown[1] = 0; // Reset for the next leg
              char dartsStr[10];
              char lastScoreStr[10];
              itoa(darts, dartsStr, 10);
              itoa(lastScoreInput, lastScoreStr, 10); 
              char displayStr[30]; // Buffer to hold the combined string
              snprintf(displayStr, sizeof(displayStr), "%s:%s", dartsStr, lastScoreStr);
              P.displayZoneText(0, "LEG!", PA_CENTER, 0, 0, PA_PRINT);
              P.displayZoneText(1, correntePlayer.c_str(), PA_CENTER, 0, 0, PA_PRINT);
              P.displayZoneText(6, displayStr, PA_CENTER, 0, 0, PA_PRINT);
              P.displayAnimate();
              playMelodyLeg();
            }
          }
        } else {
          currentInput = "";
        }
      }
    } else if (key == 'B') {
      if (gameState == PLAY_GAME) {
        // Ripristina lo stato precedente
        playerScores[0] = prevPlayerScores[0];
        playerScores[1] = prevPlayerScores[1];
        legsWon[0] = prevLegsWon[0];
        legsWon[1] = prevLegsWon[1];
        dartsThrown[0] = prevDartsThrown[0];
        dartsThrown[1] = prevDartsThrown[1];
        currentPlayer = prevCurrentPlayer;
        updateDisplays();
      }
    } else if (key == 'C') {    
      if (gameState == GAME_OVER || gameState == PLAY_GAME || gameState == ENTER_LEGS || gameState == SELECT_GAME) {
        gameState = SELECT_GAME;
        currentInput = "";
        P.displayClear();
        P.displayZoneText(5, "Game", PA_CENTER, SPEED_TIME, PAUSE_TIME, PA_OPENING);
        P.displayZoneText(6, "Over", PA_CENTER, SPEED_TIME, PAUSE_TIME, PA_OPENING);
        P.displayAnimate();
      }
    } else {
      if (gameState == PLAY_GAME && currentInput.length() < 3) {
        switch (key) {
          case '1':
            currentInput += '1';
            break;
          case '2':
            currentInput += '2';
            break;
          case '3':
            currentInput += '3';
            break;
          case '4':
            currentInput += '4';
            break;
          case '5':
            currentInput += '5';
            break;
          case '6':
            currentInput += '6';
            break;
          case '7':
            currentInput += '7';
            break;
          case '8':
            currentInput += '8';
            break;
          case '9':
            currentInput += '9';
            break;
          case '0':
            currentInput += '0';
            break;
          default:
            break;
        }
        displayCurrentInput();
      }
    }
  }
}
void updateDisplays() {
  char legWonP1[10];
  char legWonP2[10];
  char playerScoresP1[10];
  char playerScoresP2[10];
  P.displayClear();
  itoa(legsWon[0], legWonP1, 10);
  if (currentPlayer == 1) {
    strcat(legWonP1, ":");
  }
  P.displayZoneText(1, legWonP1, PA_LEFT, 0, 0, PA_NO_EFFECT);           //Player1 LEGS
  itoa(playerScores[0], playerScoresP1, 10);
  P.displayZoneText(0, playerScoresP1, PA_CENTER, 0, 0, PA_NO_EFFECT);   //Player1 SCORE
  itoa(legsWon[1], legWonP2, 10);
  if (currentPlayer == 2) {
    strcat(legWonP2, ":");
  }
  P.displayZoneText(3, legWonP2, PA_LEFT, 0, 0, PA_NO_EFFECT);           //Player2 LEGS
  itoa(playerScores[1], playerScoresP2, 10);
  P.displayZoneText(2, playerScoresP2, PA_CENTER, 0, 0, PA_NO_EFFECT);   //Player2 SCORE
  P.displayAnimate();
}
void displayCurrentInput() {
  String curScorInput = String(currentInput);
  // Se il currentPlayer è 1, visualizza il punteggio corrente su P1
  if (currentPlayer == 1) {
    P.displayClear(0);
    P.displayZoneText(0, curScorInput.c_str(), PA_CENTER, 0, 0, PA_NO_EFFECT);
    P.displayAnimate();
  }
  // Se il currentPlayer è 2, visualizza il punteggio corrente su P2
  else if (currentPlayer == 2) {
    P.displayClear(2);
    P.displayZoneText(2, curScorInput.c_str(), PA_CENTER, 0, 0, PA_NO_EFFECT);
    P.displayAnimate();
  }
}
void showSplash() {
  P.displayZoneText(5, "D===-", PA_CENTER, 10, 0, PA_SLICE);
  P.displayZoneText(6, "GameOn", PA_CENTER, 10, 0, PA_SLICE);
  P.displayAnimate();
  playMelody();
}
void showSplash2() {
  P.displayZoneText(5, "by", PA_CENTER, 10, 0, PA_SLICE);
  P.displayZoneText(6, "AleDre", PA_CENTER, 10, 0, PA_SLICE);
  P.displayAnimate();
}
void setup() {
  Serial.begin(115200);
  pinMode(ROTARY_PIN1, INPUT_PULLUP);
  pinMode(ROTARY_PIN2, INPUT_PULLUP);
  pinMode(ROTARY_BUTTON, INPUT_PULLUP);
  ledcAttachPin(BUZZ_PIN, pwmChannel);
  ledcSetup(pwmChannel, freq, resolution);
  P.begin(7);
  P.setZone(0, 0, 2); //AleDre P1 Score
  P.setZone(1, 3, 3); //AleDre P1 Legs
  P.setZone(2, 4, 6); //AleDre P2 Score
  P.setZone(3, 7, 7); //AleDre P2 Legs
  P.setZone(4, 0, 7); //AleDre All Displays
  P.setZone(5, 0, 3); //AleDre P1 All Display
  P.setZone(6, 4, 7); //AleDre P2 All Display
  P.setIntensity(15);
  P.setInvert(false);
  showSplash();
}
void loop() {
  if (gameState == SELECT_GAME || gameState == ENTER_LEGS) {
    handleRotaryEncoder();
    P.displayAnimate();
    handleButtonPress();
    P.displayAnimate();
  } else if (gameState == PLAY_GAME || gameState == GAME_OVER) {
    handleKeypad();
    P.displayAnimate();
  }
}