// Tetris: L / R = move (hold to slide), M = rotate, hold M = drop faster. Full rows disappear.
#pragma once

const uint8_t BOARD_W = 10;
const uint8_t BOARD_H = 15;
const uint8_t CELL = 4;
const uint8_t BOARD_X = 64 - BOARD_W * CELL / 2;
const uint8_t BOARD_Y = 2;

// 7 pieces x 4 rotations, each a 4x4 grid in 16 bits (bit 15 = top left)
const uint16_t PIECES[7][4] PROGMEM = {
  {0x0F00, 0x2222, 0x00F0, 0x4444},          // I
  {0x8E00, 0x6440, 0x0E20, 0x44C0},          // J
  {0x2E00, 0x4460, 0x0E80, 0xC440},          // L
  {0x6600, 0x6600, 0x6600, 0x6600},          // O
  {0x6C00, 0x4620, 0x06C0, 0x8C40},          // S
  {0x4E00, 0x4640, 0x0E40, 0x4C40},          // T
  {0xC600, 0x2640, 0x0C60, 0x4C80},          // Z
};

struct TetrisState {
  uint16_t rows[BOARD_H];                    // one bit per filled cell
  int8_t x, y;
  uint8_t piece, rot, next, fall, slide;
  uint16_t lines;
};

uint16_t piece_mask(uint8_t piece, uint8_t rot) {
  return pgm_read_word(&PIECES[piece][rot]);
}

bool piece_fits(TetrisState &g, int8_t x, int8_t y, uint8_t rot) {
  uint16_t m = piece_mask(g.piece, rot);

  for (uint8_t i = 0; i < 16; i++) {
    if (!(m & (0x8000 >> i))) {
      continue;
    }

    int8_t cx = x + i % 4;
    int8_t cy = y + i / 4;

    if (cx < 0 || cx >= BOARD_W || cy >= BOARD_H) {
      return false;
    }

    if (cy >= 0 && (g.rows[cy] & (1 << cx))) {
      return false;
    }
  }

  return true;
}

void tetris_spawn(TetrisState &g) {
  g.piece = g.next;
  g.next = random(7);
  g.rot = 0;
  g.x = 3;
  g.y = -1;

  if (!piece_fits(g, g.x, g.y, g.rot)) {
    end_game();
  }
}

void tetris_init(TetrisState &g) {
  memset(&g, 0, sizeof g);
  g.next = random(7);
  tetris_spawn(g);
}

void tetris_lock(TetrisState &g) {
  uint16_t m = piece_mask(g.piece, g.rot);

  for (uint8_t i = 0; i < 16; i++) {
    int8_t cy = g.y + i / 4;

    if ((m & (0x8000 >> i)) && cy >= 0) {
      g.rows[cy] |= 1 << (g.x + i % 4);
    }
  }

  uint8_t cleared = 0;

  for (int8_t r = BOARD_H - 1; r >= 0; r--) {
    if (g.rows[r] == (1 << BOARD_W) - 1) {
      memmove(&g.rows[1], &g.rows[0], r * sizeof g.rows[0]);
      g.rows[0] = 0;
      cleared++;
      r++;
    }
  }

  static const uint8_t points[5] = {0, 1, 3, 5, 8};
  score += points[cleared];
  g.lines += cleared;
  tetris_spawn(g);
}

void tetris_update(TetrisState &g) {
  for (uint8_t k = KEY_L; k <= KEY_R; k += 2) {
    int8_t dx = k == KEY_L ? -1 : 1;
    bool repeat = key_down[k] && key_held_ms[k] >= 200 && ++g.slide % 3 == 0;

    if ((key_tap[k] || repeat) && piece_fits(g, g.x + dx, g.y, g.rot)) {
      g.x += dx;
    }
  }

  if (key_tap[KEY_M]) {
    uint8_t rot = (g.rot + 1) % 4;

    for (int8_t kick = 0; kick < 3; kick++) {           // near a wall: try one cell left or right
      int8_t dx = kick == 0 ? 0 : kick == 1 ? -1 : 1;

      if (piece_fits(g, g.x + dx, g.y, rot)) {
        g.x += dx;
        g.rot = rot;
        break;
      }
    }
  }

  uint8_t speed = g.lines < 30 ? 18 - g.lines / 2 : 3;
  bool fast = key_down[KEY_M] && key_held_ms[KEY_M] >= 300;

  if (++g.fall < (fast ? 1 : speed)) {
    return;
  }

  g.fall = 0;

  if (piece_fits(g, g.x, g.y + 1, g.rot)) {
    g.y++;
  } else {
    tetris_lock(g);
  }
}

void tetris_cells(uint16_t m, int16_t x0, int16_t y0) {
  for (uint8_t i = 0; i < 16; i++) {
    if (m & (0x8000 >> i)) {
      box(x0 + (i % 4) * CELL, y0 + (i / 4) * CELL, CELL - 1, CELL - 1);
    }
  }
}

void tetris_draw(TetrisState &g) {
  oled.drawFrame(BOARD_X - 2, BOARD_Y - 2, BOARD_W * CELL + 3, BOARD_H * CELL + 3);

  for (uint8_t r = 0; r < BOARD_H; r++) {
    for (uint8_t c = 0; c < BOARD_W; c++) {
      if (g.rows[r] & (1 << c)) {
        box(BOARD_X + c * CELL, BOARD_Y + r * CELL, CELL - 1, CELL - 1);
      }
    }
  }

  tetris_cells(piece_mask(g.piece, g.rot), BOARD_X + g.x * CELL, BOARD_Y + g.y * CELL);
  oled.setFont(u8g2_font_5x7_tr);
  oled.drawStr(0, 7, "SCORE");
  draw_number(score, 0, 16, false);
  oled.drawStr(0, 30, "LINES");
  draw_number(g.lines, 0, 39, false);
  oled.drawStr(0, 53, "BEST");
  draw_number(best, 0, 62, false);
  oled.drawStr(98, 7, "NEXT");
  tetris_cells(piece_mask(g.next, 0), 100, 12);
}
