class Timer
{
private:
  uint32_t timestamp;
  uint32_t timespan;

public:
  Timer()
  {
  }

  void Start(uint32_t timespan)
  {
    this->timespan = timespan;
    timestamp = millis();
  }

  bool Expired() const
  {
    return (millis() - timestamp) >= timespan;
  }
};

// Prompter to control blinking.
class Prompter
{
private:
  Timer timer;
  uint32_t toggleTimespan;
  uint32_t pauseTimespan;
  uint32_t numToggles;
  uint32_t currentToggles;
  bool running;
  byte state;

public:
  Prompter(void (*function)(byte))
  {
    Callback = function;
  }

  void (*Callback)(byte);

  void Start(uint32_t toggleTimespan, uint16_t numBlinks, uint32_t pauseTimespan)
  {
    this->toggleTimespan = toggleTimespan;
    numToggles = 2 * numBlinks;
    currentToggles = 0;
    this->pauseTimespan = pauseTimespan;
    state = LOW;
    timer.Start(toggleTimespan);
    running = true;
  }

  void Stop()
  {
    running = false;
    currentToggles = 0;
  }

  void Update()
  {
    if (running && timer.Expired())
    {
      currentToggles++;
      if (currentToggles > numToggles)
      {
        state = LOW;
        timer.Start(pauseTimespan);
        currentToggles = 0;
      }
      else
      {
        state = state == HIGH ? LOW : HIGH;
        timer.Start(toggleTimespan);
      }
      if (Callback != nullptr)
      {
        Callback(state);
      }
    }
  }

  byte State() const
  {
    return state;
  }
};

class Debouncer
{
private:
  Timer timer;
  uint32_t debounceDelay;
  byte pin;
  byte state;
  bool fall;

public:
  Debouncer(byte pin, uint32_t debounceDelay)
  {
    this->pin = pin;
    this->debounceDelay = debounceDelay;
  }

  void Begin()
  {
    pinMode(pin, INPUT_PULLUP);
    state = digitalRead(pin);
  }

  byte State() const
  {
    return state;
  }

  bool Fall() const
  {
    return fall;
  }

  void Update()
  {
    const bool newState = digitalRead(pin);
    // Hysteresis:
    //   If there is no change, reset the debounce timer.
    //   Else, compare the time difference with the debounce delay.
    if (newState == state)
    {
      timer.Start(debounceDelay);
    }
    else if (timer.Expired())
    {
      // Successfully debounced, so reset the debounce timer and update the state.
      fall = !newState && state;
      state = newState;
      timer.Start(debounceDelay);
      return;
    }
    fall = false;
  }
};

class Led
{
private:
  byte pin;

public:
  Led(byte pin)
  {
    this->pin = pin;
  }

  void Begin()
  {
    pinMode(pin, OUTPUT);
  }

  void On()
  {
    digitalWrite(pin, HIGH);
  }

  void Off()
  {
    digitalWrite(pin, LOW);
  }

  void Set(byte state)
  {
    digitalWrite(pin, state);
  }
};

class RgbLed
{
private:
  byte redPin;
  byte greenPin;
  byte bluePin;

public:
  RgbLed(byte redPin, byte greenPin, byte bluePin)
  {
    this->redPin = redPin;
    this->greenPin = greenPin;
    this->bluePin = bluePin;
  }

  void Begin()
  {
    pinMode(redPin, OUTPUT);
    pinMode(greenPin, OUTPUT);
    pinMode(bluePin, OUTPUT);
  }

  void RedOn()
  {
    digitalWrite(redPin, HIGH);
  }

  void GreenOn()
  {
    digitalWrite(greenPin, HIGH);
  }

  void BlueOn()
  {
    digitalWrite(bluePin, HIGH);
  }

  void YellowOn()
  {
    digitalWrite(redPin, HIGH);
    digitalWrite(greenPin, HIGH);
    digitalWrite(bluePin, LOW);
  }

  void Off()
  {
    digitalWrite(redPin, LOW);
    digitalWrite(greenPin, LOW);
    digitalWrite(bluePin, LOW);
  }
};

class Dealer
{
public:
  Debouncer button;
  RgbLed rgbLed;

  Dealer(byte buttonPin, uint32_t buttonDebounceDelay, byte redPin, byte greenPin, byte bluePin) : button(buttonPin, buttonDebounceDelay), rgbLed(redPin, greenPin, bluePin)
  {
  }

  void Begin()
  {
    button.Begin();
    rgbLed.Begin();
  }

  void On(byte dealerIndex)
  {
    rgbLed.Off();
    switch (dealerIndex)
    {
    case 0:
      rgbLed.RedOn();
      break;
    case 1:
      rgbLed.BlueOn();
      break;
    case 2:
      rgbLed.YellowOn();
      break;
    case 3:
      rgbLed.GreenOn();
      break;
    default:
      break;
    }
  }

  void Off()
  {
    rgbLed.Off();
  }
};

class Player
{
public:
  Debouncer button;
  Led led;

  Player(byte buttonPin, uint32_t buttonDebounceDelay, byte ledPin) : button(buttonPin, buttonDebounceDelay), led(ledPin)
  {
  }

  void Begin()
  {
    button.Begin();
    led.Begin();
  }
};

class Speaker
{
private:
  byte pin;

public:
  Speaker(byte pin)
  {
    this->pin = pin;
  }

  void Begin()
  {
    pinMode(pin, OUTPUT);
  }

  void On(uint16_t frequency)
  {
    tone(pin, frequency);
  }

  void Off()
  {
    noTone(pin);
  }
};

