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;
  }
};

// Blinker to provide a heartbeat signal.
class Blinker
{
private:
  Timer timer;
  const uint32_t timespan = 500;
  byte pin;

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

  void Begin()
  {
    pinMode(pin, OUTPUT);
    timer.Start(timespan);
  }

  void Update()
  {
    if (timer.Expired())
    {
      digitalWrite(pin, !digitalRead(pin));
      timer.Start(timespan);
    }
  }
};

// Prompter to control blinking and beeping.
class Prompter
{
private:
  Timer timer;
  uint32_t toggleTimespan;
  uint32_t pauseTimespan;
  uint32_t numToggles;
  uint32_t currentToggles;
  bool indefinitely;
  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;
    indefinitely = false;
    state = LOW;
    timer.Start(toggleTimespan);
    running = true;
  }

  void StartIndefinitely(uint32_t toggleTimespan)
  {
    this->toggleTimespan = toggleTimespan;
    indefinitely = true;
    state = LOW;
    timer.Start(toggleTimespan);
    running = true;
  }

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

  void Update()
  {
    if (running && timer.Expired())
    {
      currentToggles++;
      if (!indefinitely && 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;
  }

  bool IsRunning() const
  {
    return running;
  }
};

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 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);
  }
};

// Blinker.
Blinker blinker(LED_BUILTIN);

const byte NUM_PLAYERS = 6;

// The players.
Player players[NUM_PLAYERS] =
{
  // Button pin, debounce delay, LED pin.
  Player(A0, 50, 2),
  Player(A1, 50, 3),
  Player(A2, 50, 4),
  Player(A3, 50, 5),
  Player(A4, 50, 6),
  Player(A5, 50, 7)
};

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

// State machine states.
enum class States : byte
{
  S00_WAIT_FOR_FIRST_DEALER,
  S01_FOUND_FIRST_DEALER,
  S02_PROMPT_DEALER,
  S03_WAIT_FOR_DEALER,
  S04_PROMPT_NEXT_PLAYER,
  S05_WAIT_FOR_PLAYER,
  S06_ACKNOWLEDGE_PLAYER,
  S07_MOVE_TO_NEXT_PLAYER,
};

States presentState;
byte currentDealerIndex;
byte currentPlayerIndex;
byte currentRoundIndex;
byte cardsPlayed;

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

// These values are cumulative, i.e. not cards per round.
// const byte CARDS_PLAYED_AFTER_ROUND[NUM_ROUNDS] = { 18, 42, 72, 108 };  // For game play.
const byte CARDS_PLAYED_AFTER_ROUND[NUM_ROUNDS] = {6, 12, 18, 24}; // For testing.

const uint16_t DEALER_FREQUENCY = 1000;
const uint16_t PLAYER_FREQUENCY = 1500;
const uint16_t CARD_FREQUENCY = 500;

//const uint32_t PLAYER_TIMER = 15 * 1000;  // For game play.
const uint32_t PLAYER_TIMER = 5 * 1000;  // For testing.

// Limits the number of beeps used to prompt the Dealer.
byte performSpeakerPrompt;

void PrompterCallback(byte prompterState)
{
  switch (presentState)
  {
  case States::S00_WAIT_FOR_FIRST_DEALER:
    for (auto &player : players)
    {
      player.led.Set(prompterState);
    }
    break;
  
  case States::S03_WAIT_FOR_DEALER:
    if (prompterState == HIGH)
    {
      if (performSpeakerPrompt > 0)
      {
        speaker.On(CARD_FREQUENCY);
        performSpeakerPrompt--;
      }
      players[currentDealerIndex].led.On();
    }
    else
    {
      speaker.Off();
      players[currentDealerIndex].led.Off();
    }
    break;

  case States::S05_WAIT_FOR_PLAYER:
    players[currentPlayerIndex].led.Set(prompterState);
    break;
  }
}

Prompter prompter(PrompterCallback);

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

  // Initialise.
  blinker.Begin();
  presentState = States::S00_WAIT_FOR_FIRST_DEALER;
  for (auto &player : players)
  {
    player.Begin();
  }
  speaker.Begin();

  // Blink all player LEDs until one player presses their button.
  prompter.StartIndefinitely(250);

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

