/// <reference lib="dom" />
// A classic.
//
// Arrow keys move your tetrinome.
// Space bar rotates.
// Enter pauses the game.
export const game = true

import type { GameContext, InputState } from "@/shared/game"
import { randomElement, randomIndex } from "@/shared/utils"

const COLS = 10
const ROWS = 20
const CELL = 25
const DOWN_TICK = 10
const MOVE_TICK = 3
const TETRIS_FRAMES = 5
const LOCK_DELAY = 5

type Shape = { x: number, y: number, shape: string, rotation: number }

let score = 0
let player: Shape
let nextShape: Shape
let grid: string[][] = []
let dead = false
let paused = false
let downTick = 0
let moveTick = 0
let clearedLines: number[] = []
let clearedLinesTimer = 0
let lockTimer = 0

const BORDER_COLOR = "#b388ff"
const BG_COLOR = "#222"
const LINE_COLOR = "black"
const UI_BG_COLOR = "#4c3a87"

const LINE_SCORES = [
  100,
  300,
  500,
  800
]

const COLORS: Record<string, string> = {
  I: "cyan",
  O: "yellow",
  T: "magenta",
  S: "lime",
  Z: "red",
  J: "blue",
  L: "orange",
}

// I, O, T, L, J, S, Z
const SHAPES: Record<string, number[][][]> = {
  I: [
    [[1, 1, 1, 1]],
    [[1], [1], [1], [1]]
  ],

  O: [[
    [1, 1],
    [1, 1]
  ]],

  T: [
    [
      [1, 0],
      [1, 1],
      [1, 0]
    ],
    [
      [1, 1, 1],
      [0, 1, 0],
    ],
    [
      [0, 1],
      [1, 1],
      [0, 1]
    ],
    [
      [0, 1, 0],
      [1, 1, 1],
    ]
  ],

  L: [
    [
      [1, 0],
      [1, 0],
      [1, 1]
    ],
    [
      [1, 1, 1],
      [1, 0, 0]
    ],
    [
      [1, 1],
      [0, 1],
      [0, 1]
    ],
    [
      [0, 0, 1],
      [1, 1, 1]
    ]
  ],

  J: [
    [
      [0, 1],
      [0, 1],
      [1, 1]
    ],
    [
      [1, 0, 0],
      [1, 1, 1]
    ],
    [
      [1, 1],
      [1, 0],
      [1, 0]
    ],
    [
      [1, 1, 1],
      [0, 0, 1]
    ]
  ],

  S: [
    [
      [1, 0],
      [1, 1],
      [0, 1]
    ],
    [
      [0, 1, 1],
      [1, 1, 0],
    ],
    [
      [1, 0],
      [1, 1],
      [0, 1]
    ],
    [
      [0, 1, 1],
      [1, 1, 0]
    ]
  ],

  Z: [
    [
      [0, 1],
      [1, 1],
      [1, 0]
    ],
    [
      [1, 1, 0],
      [0, 1, 1]
    ],
    [
      [0, 1],
      [1, 1],
      [1, 0]
    ],
    [
      [1, 1, 0],
      [0, 1, 1]
    ]
  ],
}

export function init() {
  paused = false
  score = 0
  dead = false
  downTick = 0
  moveTick = 0
  lockTimer = 0
  clearedLinesTimer = TETRIS_FRAMES
  clearedLines.length = 0
  grid.length = 0

  player = newShape()
  nextShape = newShape()
}

export function update(_delta: number, input: InputState) {
  if (dead) return

  if (input.justPressed.has("Enter"))
    paused = !paused
  if (paused) return

  if (input.justPressed.has(" ") || input.justPressed.has("z")) {
    rotateShape()
  }

  // tetris animation
  if (clearedLines.length > 0) {
    clearedLinesTimer--
    if (clearedLinesTimer <= 0) {
      removeClearedLines()
      clearedLines = []
    }
  } else if (anyFullRows()) {
    clearedLines = findFullRows()
    clearedLinesTimer = TETRIS_FRAMES
  }

  const hit = isBlocked()
  if (!hit) lockTimer = LOCK_DELAY

  if (hit) {
    if (lockTimer > 0) {
      lockTimer--
    } else {
      lockShape()
      return
    }
  }

  if (++moveTick % MOVE_TICK === 0)
    detectMovement(input)

  if (++downTick % downTickForScore() === 0)
    if (!collision(player.x, player.y + 1, player.rotation)) {
      player.y++
    }
}