// The Dealer.
// Button pin, debounce delay, red LED pin, green LED pin, blue LED pin.
Dealer dealer(A5, 50, 10, 11, 12);

const byte NUM_PLAYERS = 4;

// The players.
Player players[NUM_PLAYERS] =
{
  // Button pin, debounce delay, LED pin.
  Player(6, 50, 2),
  Player(7, 50, 3),
  Player(8, 50, 4),
  Player(9, 50, 5)
};

// Speaker to prompt next action.
const byte SPEAKER_PIN = A3;
Speaker speaker(SPEAKER_PIN);

// State machine states.
enum class States : byte
{
  S00_WAIT_FOR_FIRST_DEALER,
  S01_ACKNOWLEDGE_FIRST_DEALER,
  S02_WAIT_FOR_DEALER,
  S03_WAIT_FOR_PLAYERS,
};

States state;
byte currentDealerIndex;
byte currentPlayerIndex;
byte currentRoundIndex;
byte cardsPlayed;

const byte NUM_ROUNDS = 3;
const byte BLINKS_PER_ROUND[NUM_ROUNDS] = {3, 4, 6};

// These values are cumulative, i.e. not cards per round.
// const byte CARDS_PLAYED_AFTER_ROUND[NUM_ROUNDS] = { 18, 28, 52 };  // For game play.
const byte CARDS_PLAYED_AFTER_ROUND[NUM_ROUNDS] = {4, 8, 12}; // For testing.

void PrompterCallback(byte prompterState)
{
  switch (state)
  {
  case States::S00_WAIT_FOR_FIRST_DEALER:
    for (auto &player : players)
    {
      player.led.Set(prompterState);
    }
    break;

  case States::S02_WAIT_FOR_DEALER:
    if (prompterState == HIGH)
    {
      dealer.On(currentDealerIndex);
    }
    else
    {
      dealer.Off();
    }
  }
}

Prompter prompter(PrompterCallback);

void setup()
{
  Serial.begin(115200);

  // Initialise.
  state = States::S00_WAIT_FOR_FIRST_DEALER;
  dealer.Begin();
  for (auto &player : players)
  {
    player.Begin();
  }
  speaker.Begin();
  prompter.Start(250, 65000, 250);

  Serial.println("Waiting for the first dealer.");
}

Timer acknowledgmentTimer;

void loop()
{
  // Update button states and blinking states.
  dealer.button.Update();
  for (auto &player : players)
  {
    player.button.Update();
  }
  prompter.Update();

  // State machine.
  switch (state)
  {
  case States::S00_WAIT_FOR_FIRST_DEALER:
    for (byte i = 0; i < NUM_PLAYERS; i++)
    {
      if (players[i].button.Fall())
      {
        currentDealerIndex = i;
        Serial.print("Found the first dealer: Player ");
        Serial.println(currentDealerIndex + 1);

        currentPlayerIndex = (currentDealerIndex + 1) % NUM_PLAYERS;

        prompter.Stop();

        for (auto &player : players)
        {
          player.led.Off();
        }

        dealer.On(currentDealerIndex);
        players[currentDealerIndex].led.On();

        // Beep the speaker for 500 ms.
        speaker.On(500); // Dealer Alert Deal Last Card Down. You can adjust the frequency (Hz) and duration (ms).
        acknowledgmentTimer.Start(500);

        state = States::S01_ACKNOWLEDGE_FIRST_DEALER;
        break;
      }
    }
    break;

  case States::S01_ACKNOWLEDGE_FIRST_DEALER:
    if (acknowledgmentTimer.Expired())
    {
      dealer.rgbLed.Off();
      players[currentDealerIndex].led.Off();
      speaker.Off();
      prompter.Start(250, BLINKS_PER_ROUND[currentRoundIndex], 500);
      state = States::S02_WAIT_FOR_DEALER;
    }
    break;

  case States::S02_WAIT_FOR_DEALER:
    if (dealer.button.Fall())
    {
      currentPlayerIndex = (currentDealerIndex + 1) % NUM_PLAYERS;
      prompter.Stop();
      dealer.Off(); // Turn off the RGB LED here.
      speaker.Off();
      players[currentPlayerIndex].led.On();
      Serial.print("Round ");
      Serial.println(currentRoundIndex + 1);
      state = States::S03_WAIT_FOR_PLAYERS;
    }
    break;

  case States::S03_WAIT_FOR_PLAYERS:
    Player *currentPlayer = &players[currentPlayerIndex];
    if (currentPlayer->button.Fall())
    {
      currentPlayer->led.Off();
      currentPlayerIndex = ++currentPlayerIndex % NUM_PLAYERS;
      currentPlayer = &players[currentPlayerIndex];
      cardsPlayed++;
      Serial.print("cardsPlayed=");
      Serial.println(cardsPlayed);
      if (cardsPlayed >= CARDS_PLAYED_AFTER_ROUND[currentRoundIndex])
      {
        Serial.print("End of round ");
        Serial.println(currentRoundIndex + 1);
        currentRoundIndex++;
        if (currentRoundIndex >= NUM_ROUNDS)
        {
          currentDealerIndex = ++currentDealerIndex % NUM_PLAYERS;
          currentRoundIndex = 0;
          cardsPlayed = 0;
        }
        currentRoundIndex = currentRoundIndex % NUM_ROUNDS;
        prompter.Start(250, BLINKS_PER_ROUND[currentRoundIndex], 500); // Current Dealer Alert Round N. You can adjust the timespans (ms) and number of blinks.
        state = States::S02_WAIT_FOR_DEALER;
      }
      else
      {
        currentPlayer->led.On();
      }
    }
    break;

  default:
    break;
  }
}