Timer promptTimer;

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

  //
  // Next state logic.
  //

  // The next state defaults to the present state;
  States nextState = presentState;

  switch (presentState)
  {
  case States::S00_WAIT_FOR_FIRST_DEALER:
    for (byte i = 0; i < NUM_PLAYERS; i++)
    {
      if (players[i].button.Fall())
      {
        currentDealerIndex = i;
        currentPlayerIndex = (currentDealerIndex + 1) % NUM_PLAYERS;
        nextState = States::S01_FOUND_FIRST_DEALER;
        break;
      }
    }
    break;
  
  case States::S01_FOUND_FIRST_DEALER:
    nextState = States::S02_PROMPT_DEALER;
    break;

  case States::S02_PROMPT_DEALER:
    if (promptTimer.Expired())
    {
      nextState = States::S03_WAIT_FOR_DEALER;
    }
    break;

  case States::S03_WAIT_FOR_DEALER:
    if (players[currentDealerIndex].button.Fall())
    {
      currentPlayerIndex = (currentDealerIndex + 1) % NUM_PLAYERS;
      nextState = States::S04_PROMPT_NEXT_PLAYER;
    }
    break;

  case States::S04_PROMPT_NEXT_PLAYER:
    if (promptTimer.Expired())
    {
      nextState = States::S05_WAIT_FOR_PLAYER;
    }
    break;

  case States::S05_WAIT_FOR_PLAYER:
    if (!prompter.IsRunning() && promptTimer.Expired())
    {
      Serial.println("    Player timer expired.");
      prompter.StartIndefinitely(250);
    }

    if (players[currentPlayerIndex].button.Fall())
    {
      cardsPlayed++;
      nextState = States::S06_ACKNOWLEDGE_PLAYER;
    }
    break;
  
  case States::S06_ACKNOWLEDGE_PLAYER:
    nextState = States::S07_MOVE_TO_NEXT_PLAYER;
    break;
  
  case States::S07_MOVE_TO_NEXT_PLAYER:
    nextState = States::S05_WAIT_FOR_PLAYER;
    currentPlayerIndex = ++currentPlayerIndex % NUM_PLAYERS;  // Move to next player.
    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;
      nextState = States::S02_PROMPT_DEALER;
    }
    else
    {
      nextState = States::S04_PROMPT_NEXT_PLAYER;
    }
    break;

  default:
    break;
  }

  if (nextState != presentState)
  {
    // Update the present state with the next state.
    presentState = nextState;

    //
    // Output logic.
    //

    switch (presentState)
    {
    case States::S00_WAIT_FOR_FIRST_DEALER:
      Serial.println("S00_WAIT_FOR_FIRST_DEALER");
      break;

    case States::S01_FOUND_FIRST_DEALER:
      Serial.println("S01_FOUND_FIRST_DEALER");
      Serial.print("    Found the first dealer: Player ");
      Serial.println(currentDealerIndex + 1);
      prompter.Stop();
      for (auto &player : players)
      {
        player.led.Off();
      }
      break;
    
    case States::S02_PROMPT_DEALER:
      Serial.println("S02_PROMPT_DEALER");
      players[currentDealerIndex].led.On();

      // Beep the speaker for 500 ms.
      speaker.On(DEALER_FREQUENCY);  // Prompt the dealer. You can adjust the frequency (Hz) and duration (ms).
      promptTimer.Start(500);
      break;

    case States::S03_WAIT_FOR_DEALER:
      Serial.println("S03_WAIT_FOR_DEALER");
      Serial.print("    Round ");
      Serial.println(currentRoundIndex + 1);
      players[currentDealerIndex].led.Off();
      speaker.Off();
      performSpeakerPrompt = BLINKS_PER_ROUND[currentRoundIndex];  // Limit the number of beeps to one sequence.
      prompter.Start(250, BLINKS_PER_ROUND[currentRoundIndex], 500);  // Blink and beep to prompt the dealer.
      break;
    
    case States::S04_PROMPT_NEXT_PLAYER:
      Serial.println("S04_PROMPT_NEXT_PLAYER");
      prompter.Stop();
      players[currentDealerIndex].led.Off();  // Turn off blinking for current dealer.
      speaker.Off();
      speaker.On(PLAYER_FREQUENCY);
      players[currentPlayerIndex].led.On();  // Light up LED for next player.
      promptTimer.Start(500);
      break;
    
    case States::S05_WAIT_FOR_PLAYER:
      Serial.println("S05_WAIT_FOR_PLAYER");
      speaker.Off();
      promptTimer.Start(PLAYER_TIMER);
      Serial.println("    Started player timer.");
      break;
    
    case States::S06_ACKNOWLEDGE_PLAYER:
      Serial.println("S06_ACKNOWLEDGE_PLAYER");
      Serial.print("    cardsPlayed = ");
      Serial.println(cardsPlayed);
      prompter.Stop();
      players[currentPlayerIndex].led.Off();  // Turn off LED for current player.
      speaker.Off();
      break;
    
    case States::S07_MOVE_TO_NEXT_PLAYER:
      Serial.println("S07_MOVE_TO_NEXT_PLAYER");
      break;

    default:
      break;
    }
  }
}