export function draw(game: GameContext) {
  game.clear(UI_BG_COLOR)

  const boardW = COLS * CELL
  const boardH = ROWS * CELL
  const offsetX = (game.width - boardW) / 2
  const offsetY = (game.height - boardH) / 2

  const c = game.ctx
  c.save()
  c.translate(offsetX, offsetY)

  // lines (shows through cracks between blocks)
  game.rectfill(0, 0, boardW, boardH, LINE_COLOR)

  // draw border
  drawBorder(game)

  // draw board
  drawBoard(game)

  // player shape
  drawPlayer(game)

  // "next shape" UI
  drawPreview(game)

  // high score
  game.text(`Score: ${score}`, (COLS + 2) * CELL, (8 * CELL) + 10, "yellow", 15)

  c.restore()

  if (paused)
    game.centerTextX("PAUSED", 200, "lime", 60)

  // ya dead
  if (dead)
    game.centerTextX("GAME OVER", 200, "red", 60)
}

function spawn() {
  player = nextShape
  player.x = 3
  player.y = 0
  nextShape = newShape()

  if (collision(player.x, player.y, player.rotation)) {
    dead = true
  }
}

function newShape(): Shape {
  const shape = randomElement(Object.keys(SHAPES))!
  return { x: 3, y: 0, shape, rotation: randomIndex(SHAPES[shape]!)! }
}

function anyFullRows(): boolean {
  return findFullRows().length > 0
}

function findFullRows(): number[] {
  const rows: number[] = []
  for (let y = 0; y < ROWS; y++) {
    let full = true
    for (let x = 0; x < COLS; x++) {
      if (!grid[y]?.[x]) {
        full = false
        break
      }
    }
    if (full) rows.push(y)
  }
  return rows
}

function downTickForScore(): number {
  return Math.max(1, DOWN_TICK - Math.floor(score / 1000))
}

function removeClearedLines() {
  const newGrid: string[][] = []
  score += LINE_SCORES[clearedLines.length - 1] || 0

  for (let y = 0; y < ROWS; y++)
    if (!clearedLines.includes(y))
      newGrid.push(grid[y] ?? [])

  // add empty rows on top to restore height
  while (newGrid.length < ROWS)
    newGrid.unshift([])

  grid = newGrid
  clearedLines = []
}

function collision(x: number, y: number, rotation: number): boolean {
  const shape = SHAPES[player.shape]![rotation]!

  for (let row = 0; row < shape.length; row++) {
    for (let col = 0; col < shape[row]!.length; col++) {
      if (!shape[row]![col]) continue

      const nx = x + col
      const ny = y + row

      if (nx < 0 || nx >= COLS) return true
      if (ny >= ROWS) return true
      if (grid[ny]?.[nx]) return true
    }
  }

  return false
}

function lockShape() {
  const shape = SHAPES[player.shape]![player.rotation]!
  for (const row in shape) {
    for (const col in shape[row]!) {
      const filled = shape[row][col]
      if (!filled) continue

      const x = player.x + Number(col)
      const y = player.y + Number(row)

      grid[y] ??= []
      grid[y][x] = COLORS[player.shape]!
    }
  }

  spawn()
}

function isBlocked(): boolean {
  const shape = SHAPES[player.shape]![player.rotation]!
  let hit = false

  outer: for (const row in shape) {
    for (const col in shape[row]!) {
      const filled = shape[row][col]
      if (!filled) continue

      const x = player.x + Number(col)
      const y = player.y + Number(row)

      grid[y + 1] ??= []

      if ((y + 1) >= ROWS || grid[y + 1]![x]) {
        hit = true
        break outer
      }
    }
  }

  return hit
}

