// The BLA game.
// By Koepel.
// Public Domain
//
// 25 September 2024
// Version 1, it seems to work.
// 
// A project to celibrate the historic event that
// someone made 1000 posts on the Wokwi Discord
// channel.

#include <Wire.h>
#include <hd44780.h>
#include <hd44780ioClass/hd44780_I2Cexp.h>

int signalLedPin = 2;
int buzzerPin = 3;
int hackLedPin = 4;

int rows = 4;
int cols = 20;

hd44780_I2Cexp lcd;

// Variables for the BLA game
byte occupied[4][20];
char blaText[] = "BLA";
int blaCount;
int printCount;
int printCountMax;

// A buffer and index for serial input
char buffer[20];
int index;

// The score is how many times in a row the answer is right.
unsigned long score;

// Special mode, score increases much more and any answer is right.
bool hackMode = false;

unsigned long previousMillis;
unsigned long previousMillisInactivity;
unsigned long inactivityTime = 1000UL * 60UL * 2UL; // 2 minutes
unsigned long previousMillisBlink;
const unsigned long intervalBlink = 800;
bool Blink;

enum
{
  IDLE,
  START,
  WAIT_BEFORE_RUN,
  RUNNING,
  PREPARE_FOR_INPUT,
  INPUT_NUMBER,
} state;

void setup() 
{
  Serial.begin(115200);
  Serial.println(F("Welcome to the \"BLA\" game."));

  pinMode(signalLedPin,OUTPUT);
  pinMode(hackLedPin,OUTPUT);
  // note that pinMode is not needed for tone() on buzzerPin.

  randomSeed(noiseToRandom());

  lcd.begin(cols, rows);

  score = 0UL;
  state = IDLE;
}

void loop() 
{
  unsigned long currentMillis = millis();

  switch(state)
  {
    case IDLE:
      // The game is not started by a button.
      // Continue to start.
      state = START;
      break;
    case START:
      lcd.clear();
      lcd.setCursor(3,0);
      lcd.print(F("The \"BLA\" game"));
      lcd.setCursor(2,1);
      lcd.print(F("Count the \"BLA\"."));
      lcd.setCursor(0,3);
      lcd.print(F("Score = "));
      lcd.print(score);

      // Check if there was inactivity for a long time.
      if(currentMillis - previousMillisInactivity >= inactivityTime)
      {
        previousMillisInactivity = currentMillis; // reset timer
        playInactivityMelody();
      }

      previousMillis = currentMillis;
      state = WAIT_BEFORE_RUN;
      break;
    case WAIT_BEFORE_RUN:
      if(currentMillis - previousMillis >= 3000UL)
      {
        tone(buzzerPin,400,150); // start tone
        lcd.clear();
        printCount = 0;
        printCountMax = random(5,15);
        blaCount = 0;
        clearOccupied();
        previousMillis = currentMillis;
        state = RUNNING;
      }
      break;
    case RUNNING:
      if(currentMillis - previousMillis >= 1000UL)
      {
        previousMillis = currentMillis;

        if(printSomething())
          blaCount++;

        printCount++;
        if(printCount > printCountMax)
        {
          state = PREPARE_FOR_INPUT;
        }
      }
      break;
    case PREPARE_FOR_INPUT:
      tone(buzzerPin,1850,120); // input a number tone
      Serial.println(F("Enter the result within 5 seconds"));
      previousMillis = currentMillis;
      // clear the buffer for serial input
      index = 0;
      buffer[0] = '\0';
      // Remove everything from the Serial input.
      while( Serial.available() > 0)
        Serial.read();
      digitalWrite(signalLedPin,HIGH);
      state = INPUT_NUMBER;
      break;
    case INPUT_NUMBER:
      if(currentMillis - previousMillis >= 5000UL)
      {
        // Timeout.
        Serial.print(F("Timeout! "));
        Serial.print(F("There were "));
        Serial.print(blaCount);
        Serial.print(F(" \"BLA\"."));
        Serial.println();
        digitalWrite(signalLedPin,LOW);
        // A timeout is wrong, so the score is reset.
        score = 0UL;

        // Play a timeout tune
        for(float f=800.0; f>50.0; f*=0.85)
        {
          tone(buzzerPin,(int)f);
          delay(30 + int(f/10.0));
        }
        noTone(buzzerPin);

        state = IDLE;
      }

      if(readInput())
      {
        // something was entered.

        // Reset the inactivity checker
        previousMillisInactivity = currentMillis;

        // Check first the command for the hack mode.
        // Once the hack mode is set, skip the check.
        if(strncmp(buffer,"BLA",3)==0)
        {
          if(!hackMode)
          {
            hackMode = true;
            Serial.println(F("Special hack mode activated."));
          }
          else
          {
            hackMode = false;
            Serial.println(F("Hack mode turned off."));
          }
          digitalWrite(signalLedPin,LOW);
          state = IDLE;
        }
        else
        {
          // Get the number that was entered.
          // If it is not numbers, then atoi() will return zero.
          int answer = atoi(buffer);
          if(answer == blaCount || hackMode)
          {
            Serial.print(F("Correct. "));
            Serial.print(F("There were indeed "));
            Serial.print(blaCount);
            if(hackMode)
            {
              Serial.print(F(" or "));
              Serial.print(answer);
            }
            Serial.print(F(" \"BLA\"."));
            Serial.println();
            // The score is increased.
            score++;

            if(hackMode)
              score += random(0,1000);

            // Play scoring tune
            for(int i=0; i<3; i++)
            {
              for(float f=650.0; f<3000; f*=1.5)
              {
                tone(buzzerPin,(int)f);
                delay(40);
              }
            }
            noTone(buzzerPin);
          }
          else
          {
            Serial.print(F("Wrong. "));
            Serial.print(F("You entered "));
            Serial.print(answer);
            Serial.print(F(", but it was "));
            Serial.print(blaCount);
            Serial.print(F("."));
            Serial.println();
            // Bad answer, the score is reset.
            score = 0UL;

            // Play bad input tune
            tone(buzzerPin,660);
            delay(100);
            tone(buzzerPin,440);
            delay(200);
            noTone(buzzerPin);
          }
          digitalWrite(signalLedPin,LOW);
          state = IDLE;
        }
      }
      break;
    default:
      Serial.println(F("Bug"));
      break;
  }

  // Millis timer to blink the blue led
  if(hackMode)
  {
    if(currentMillis - previousMillisBlink >= intervalBlink)
    {
      previousMillisBlink = currentMillis;

      if(Blink)
      {
        digitalWrite(hackLedPin,LOW);
        Blink = false;
      }
      else
      {
        digitalWrite(hackLedPin,HIGH);
        Blink = true;
      }
    }
  }
}

