/* global */
// ============================================================
// Block Classic — pure logic
//   • Piece definitions, colors, scoring tables, speed curve
//   • 7-bag randomizer
//   • Board helpers (collision, lock, line-clear)
//   • Reducer for the game state machine
// All exports live on window.tetris.* — kept side-effect free.
// ============================================================

(function (global) {
  // ----------------------------------------------------------
  // Board geometry
  // ----------------------------------------------------------
  const COLS = 10;
  const VISIBLE_ROWS = 20;
  const HIDDEN_ROWS = 2;            // spawn buffer above the playfield
  const ROWS = VISIBLE_ROWS + HIDDEN_ROWS;

  // ----------------------------------------------------------
  // Tetromino shapes
  // Each entry is an array of rotation states; each rotation
  // is an array of [x, y] cell offsets relative to the piece origin.
  // Origins are picked so spawn position (x = 3, y = 0) lands the
  // pieces with their top-left near the top of the board.
  // ----------------------------------------------------------
  const PIECES = {
    I: {
      colorIdx: 1,
      rotations: [
        [[0,1],[1,1],[2,1],[3,1]],
        [[2,0],[2,1],[2,2],[2,3]],
        [[0,2],[1,2],[2,2],[3,2]],
        [[1,0],[1,1],[1,2],[1,3]],
      ],
    },
    O: {
      colorIdx: 2,
      rotations: [
        [[1,0],[2,0],[1,1],[2,1]],
        [[1,0],[2,0],[1,1],[2,1]],
        [[1,0],[2,0],[1,1],[2,1]],
        [[1,0],[2,0],[1,1],[2,1]],
      ],
    },
    T: {
      colorIdx: 3,
      rotations: [
        [[1,0],[0,1],[1,1],[2,1]],
        [[1,0],[1,1],[2,1],[1,2]],
        [[0,1],[1,1],[2,1],[1,2]],
        [[1,0],[0,1],[1,1],[1,2]],
      ],
    },
    S: {
      colorIdx: 4,
      rotations: [
        [[1,0],[2,0],[0,1],[1,1]],
        [[1,0],[1,1],[2,1],[2,2]],
        [[1,1],[2,1],[0,2],[1,2]],
        [[0,0],[0,1],[1,1],[1,2]],
      ],
    },
    Z: {
      colorIdx: 5,
      rotations: [
        [[0,0],[1,0],[1,1],[2,1]],
        [[2,0],[1,1],[2,1],[1,2]],
        [[0,1],[1,1],[1,2],[2,2]],
        [[1,0],[0,1],[1,1],[0,2]],
      ],
    },
    J: {
      colorIdx: 6,
      rotations: [
        [[0,0],[0,1],[1,1],[2,1]],
        [[1,0],[2,0],[1,1],[1,2]],
        [[0,1],[1,1],[2,1],[2,2]],
        [[1,0],[1,1],[0,2],[1,2]],
      ],
    },
    L: {
      colorIdx: 7,
      rotations: [
        [[2,0],[0,1],[1,1],[2,1]],
        [[1,0],[1,1],[1,2],[2,2]],
        [[0,1],[1,1],[2,1],[0,2]],
        [[0,0],[1,0],[1,1],[1,2]],
      ],
    },
  };

  const PIECE_TYPES = ['I','O','T','S','Z','J','L'];

  // Color index 0 = empty cell. 1-7 map to piece types I O T S Z J L.
  // Tints are tuned for the violet-on-near-black canvas; saturation is
  // medium to keep the playfield calm rather than carnival-loud.
  const COLORS = [
    null,
    { fill: '#22d3ee', edge: '#67e8f9' }, // I — cyan
    { fill: '#facc15', edge: '#fde047' }, // O — yellow
    { fill: '#a855f7', edge: '#c084fc' }, // T — purple (brand)
    { fill: '#34d399', edge: '#6ee7b7' }, // S — emerald
    { fill: '#f87171', edge: '#fca5a5' }, // Z — red
    { fill: '#60a5fa', edge: '#93c5fd' }, // J — blue
    { fill: '#fb923c', edge: '#fdba74' }, // L — orange
  ];

  // ----------------------------------------------------------
  // Scoring + level curve
  // ----------------------------------------------------------
  const LINE_SCORES = { 1: 100, 2: 300, 3: 500, 4: 800 };

  // Gravity in ms per cell. PRD: level 1 ≈ 1 cell/sec, level 15 ≈ 12/sec.
  // Smooth curve in between; clamps at level 15.
  function gravityMs(level) {
    const lvl = Math.max(1, Math.min(15, level));
    // 1000 → 80ms roughly, exponential-ish drop.
    // Derived so lvl 1 = 1000, lvl 5 ~ 470, lvl 10 ~ 180, lvl 15 ~ 85.
    return Math.round(1000 * Math.pow(0.78, lvl - 1));
  }

  // ----------------------------------------------------------
  // 7-bag randomizer
  // ----------------------------------------------------------
  function shuffle(arr) {
    const a = arr.slice();
    for (let i = a.length - 1; i > 0; i--) {
      const j = Math.floor(Math.random() * (i + 1));
      [a[i], a[j]] = [a[j], a[i]];
    }
    return a;
  }
  function newBag() {
    return shuffle(PIECE_TYPES);
  }
  // Pull one piece type from the bag, refilling if empty.
  function drawFromBag(bag) {
    if (!bag.length) bag = newBag();
    const [head, ...rest] = bag;
    return { piece: head, bag: rest.length ? rest : newBag() };
  }
  // Pull `n` piece types from the bag (for filling the next queue).
  function drawN(bag, n) {
    const out = [];
    let cur = bag.slice();
    for (let i = 0; i < n; i++) {
      const { piece, bag: rest } = drawFromBag(cur);
      out.push(piece);
      cur = rest;
    }
    return { pieces: out, bag: cur };
  }

  // ----------------------------------------------------------
  // Board helpers
  // ----------------------------------------------------------
  function emptyBoard() {
    return Array.from({ length: ROWS }, () => Array(COLS).fill(0));
  }

  function pieceCells(piece) {
    if (!piece) return [];
    const shape = PIECES[piece.type].rotations[piece.rotation];
    return shape.map(([dx, dy]) => [piece.x + dx, piece.y + dy]);
  }

  function collides(board, piece) {
    for (const [x, y] of pieceCells(piece)) {
      if (x < 0 || x >= COLS || y >= ROWS) return true;
      if (y < 0) continue; // above the board is allowed during spawn
      if (board[y][x] !== 0) return true;
    }
    return false;
  }

  function lockPiece(board, piece) {
    const out = board.map(r => r.slice());
    const idx = PIECES[piece.type].colorIdx;
    for (const [x, y] of pieceCells(piece)) {
      if (y >= 0 && y < ROWS && x >= 0 && x < COLS) out[y][x] = idx;
    }
    return out;
  }

  // Returns { board, clearedRows: number[] } — rows are indexes into `board`.
  function findFullRows(board) {
    const rows = [];
    for (let y = 0; y < ROWS; y++) {
      if (board[y].every(c => c !== 0)) rows.push(y);
    }
    return rows;
  }

  function clearRows(board, rows) {
    if (!rows.length) return board;
    const set = new Set(rows);
    const kept = board.filter((_, y) => !set.has(y));
    const fresh = Array.from({ length: rows.length }, () => Array(COLS).fill(0));
    return [...fresh, ...kept];
  }

  // Compute landing y for the ghost piece. Returns the piece copy
  // with the deepest legal y.
  function ghostFor(board, piece) {
    if (!piece) return null;
    let g = { ...piece };
    while (!collides(board, { ...g, y: g.y + 1 })) g.y += 1;
    return g;
  }

  // Simplified wall-kicks: try original, then ±1 col, then ±1 row.
  // PRD asks not to ship full SRS — this still rescues most casual rotations.
  const KICK_OFFSETS = [
    [0, 0],
    [-1, 0], [1, 0],
    [0, -1],
    [-2, 0], [2, 0],
    [-1, -1], [1, -1],
  ];
  function tryRotate(board, piece, dir) {
    const nextRot = (piece.rotation + (dir === 'cw' ? 1 : 3)) % 4;
    for (const [dx, dy] of KICK_OFFSETS) {
      const candidate = { ...piece, rotation: nextRot, x: piece.x + dx, y: piece.y + dy };
      if (!collides(board, candidate)) return candidate;
    }
    return null;
  }

  // ----------------------------------------------------------
  // Spawn
  // ----------------------------------------------------------
  function spawnPiece(type) {
    // Top-left of the 4-wide tetromino box.
    // x = 3 centers the I/O/T/S/Z/J/L pieces on the 10-wide board.
    return { type, rotation: 0, x: 3, y: 0 };
  }

  // ----------------------------------------------------------
  // Reducer
  // ----------------------------------------------------------
  // status: 'menu' | 'playing' | 'paused' | 'lineclear' | 'gameover'

  const QUEUE_SIZE = 3;
  const LOCK_DELAY_MS = 500;
  const LOCK_DELAY_MAX_RESETS = 5;
  const CLEAR_ANIM_MS = 180;

  function initialState() {
    return {
      board: emptyBoard(),
      currentPiece: null,
      nextQueue: [],
      holdPiece: null,
      canHold: true,
      bag: [],
      score: 0,
      level: 1,
      lines: 0,
      status: 'menu',
      lockDelay: null,    // { startedAt, resetsRemaining } or null
      clearAnim: null,    // { rows, startedAt, newBoard } or null
      backToBack: false,
      lastClearText: null,// e.g. 'Tetris ×2' for the small flash
    };
  }

  // Spawn a new piece, refilling the queue + bag. Triggers game over
  // if the spawn position is already blocked.
  function withSpawn(state) {
    let queue = state.nextQueue.slice();
    let bag = state.bag.slice();
    if (queue.length < QUEUE_SIZE + 1) {
      const need = (QUEUE_SIZE + 1) - queue.length;
      const { pieces, bag: nb } = drawN(bag, need);
      queue = [...queue, ...pieces];
      bag = nb;
    }
    const [nextType, ...rest] = queue;
    const piece = spawnPiece(nextType);
    if (collides(state.board, piece)) {
      return { ...state, currentPiece: null, status: 'gameover', lockDelay: null };
    }
    return {
      ...state,
      currentPiece: piece,
      nextQueue: rest,
      bag,
      canHold: true,
      lockDelay: null,
    };
  }

  function updateLockDelay(state) {
    // If piece is resting on something, start (or keep) lock timer.
    // If it's floating, clear it.
    if (!state.currentPiece) return state;
    const resting = collides(state.board, { ...state.currentPiece, y: state.currentPiece.y + 1 });
    if (resting) {
      if (!state.lockDelay) {
        return { ...state, lockDelay: { startedAt: performance.now(), resetsRemaining: LOCK_DELAY_MAX_RESETS } };
      }
      return state;
    }
    if (state.lockDelay) return { ...state, lockDelay: null };
    return state;
  }

  // Lock the current piece, then either kick off a line-clear animation
  // or spawn the next piece immediately.
  function lockAndAdvance(state, hardDropCells = 0) {
    if (!state.currentPiece) return state;
    const locked = lockPiece(state.board, state.currentPiece);
    const fullRows = findFullRows(locked);

    let score = state.score;
    if (hardDropCells > 0) score += hardDropCells * 2;

    if (fullRows.length === 0) {
      // No clear, just advance.
      return withSpawn({
        ...state,
        board: locked,
        currentPiece: null,
        lockDelay: null,
        score,
        backToBack: false,
        lastClearText: null,
      });
    }

    // Compute clear score (applied after the animation runs out;
    // we apply it now and animate purely for visuals).
    const base = LINE_SCORES[fullRows.length] * state.level;
    const isTetris = fullRows.length === 4;
    let bonus = 0;
    if (isTetris && state.backToBack) bonus = Math.round(base * 0.5);
    const linesNext = state.lines + fullRows.length;
    const levelNext = Math.min(15, 1 + Math.floor(linesNext / 10));

    const labels = { 1: 'Single', 2: 'Double', 3: 'Triple', 4: 'Tetris' };
    let text = labels[fullRows.length];
    if (isTetris && state.backToBack) text = 'Tetris ×B2B';

    return {
      ...state,
      board: locked, // show the locked piece briefly behind the flash
      currentPiece: null,
      lockDelay: null,
      status: 'lineclear',
      clearAnim: {
        rows: fullRows,
        startedAt: performance.now(),
        newBoard: clearRows(locked, fullRows),
      },
      score: score + base + bonus,
      lines: linesNext,
      level: levelNext,
      backToBack: isTetris,
      lastClearText: text,
    };
  }

  function reducer(state, action) {
    switch (action.type) {
      case 'RESET': {
        return initialState();
      }
      case 'START_GAME': {
        const fresh = initialState();
        return withSpawn({ ...fresh, status: 'playing' });
      }
      case 'RESUME_SAVED': {
        // Restore a previously serialized state. We trust the caller (storage)
        // to hand us a valid shape — fall back to a fresh state if not.
        const saved = action.state;
        if (!saved || !Array.isArray(saved.board)) return initialState();
        return {
          ...initialState(),
          ...saved,
          status: 'paused', // always resume paused, never mid-frame
          lockDelay: null,
          clearAnim: null,
        };
      }
      case 'TICK': {
        if (state.status !== 'playing' || !state.currentPiece) return state;
        const moved = { ...state.currentPiece, y: state.currentPiece.y + 1 };
        if (collides(state.board, moved)) {
          // Can't move down. Either lock now (delay expired) or start delay.
          if (state.lockDelay && performance.now() - state.lockDelay.startedAt >= LOCK_DELAY_MS) {
            return lockAndAdvance(state);
          }
          return updateLockDelay(state);
        }
        return updateLockDelay({ ...state, currentPiece: moved });
      }
      case 'MOVE_LEFT':
      case 'MOVE_RIGHT': {
        if (state.status !== 'playing' || !state.currentPiece) return state;
        const dx = action.type === 'MOVE_LEFT' ? -1 : 1;
        const moved = { ...state.currentPiece, x: state.currentPiece.x + dx };
        if (collides(state.board, moved)) return state;
        // If on the ground, this counts as a lock-delay reset.
        let next = { ...state, currentPiece: moved };
        if (state.lockDelay) {
          if (state.lockDelay.resetsRemaining > 0) {
            next.lockDelay = { startedAt: performance.now(), resetsRemaining: state.lockDelay.resetsRemaining - 1 };
          }
        }
        return updateLockDelay(next);
      }
      case 'SOFT_DROP': {
        if (state.status !== 'playing' || !state.currentPiece) return state;
        const moved = { ...state.currentPiece, y: state.currentPiece.y + 1 };
        if (collides(state.board, moved)) {
          // At rest — start/continue lock delay.
          return updateLockDelay(state);
        }
        return updateLockDelay({ ...state, currentPiece: moved, score: state.score + 1 });
      }
      case 'HARD_DROP': {
        if (state.status !== 'playing' || !state.currentPiece) return state;
        const ghost = ghostFor(state.board, state.currentPiece);
        const dropDistance = ghost.y - state.currentPiece.y;
        return lockAndAdvance({ ...state, currentPiece: ghost }, dropDistance);
      }
      case 'ROTATE_CW':
      case 'ROTATE_CCW': {
        if (state.status !== 'playing' || !state.currentPiece) return state;
        const dir = action.type === 'ROTATE_CW' ? 'cw' : 'ccw';
        const rotated = tryRotate(state.board, state.currentPiece, dir);
        if (!rotated) return state;
        let next = { ...state, currentPiece: rotated };
        if (state.lockDelay && state.lockDelay.resetsRemaining > 0) {
          next.lockDelay = { startedAt: performance.now(), resetsRemaining: state.lockDelay.resetsRemaining - 1 };
        }
        return updateLockDelay(next);
      }
      case 'HOLD': {
        if (state.status !== 'playing' || !state.currentPiece || !state.canHold) return state;
        const held = state.holdPiece;
        const currentType = state.currentPiece.type;
        if (held) {
          const piece = spawnPiece(held);
          if (collides(state.board, piece)) return state; // can't hold into a wall
          return { ...state, currentPiece: piece, holdPiece: currentType, canHold: false, lockDelay: null };
        }
        // No held piece yet — stash current and spawn from queue.
        const stashed = { ...state, holdPiece: currentType, currentPiece: null, canHold: false };
        return withSpawn(stashed);
      }
      case 'COMMIT_LINE_CLEAR': {
        // Called after the line-clear animation has run its course.
        if (!state.clearAnim) return state;
        const next = {
          ...state,
          board: state.clearAnim.newBoard,
          clearAnim: null,
          status: 'playing',
        };
        return withSpawn(next);
      }
      case 'PAUSE': {
        if (state.status === 'playing') return { ...state, status: 'paused' };
        return state;
      }
      case 'RESUME': {
        if (state.status === 'paused') return { ...state, status: 'playing' };
        return state;
      }
      case 'GAME_OVER': {
        return { ...state, status: 'gameover' };
      }
      default: return state;
    }
  }

  // ----------------------------------------------------------
  // Serialization — drop the transient timing fields so saved
  // games are stable across reloads.
  // ----------------------------------------------------------
  function serialize(state) {
    if (!state || state.status === 'gameover' || state.status === 'menu') return null;
    return {
      board: state.board,
      currentPiece: state.currentPiece,
      nextQueue: state.nextQueue,
      holdPiece: state.holdPiece,
      canHold: state.canHold,
      bag: state.bag,
      score: state.score,
      level: state.level,
      lines: state.lines,
      backToBack: state.backToBack,
    };
  }

  // ----------------------------------------------------------
  // Public API
  // ----------------------------------------------------------
  global.tetris = {
    COLS, VISIBLE_ROWS, HIDDEN_ROWS, ROWS,
    PIECES, PIECE_TYPES, COLORS,
    LINE_SCORES, QUEUE_SIZE, LOCK_DELAY_MS, CLEAR_ANIM_MS,
    gravityMs,
    newBag, drawFromBag, drawN,
    emptyBoard, pieceCells, collides, lockPiece,
    findFullRows, clearRows, ghostFor, tryRotate, spawnPiece,
    reducer, initialState, serialize,
  };
})(window);
