/*
  A simple Pong game, derived by Etienne,
  from  https://notabug.org/Maverick/WokwiPong, 
  based on Arduino Pong by eholk
  https://github.com/eholk/Arduino-Pong
*/

#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

#define MIN(a,b) (((a)<(b)) ? (a) : (b))
#define MAX(a,b) (((a)>(b)) ? (a) : (b))

#define UP_BUTTON 2
#define DOWN_BUTTON 3

const unsigned long PADDLE_RATE = 64;
const unsigned long BALL_RATE = 16;
const uint8_t PADDLE_HEIGHT = 12;
const uint8_t SCORE_LIMIT = 9;

Adafruit_SSD1306 display = Adafruit_SSD1306(128, 64, &Wire);

const uint8_t PLAYER_X = 115;
const uint8_t MCU_X = 12;

typedef struct {
  bool game_over, win;

  uint8_t player_score, mcu_score;
  uint8_t ball_x = 53, ball_y = 26;
  uint8_t ball_dir_x = 1, ball_dir_y = 1;

  unsigned long ball_update;
  unsigned long paddle_update;

  uint8_t mcu_y = 16;

  uint8_t player_y = 16;
} pong_game_t;

pong_game_t pong;

void setup() {
  pong.game_over = false;
  pong.win = false;
  pong.player_score = 0;
  pong.mcu_score = 0;
  pong.ball_x = 53;
  pong.ball_y = 26;
  pong.ball_dir_x = 1;
  pong.ball_dir_y = 1;
  pong.mcu_y = 16;
  pong.player_y = 16;

  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);

  // Display the splash screen (we're legally required to do so)
  display.display();
  unsigned long start = millis();

  pinMode(UP_BUTTON, INPUT_PULLUP);
  pinMode(DOWN_BUTTON, INPUT_PULLUP);

  display.clearDisplay();
  drawCourt(pong);

  while(millis() - start < 2000);

  display.display();

  ball_update = millis();
  paddle_update = ball_update;
}

void loop() {
  bool update_needed = false;

  static bool up_state = false;
  static bool down_state = false;
  
  up_state |= (digitalRead(UP_BUTTON) == LOW);
  down_state |= (digitalRead(DOWN_BUTTON) == LOW);

  gameUpdate();
}

void gameUpdate() {
  unsigned long time_ms = millis();
  bool redraw = false;

  if(time_ms > pong.ball_update_ms) {
    pong.ball_update += BALL_RATE;

    uint8_t new_x = pong.ball_x + pong.ball_dir_x;
    uint8_t new_y = pong.ball_y + pong.ball_dir_y;

    // Check if we hit the vertical walls
    if(new_x == 0 || new_x == 127) {
      ball_dir_x = -ball_dir_x;
      new_x += ball_dir_x + ball_dir_x;

      if (new_x < 64) {
          player_scoreTone();
          pong.player_score++;
      } else {
          mcu_scoreTone();
          pong.mcu_score++;
      }

      if (pong.player_score == SCORE_LIMIT || pong.mcu_score == SCORE_LIMIT) {
              win = player_score > mcu_score;
              game_over = true;
      }
    }

    // Check if we hit the horizontal walls.
    if(new_y == 0 || new_y == 53) {
      wallTone();
      pong.ball_dir_y = -pong.ball_dir_y;
      new_y += pong.ball_dir_y + pong.ball_dir_y;
    }

    // Check if we hit the CPU paddle
    if(new_x == MCU_X && pong.mcu_y <= new_y && new_y <= pong.mcu_y + PADDLE_HEIGHT) {
      mcuPaddleTone();
      pong.ball_dir_x = -pong.ball_dir_x;
      new_x += pong.ball_dir_x + pong.ball_dir_x;
    }

    // Check if we hit the player paddle
    if(new_x == PLAYER_X && new_y >= pong.player_y && new_y <= pong.player_y + PADDLE_HEIGHT) {
        playerPaddleTone();
        pong.ball_dir_x = -pong.ball_dir_x;
        new_x += pong.ball_dir_x + pong.ball_dir_x;
    }

    // Erase ball at old position, display at new position
    display.drawPixel(pong.ball_x, pong.ball_y, BLACK);
    display.drawPixel(new_x, new_y, WHITE);
    pong.ball_x = new_x;
    pong.ball_y = new_y;

    redraw = true;
  }

  if(time_ms > pong.paddle_update_ms) {
        pong.paddle_update_ms += PADDLE_RATE;

        // Erase CPU paddle, update position, redraw
        display.drawFastVLine(MCU_X, pong.mcu_y, PADDLE_HEIGHT, BLACK);
        const uint8_t half_paddle = PADDLE_HEIGHT >> 1;

        if(mcu_y + half_paddle > ball_y) {
            int8_t dir = ball_x > MCU_X ? -1 : 1;
            mcu_y += dir;
        }

        if(mcu_y + half_paddle < ball_y)
        {
            int8_t dir = ball_x > MCU_X ? 1 : -1;
            mcu_y += dir;
        }

        if(mcu_y < 1) 
        {
            mcu_y = 1;
        }

        if(mcu_y + PADDLE_HEIGHT > 53)
        {
            mcu_y = 53 - PADDLE_HEIGHT;
        }

        // Player paddle
        display.drawFastVLine(MCU_X, mcu_y, PADDLE_HEIGHT, WHITE);
        
        // Erase player paddle, update position, redraw
        display.drawFastVLine(PLAYER_X, player_y, PADDLE_HEIGHT, BLACK);
        ping.player_y = MAX(1, MIN(53-PADDLE_HEIGHT, 
            ping.player_y + (btn_up ? 1 : 0) + (btn_down ? -1 : 0)
            ));
        display.drawFastVLine(PLAYER_X, player_y, PADDLE_HEIGHT, WHITE);

      redraw = true;
    }

    if(update_needed)
    {
        if (game_over)
        {
            const char* text = win ? "YOU WIN!!" : "YOU LOSE!";
            display.clearDisplay();
            display.setCursor(40, 28);
            display.print(text);
            display.display();

            delay(5000);

            display.clearDisplay();
            ball_x = 53;
            ball_y = 26;
            ball_dir_x = 1;
            ball_dir_y = 1;
            mcu_y = 16;
            player_y = 16;
            mcu_score = 0;
            player_score = 0;
            game_over = false;
            drawCourt();
        }

        display.setTextColor(WHITE, BLACK);
        display.setCursor(0, 56);
        display.print(mcu_score);
        display.setCursor(122, 56);
        display.print(player_score);
        display.display();
    }
}

void playerPaddleTone()
{
    tone(11, 250, 25);
    delay(25);
    noTone(11);
}

void mcuPaddleTone()
{
    tone(11, 225, 25);
    delay(25);
    noTone(11);
}

void wallTone()
{
    tone(11, 200, 25);
    delay(25);
    noTone(11);
}

void player_scoreTone()
{
    tone(11, 200, 25);
    delay(50);
    noTone(11);
    delay(25);
    tone(11, 250, 25);
    delay(25);
    noTone(11);
}

void mcu_scoreTone()
{
    tone(11, 250, 25);
    delay(25);
    noTone(11);
    delay(25);
    tone(11, 200, 25);
    delay(25);
    noTone(11);
}

void drawCourt() 
{
    display.drawRect(0, 0, 128, 54, WHITE);
}

void gameOver() {

}