// The function printSomething()
// might print "BLA" or something else.
bool printSomething()
{
  bool itWasBla = false;
  bool success = false;

  for(int tries=0; tries<10 and !success; tries++)
  {
    int r = random(0,rows);
    int c = random(0,cols-2);

    if(occupied[r][c] == 0 and occupied[r][c+1] == 0 and occupied[r][c+2] == 0)
    {
      char outputText[4];

      if(random(0,2) == 0)
      {
        strcpy(outputText,blaText);
      }
      else
      {
        outputText[0] = blaText[random(0,3)];
        outputText[1] = blaText[random(0,3)];
        outputText[2] = blaText[random(0,3)];
        outputText[3] = '\0';
      }

      lcd.setCursor(c,r);
      lcd.print(outputText);

      if(strncmp(outputText,blaText,3)==0)
        itWasBla = true;

      occupied[r][c] = 1;
      occupied[r][c+1] = 1;
      occupied[r][c+2] = 1;

      success = true;
    }
  }
  return(itWasBla);
}

void clearOccupied()
{
  // A single memset() would do.
  for(int r=0; r<rows; r++)
    for(int c=0; c<cols; c++)
      occupied[r][c] = 0; 
}

bool readInput()
{
  bool dataEntered = false;

  if( Serial.available() > 0)          // new data received ?
  {
    int inChar = Serial.read();
    
    if( inChar == '\n' || inChar == '\r')  // end of line ?
    {
      dataEntered = true;
    }
    else
    {
      buffer[index] = (char) inChar;  // put new data in the buffer
      if( index < (int)(sizeof( buffer) - 1))  // the 'sizeof' returns an unsigned size_t
      {
        index++;
      }
      else
      {
        // The buffer is full, too much data was received.
        // Process the command and (for safety) clear
        // the remaining of the data.
        // With a low baudrate, it is possible that still
        // extra characters will come in after this.
        dataEntered = true;
        
        while( Serial.available() > 0)
        {
          Serial.read();
        }
      }
      buffer[index] = '\0';          // set a new zero-terminator
    }
  }
  return(dataEntered);
}

long noiseToRandom()
{
  long data;

  for(int pin=A0; pin<=A5; pin++)
  {
    data += (long) analogRead(pin);  
  }
  return(data);
}

void playInactivityMelody()
{
  for(int i=0; i<30; i++)
  {
    int f = random(100,4000);
    int d = random(20,180);
    int p = random(20,100);
    tone(buzzerPin,f);
    delay(d);
    noTone(buzzerPin);
    delay(p);
  }
}