// Macropad games: the same board, keys and screen as the PC macropad, but it works without a computer
// (power from any USB charger or power bank). Seven games, best scores are kept in the Nano's EEPROM.
// Menu: L / R = choose, M = play. In a game: hold L and R together 1 s = back to the menu.
// The PC firmware (macropad_nano) goes back on with the PC program: it offers to flash it by itself.
// Library: U8g2 in page mode (the screen is drawn in 4 strips, so the draw functions only draw, never change the game).

#include <EEPROM.h>
#include <U8g2lib.h>
#include <Wire.h>
#include "arcade.h"
#include "game_dino.h"
#include "game_flappy.h"
#include "game_snake.h"
#include "game_breakout.h"
#include "game_racer.h"
#include "game_simon.h"
#include "game_tetris.h"

const char HELLO[] = "MACROPAD-GAMES 3";

enum GameId : uint8_t { DINO, FLAPPY, SNAKE, BREAKOUT, RACER, SIMON, TETRIS, GAME_COUNT };

const char *const NAMES[GAME_COUNT] = {"DINO", "FLAPPY", "SNAKE", "BREAKOUT", "RACER", "SIMON", "TETRIS"};
const char *const HELP[GAME_COUNT] = {
  "any key: jump",
  "any key: flap",
  "L / R: turn",
  "L / R: move  M: launch",
  "L / R: change lane",
  "repeat with L M R",
  "L/R move  M rotate",
};

union GameState {                            // only one game runs at a time, so they share this memory
  DinoState dino;
  FlappyState flappy;
  SnakeState snake;
  BreakoutState breakout;
  RacerState racer;
  SimonState simon;
  TetrisState tetris;
} state;

uint8_t selected = 0;
bool in_menu = true;
uint32_t next_frame = 0;
uint16_t both_held_ms = 0;
char line[20];
uint8_t line_len = 0;

void start_game() {
  in_menu = false;
  wait_release = true;                       // the press that started the game does not also play
  game_over = false;
  score = 0;
  best = hiscore(selected);
  randomSeed(micros() ^ analogRead(A7));

  switch (selected) {
    case DINO: dino_init(state.dino); break;
    case FLAPPY: flappy_init(state.flappy); break;
    case SNAKE: snake_init(state.snake); break;
    case BREAKOUT: breakout_init(state.breakout); break;
    case RACER: racer_init(state.racer); break;
    case SIMON: simon_init(state.simon); break;
    case TETRIS: tetris_init(state.tetris); break;
  }
}

void update_game() {
  switch (selected) {
    case DINO: dino_update(state.dino); break;
    case FLAPPY: flappy_update(state.flappy); break;
    case SNAKE: snake_update(state.snake); break;
    case BREAKOUT: breakout_update(state.breakout); break;
    case RACER: racer_update(state.racer); break;
    case SIMON: simon_update(state.simon); break;
    case TETRIS: tetris_update(state.tetris); break;
  }

  if (game_over && score > best) {
    save_hiscore(selected, score);
  }
}

void draw_game() {
  switch (selected) {
    case DINO: dino_draw(state.dino); break;
    case FLAPPY: flappy_draw(state.flappy); break;
    case SNAKE: snake_draw(state.snake); break;
    case BREAKOUT: breakout_draw(state.breakout); break;
    case RACER: racer_draw(state.racer); break;
    case SIMON: simon_draw(state.simon); break;
    case TETRIS: tetris_draw(state.tetris); break;
  }

  if (game_over) {
    draw_game_over();
  }
}

void draw_menu() {
  oled.setFont(u8g2_font_5x7_tr);
  text_center("MACROPAD ARCADE", 7);
  oled.drawHLine(0, 10, 128);
  oled.setFont(u8g2_font_7x14B_tr);
  text_center(NAMES[selected], 30);
  oled.drawTriangle(4, 25, 10, 19, 10, 31);
  oled.drawTriangle(123, 25, 117, 19, 117, 31);
  oled.setFont(u8g2_font_5x7_tr);
  text_center(HELP[selected], 43);
  char s[12];
  strcpy(s, "best ");
  utoa(hiscore(selected), s + 5, 10);
  text_center(s, 53);
  text_center("M: play", 63);

  for (uint8_t i = 0; i < GAME_COUNT; i++) {
    uint8_t x = 64 - GAME_COUNT * 3 + i * 6;

    if (i == selected) {
      oled.drawBox(x, 12, 4, 2);
    } else {
      oled.drawPixel(x + 1, 13);
    }
  }
}

void update_menu() {
  if (key_tap[KEY_L]) {
    selected = (selected + GAME_COUNT - 1) % GAME_COUNT;
  }

  if (key_tap[KEY_R]) {
    selected = (selected + 1) % GAME_COUNT;
  }

  if (key_tap[KEY_M]) {
    start_game();
  }
}

// The PC program asks "?" to find the board; the answer tells it this is the games firmware.
// "i2c?" answers the number of mended OLED bus failures (a check of the wires, see arcade.h).
void read_serial() {
  while (Serial.available() > 0) {
    char ch = Serial.read();

    if (ch == '\n' || ch == '\r') {
      line[line_len] = '\0';

      if (strcmp(line, "?") == 0) {
        Serial.println(HELLO);
      } else if (strcmp(line, "i2c?") == 0) {   // how many OLED bus failures were mended since power-on
        Serial.println(oled_recoveries);
      }

      line_len = 0;
    } else if (line_len < sizeof line - 1) {
      line[line_len++] = ch;
    }
  }
}

void draw_frame() {
  oled.firstPage();

  do {
    if (in_menu) {
      draw_menu();
    } else {
      draw_game();
    }
  } while (oled.nextPage());
}

void setup() {
  for (uint8_t k = 0; k < KEY_COUNT; k++) {
    pinMode(KEY_PINS[k], INPUT_PULLUP);
  }

  Serial.begin(115200);
  oled_start();
}

void loop() {
  read_serial();
  uint32_t now = millis();

  if ((int32_t)(now - next_frame) < 0) {
    return;
  }

  next_frame = now + FRAME_MS;
  read_keys();

  if (in_menu) {
    update_menu();
  } else if (game_over) {
    if (now - over_at > OVER_PAUSE_MS && key_tap[KEY_M]) {
      start_game();
    } else if (now - over_at > OVER_PAUSE_MS && key_tap[KEY_L]) {
      in_menu = true;
      wait_release = true;
    }
  } else {
    update_game();
  }

  both_held_ms = key_down[KEY_L] && key_down[KEY_R] ? both_held_ms + FRAME_MS : 0;

  if (!in_menu && both_held_ms >= MENU_HOLD_MS) {
    in_menu = true;
    both_held_ms = 0;
    wait_release = true;
  }

  draw_frame();

  if (oled_glitched) {                       // a transfer failed during this frame: paint it once more at once
    oled_glitched = false;
    draw_frame();
  }
}