function detectMovement(input: InputState) {
  const keys = input.pressed

  if (
    (keys.has("ArrowLeft") || keys.has("a")) &&
    !collision(player.x - 1, player.y, player.rotation)
  )
    player.x = Math.max(0, player.x - 1)

  if (
    (keys.has("ArrowRight") || keys.has("d")) &&
    !collision(player.x + 1, player.y, player.rotation)
  )
    player.x = Math.min(COLS, player.x + 1)

  if (
    (keys.has("ArrowDown") || keys.has("s")) &&
    !collision(player.x, player.y + 1, player.rotation)
  )
    player.y += 1
}

function drawBlock(game: GameContext, x: number, y: number, color: string) {
  game.rectfill(
    x * CELL,
    y * CELL,
    ((x + 1) * CELL) - .5,
    ((y + 1) * CELL) - .5,
    color
  )
}

// I, O, T, L, J, S, Z
const PREVIEW_OFFSETS: Record<string, [number, number]> = {
  I: [2, 3],
  O: [3, 2],
  T: [3, 2],
  L: [3, 2],
  J: [3, 2],
  S: [3, 2],
  Z: [3, 2],
}

// draw next shape (top-right corner)
function drawPreview(game: GameContext) {
  const previewX = COLS + 2
  const previewY = 0
  const cols = 7
  const rows = 7

  // lines (shows through cracks between blocks)
  game.rectfill(
    previewX * CELL,
    previewY * CELL,
    (previewX + cols) * CELL,
    (previewY + rows) * CELL,
    LINE_COLOR
  )
  // draw board
  for (let row = 0; row < rows; row++) {
    for (let col = 0; col < cols; col++) {
      drawBlock(game, previewX + col, previewY + row, BG_COLOR)
    }
  }

  // draw border
  for (let col = -1; col < cols; col++) {
    drawBlock(game, previewX + col + 1, previewY + -1, BORDER_COLOR)
    drawBlock(game, previewX + col + 1, previewY + rows, BORDER_COLOR)
  }

  for (let row = -1; row < rows; row++) {
    drawBlock(game, previewX, previewY + row, BORDER_COLOR)
    drawBlock(game, previewX + cols, previewY + row, BORDER_COLOR)
  }

  const next = SHAPES[nextShape.shape]![0]!
  for (let row = 0; row < next.length; row++) {
    for (let col = 0; col < next[row]!.length; col++) {
      if (!next[row]![col]) continue
      const [offsetX, offsetY] = PREVIEW_OFFSETS[nextShape.shape]!
      drawBlock(game, previewX + col + offsetX, previewY + row + offsetY, COLORS[nextShape.shape]!)
    }
  }
}

function rotateShape() {
  const oldRot = player.rotation
  const newRot = (oldRot + 1) % SHAPES[player.shape]!.length

  if (!collision(player.x, player.y, newRot)) {
    player.rotation = newRot
    return
  }

  if (!collision(player.x + 1, player.y, newRot)) {
    player.x += 1
    player.rotation = newRot
    return
  }

  if (!collision(player.x - 1, player.y, newRot)) {
    player.x -= 1
    player.rotation = newRot
    return
  }
}

function drawBorder(game: GameContext) {
  for (let col = -1; col <= COLS; col++) {
    drawBlock(game, col, -1, BORDER_COLOR)
    drawBlock(game, col, ROWS, BORDER_COLOR)
  }
  for (let row = 0; row < ROWS; row++) {
    drawBlock(game, -1, row, BORDER_COLOR)
    drawBlock(game, COLS, row, BORDER_COLOR)
  }
}

function drawBoard(game: GameContext) {
  for (let row = 0; row < ROWS; row++) {
    for (let col = 0; col < COLS; col++) {
      let color = grid[row]?.[col] || BG_COLOR
      if (clearedLines.includes(row)) color = "white"
      drawBlock(game, col, row, color)
    }
  }
}

function drawPlayer(game: GameContext) {
  const shape = SHAPES[player.shape]![player.rotation]!
  for (const row in shape) {
    for (const col in shape[row]!) {
      const filled = shape[row][col]
      if (!filled) continue

      const x = player.x + Number(col)
      const y = player.y + Number(row)

      drawBlock(game, x, y, COLORS[player.shape]!)
    }
  }
}