/* global React, Icon, IconBtn, db, tetris */
const { useReducer, useEffect, useRef, useState, useCallback, useMemo } = React;

// ============================================================
// Block Classic — Tetris-style game tool
// Owns the game loop, input handling, persistence, and UI.
// Pure logic + reducer lives in tools/tetris-engine.jsx (window.tetris).
// ============================================================

const SAVE_KEYS = {
  highScore:     'tetris_highScore',
  lifetimeLines: 'tetris_lifetimeLines',
  gamesPlayed:   'tetris_gamesPlayed',
  savedGame:     'tetris_savedGame',
  settings:      'tetris_settings',
  history:       'tetris_history',
};

const HISTORY_MAX = 20;

// Format a duration in ms as "1m 23s" / "45s".
function fmtDuration(ms) {
  if (!ms || ms < 0) return '0s';
  const totalSec = Math.floor(ms / 1000);
  const m = Math.floor(totalSec / 60);
  const s = totalSec % 60;
  if (m >= 60) {
    const h = Math.floor(m / 60);
    return `${h}h ${m % 60}m`;
  }
  return m > 0 ? `${m}m ${s}s` : `${s}s`;
}

// Format a timestamp as a compact "3d ago" / "Just now" string.
function fmtAgo(ts) {
  if (!ts) return '';
  const d = Date.now() - ts;
  if (d < 60 * 1000) return 'just now';
  if (d < 60 * 60 * 1000) return `${Math.floor(d / 60000)}m ago`;
  if (d < 24 * 60 * 60 * 1000) return `${Math.floor(d / 3600000)}h ago`;
  const days = Math.floor(d / 86400000);
  if (days < 7) return `${days}d ago`;
  return new Date(ts).toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
}

const DEFAULT_TETRIS_SETTINGS = {
  ghost: true,
  showHold: true,
  showNext: true,
  sound: false,        // off by default per PRD ("nothing surprising")
};

// DAS — auto-repeat tuning. PRD: ~170ms initial, ~50ms repeat.
const DAS_DELAY = 170;
const DAS_REPEAT = 50;
const SOFT_DROP_REPEAT = 35;

// ----------------------------------------------------------
// Tiny audio helper — synthesized blips via WebAudio. No assets.
// Lazy-creates AudioContext on first interaction so Safari is happy.
// ----------------------------------------------------------
const audio = (() => {
  let ctx = null;
  let muted = true;
  function ensure() {
    if (!ctx) {
      try { ctx = new (window.AudioContext || window.webkitAudioContext)(); }
      catch (e) { ctx = null; }
    }
    return ctx;
  }
  function blip(freq, duration = 0.08, type = 'sine', volume = 0.06) {
    if (muted) return;
    const c = ensure();
    if (!c) return;
    const t0 = c.currentTime;
    const osc = c.createOscillator();
    const gain = c.createGain();
    osc.type = type;
    osc.frequency.setValueAtTime(freq, t0);
    gain.gain.setValueAtTime(0, t0);
    gain.gain.linearRampToValueAtTime(volume, t0 + 0.01);
    gain.gain.exponentialRampToValueAtTime(0.0001, t0 + duration);
    osc.connect(gain).connect(c.destination);
    osc.start(t0);
    osc.stop(t0 + duration + 0.02);
  }
  return {
    setMuted(v) { muted = !!v; },
    lock()      { blip(180, 0.06, 'square', 0.04); },
    move()      { blip(420, 0.03, 'sine', 0.025); },
    rotate()    { blip(560, 0.04, 'triangle', 0.03); },
    clear(n)    {
      // Ascending tones — single → tetris brighter & longer.
      const base = 440;
      const steps = [0, 4, 7, 12];
      for (let i = 0; i < n; i++) {
        const freq = base * Math.pow(2, steps[i] / 12);
        setTimeout(() => blip(freq, 0.10, 'triangle', 0.05), i * 40);
      }
    },
    over()      { blip(140, 0.4, 'sawtooth', 0.04); },
  };
})();

// ----------------------------------------------------------
// Cell — small block in the board grid (memoized).
// ----------------------------------------------------------
const TetrisCell = React.memo(function TetrisCell({ value, ghost, clearing }) {
  if (value === 0 && !ghost) {
    return <div className={`tt-cell is-empty ${clearing ? 'is-clearing' : ''}`} />;
  }
  const c = tetris.COLORS[value || ghost];
  const cls = `tt-cell ${ghost && !value ? 'is-ghost' : 'is-filled'} ${clearing ? 'is-clearing' : ''}`;
  return (
    <div
      className={cls}
      style={{
        '--fill': c.fill,
        '--edge': c.edge,
      }}
    />
  );
});

// ----------------------------------------------------------
// Mini board — draws a small grid for next/hold previews.
// ----------------------------------------------------------
function MiniPiece({ type, dim = false }) {
  if (!type) {
    return <div className="tt-mini tt-mini-empty" />;
  }
  const piece = tetris.PIECES[type];
  const shape = piece.rotations[0];
  // Find bounding box so the piece is centered in a 4x2 grid.
  const xs = shape.map(s => s[0]);
  const ys = shape.map(s => s[1]);
  const minX = Math.min(...xs), maxX = Math.max(...xs);
  const minY = Math.min(...ys), maxY = Math.max(...ys);
  const w = maxX - minX + 1, h = maxY - minY + 1;

  // Render as a 4×3 grid for visual consistency across all pieces.
  const gridW = 4, gridH = 3;
  const offX = Math.floor((gridW - w) / 2) - minX;
  const offY = Math.floor((gridH - h) / 2) - minY;

  const cells = Array.from({ length: gridH }, () => Array(gridW).fill(0));
  for (const [x, y] of shape) {
    const nx = x + offX;
    const ny = y + offY;
    if (ny >= 0 && ny < gridH && nx >= 0 && nx < gridW) {
      cells[ny][nx] = piece.colorIdx;
    }
  }

  return (
    <div
      className={`tt-mini ${dim ? 'is-dim' : ''}`}
      style={{
        gridTemplateColumns: `repeat(${gridW}, 1fr)`,
        gridTemplateRows: `repeat(${gridH}, 1fr)`,
      }}
    >
      {cells.flatMap((row, y) => row.map((v, x) => (
        <TetrisCell key={y * gridW + x} value={v} />
      )))}
    </div>
  );
}

// ----------------------------------------------------------
// Board — playfield renderer. Merges board + ghost + active piece.
// ----------------------------------------------------------
function TetrisBoard({ state, showGhost, clearAnim }) {
  const { COLS, VISIBLE_ROWS, HIDDEN_ROWS, ROWS } = tetris;

  const merged = useMemo(() => {
    const out = state.board.map(r => r.slice());
    const ghostOverlay = Array.from({ length: ROWS }, () => Array(COLS).fill(0));

    if (state.currentPiece) {
      if (showGhost && state.status === 'playing') {
        const ghost = tetris.ghostFor(state.board, state.currentPiece);
        if (ghost) {
          const idx = tetris.PIECES[ghost.type].colorIdx;
          for (const [x, y] of tetris.pieceCells(ghost)) {
            if (y >= 0 && y < ROWS && x >= 0 && x < COLS) ghostOverlay[y][x] = idx;
          }
        }
      }
      const idx = tetris.PIECES[state.currentPiece.type].colorIdx;
      for (const [x, y] of tetris.pieceCells(state.currentPiece)) {
        if (y >= 0 && y < ROWS && x >= 0 && x < COLS) out[y][x] = idx;
      }
    }
    return { board: out, ghost: ghostOverlay };
  }, [state.board, state.currentPiece, state.status, showGhost, COLS, ROWS]);

  const clearingRows = clearAnim ? new Set(clearAnim.rows) : null;

  // Only render the visible portion.
  return (
    <div
      className="tt-board"
      style={{
        gridTemplateColumns: `repeat(${COLS}, 1fr)`,
        gridTemplateRows: `repeat(${VISIBLE_ROWS}, 1fr)`,
      }}
    >
      {Array.from({ length: VISIBLE_ROWS }).map((_, vy) => {
        const y = vy + HIDDEN_ROWS;
        return merged.board[y].map((v, x) => (
          <TetrisCell
            key={y * COLS + x}
            value={v}
            ghost={merged.ghost[y][x]}
            clearing={clearingRows ? clearingRows.has(y) : false}
          />
        ));
      })}
    </div>
  );
}

// ----------------------------------------------------------
// HUD — score, level, lines, next, hold.
// ----------------------------------------------------------
function TetrisHud({ state, settings, highScore, lastClearText, history }) {
  return (
    <div className="tt-hud">
      <div className="tt-hud-section">
        <div className="tt-hud-label">Score</div>
        <div className="tt-hud-score">{state.score.toLocaleString()}</div>
        <div className="tt-hud-meta">
          High <strong>{(highScore || 0).toLocaleString()}</strong>
        </div>
      </div>

      <div className="tt-hud-grid">
        <div className="tt-hud-stat">
          <div className="tt-hud-label">Level</div>
          <div className="tt-hud-stat-value">{state.level}</div>
        </div>
        <div className="tt-hud-stat">
          <div className="tt-hud-label">Lines</div>
          <div className="tt-hud-stat-value">{state.lines}</div>
        </div>
      </div>

      {(settings.showHold || settings.showNext) && (
        <div className="tt-hud-pair">
          {settings.showHold && (
            <div className="tt-hud-section">
              <div className="tt-hud-label">Hold</div>
              <MiniPiece type={state.holdPiece} dim={!state.canHold} />
            </div>
          )}
          {settings.showNext && (
            <div className="tt-hud-section">
              <div className="tt-hud-label">Next</div>
              <div className="tt-hud-queue">
                {state.nextQueue.slice(0, 3).map((t, i) => (
                  <MiniPiece key={i} type={t} />
                ))}
              </div>
            </div>
          )}
        </div>
      )}

      <div className="tt-hud-section tt-hud-history">
        <div className="tt-hud-label">Recent games</div>
        {history && history.length > 0 ? (
          <div className="tt-hud-history-list">
            {history.slice(0, 8).map((h, i) => (
              <HistoryRow key={h.endedAt || i} entry={h} />
            ))}
          </div>
        ) : (
          <div className="tt-history-empty">Your first game will land here.</div>
        )}
      </div>

      {lastClearText && (
        <div className="tt-hud-clear-flash" key={state.lines}>
          {lastClearText}
        </div>
      )}
    </div>
  );
}

function HistoryRow({ entry }) {
  return (
    <div className="tt-history-row">
      <div className="tt-history-score">{(entry.score || 0).toLocaleString()}</div>
      <div className="tt-history-meta">L{entry.level || 1} · {entry.lines || 0} lines · {fmtDuration(entry.duration)}</div>
      <div className="tt-history-when">{fmtAgo(entry.endedAt)}</div>
    </div>
  );
}

// ----------------------------------------------------------
// Mobile control bar
// ----------------------------------------------------------
function TouchControls({ dispatch, onPause }) {
  // Use pointer events so the same handler works for finger + stylus.
  const press = (action) => (e) => {
    e.preventDefault();
    dispatch({ type: action });
  };
  return (
    <div className="tt-touch-bar">
      <button className="tt-touch-btn" onPointerDown={press('MOVE_LEFT')} aria-label="Move left">
        <Icon name="chevron_left" />
      </button>
      <button className="tt-touch-btn" onPointerDown={press('ROTATE_CCW')} aria-label="Rotate counter-clockwise">
        <Icon name="rotate_left" />
      </button>
      <button className="tt-touch-btn" onPointerDown={press('SOFT_DROP')} aria-label="Soft drop">
        <Icon name="south" />
      </button>
      <button className="tt-touch-btn" onPointerDown={press('ROTATE_CW')} aria-label="Rotate clockwise">
        <Icon name="rotate_right" />
      </button>
      <button className="tt-touch-btn" onPointerDown={press('MOVE_RIGHT')} aria-label="Move right">
        <Icon name="chevron_right" />
      </button>
      <button className="tt-touch-btn tt-touch-btn-wide" onPointerDown={press('HARD_DROP')} aria-label="Hard drop">
        <Icon name="vertical_align_bottom" />
        <span>Drop</span>
      </button>
      <button className="tt-touch-btn" onPointerDown={press('HOLD')} aria-label="Hold">
        <Icon name="back_hand" />
      </button>
      <button className="tt-touch-btn" onPointerDown={(e) => { e.preventDefault(); onPause(); }} aria-label="Pause">
        <Icon name="pause" />
      </button>
    </div>
  );
}

// ----------------------------------------------------------
// Overlays
// ----------------------------------------------------------
function StartOverlay({ onStart, onContinue, highScore, hasSaved, stats, history, settings, onSetting }) {
  const helpers = [
    { key: 'ghost',    icon: 'visibility',    label: 'Ghost' },
    { key: 'showHold', icon: 'back_hand',     label: 'Hold' },
    { key: 'showNext', icon: 'queue_music',   label: 'Next' },
  ];
  return (
    <div className="tt-overlay">
      <div className="tt-overlay-card">
        <div className="tt-overlay-brand">
          <Icon name="grid_view" />
          <h2>Block Classic</h2>
        </div>
        <p className="tt-overlay-sub">A cozy take on falling-block puzzles. Five minutes, one good run.</p>

        <div className="tt-overlay-stats">
          <div>
            <div className="tt-hud-label">High score</div>
            <div className="tt-overlay-bignum">{(highScore || 0).toLocaleString()}</div>
          </div>
          <div>
            <div className="tt-hud-label">Lifetime lines</div>
            <div className="tt-overlay-bignum">{(stats.lifetimeLines || 0).toLocaleString()}</div>
          </div>
          <div>
            <div className="tt-hud-label">Games played</div>
            <div className="tt-overlay-bignum">{(stats.gamesPlayed || 0).toLocaleString()}</div>
          </div>
        </div>

        {history && history.length > 0 && (
          <div className="tt-overlay-meta-line">
            Best run <strong>{history[0].score.toLocaleString()}</strong> · {fmtAgo(history[0].endedAt)}
          </div>
        )}

        {/* Helper toggles — turn any off for a harder run. */}
        <div className="tt-helpers">
          <div className="tt-hud-label">Helpers · tap to make it harder</div>
          <div className="tt-helpers-row">
            {helpers.map(h => (
              <button
                key={h.key}
                className={`tt-helper-chip ${settings[h.key] ? 'is-on' : 'is-off'}`}
                onClick={() => onSetting({ [h.key]: !settings[h.key] })}
                aria-pressed={!!settings[h.key]}
                title={settings[h.key] ? `${h.label} on` : `${h.label} off — harder`}
              >
                <Icon name={h.icon} />
                <span>{h.label}</span>
                <span className="tt-helper-state">{settings[h.key] ? 'ON' : 'OFF'}</span>
              </button>
            ))}
          </div>
        </div>

        <div className="tt-overlay-actions">
          {hasSaved && (
            <button className="lh-btn primary" onClick={onContinue}>
              <Icon name="play_arrow" />Continue
            </button>
          )}
          <button className={`lh-btn ${hasSaved ? '' : 'primary'}`} onClick={onStart}>
            <Icon name={hasSaved ? 'restart_alt' : 'play_arrow'} />{hasSaved ? 'New game' : 'Play'}
          </button>
        </div>

        <ControlsHint />
      </div>
    </div>
  );
}

function PauseOverlay({ onResume, onRestart, onQuit }) {
  return (
    <div className="tt-overlay">
      <div className="tt-overlay-card">
        <div className="tt-overlay-brand">
          <Icon name="pause_circle" />
          <h2>Paused</h2>
        </div>
        <p className="tt-overlay-sub">Take your time. Game state is saved.</p>
        <div className="tt-overlay-actions">
          <button className="lh-btn primary" onClick={onResume}>
            <Icon name="play_arrow" />Resume
          </button>
          <button className="lh-btn" onClick={onRestart}>
            <Icon name="restart_alt" />Restart
          </button>
          <button className="lh-btn ghost" onClick={onQuit}>
            <Icon name="exit_to_app" />Quit
          </button>
        </div>
        <ControlsHint />
      </div>
    </div>
  );
}

function GameOverOverlay({ state, isNewHigh, duration, onPlayAgain, onQuit }) {
  return (
    <div className="tt-overlay">
      <div className="tt-overlay-card">
        <div className="tt-overlay-brand">
          <Icon name="emoji_events" />
          <h2>Game over</h2>
        </div>
        {isNewHigh && (
          <div className="tt-overlay-banner">
            <Icon name="star" />New high score!
          </div>
        )}
        <div className="tt-overlay-stats">
          <div>
            <div className="tt-hud-label">Score</div>
            <div className="tt-overlay-bignum">{state.score.toLocaleString()}</div>
          </div>
          <div>
            <div className="tt-hud-label">Lines</div>
            <div className="tt-overlay-bignum">{state.lines}</div>
          </div>
          <div>
            <div className="tt-hud-label">Level</div>
            <div className="tt-overlay-bignum">{state.level}</div>
          </div>
          <div>
            <div className="tt-hud-label">Duration</div>
            <div className="tt-overlay-bignum">{fmtDuration(duration)}</div>
          </div>
        </div>
        <div className="tt-overlay-actions">
          <button className="lh-btn primary" onClick={onPlayAgain}>
            <Icon name="restart_alt" />Play again
          </button>
          <button className="lh-btn ghost" onClick={onQuit}>
            <Icon name="exit_to_app" />Back to menu
          </button>
        </div>
      </div>
    </div>
  );
}

function ControlsHint() {
  return (
    <details className="tt-controls-hint">
      <summary>Controls</summary>
      <div className="tt-controls-grid">
        <kbd>← →</kbd><span>Move</span>
        <kbd>↓</kbd><span>Soft drop</span>
        <kbd>↑ / X</kbd><span>Rotate CW</span>
        <kbd>Z / Ctrl</kbd><span>Rotate CCW</span>
        <kbd>Space</kbd><span>Hard drop</span>
        <kbd>Shift / C</kbd><span>Hold</span>
        <kbd>P / Esc</kbd><span>Pause</span>
      </div>
    </details>
  );
}

// ============================================================
// Main tool
// ============================================================
function TetrisTool() {
  const [state, dispatch] = useReducer(tetris.reducer, null, tetris.initialState);
  const stateRef = useRef(state);
  stateRef.current = state;

  const [settings, setSettings] = useState(DEFAULT_TETRIS_SETTINGS);
  const [highScore, setHighScore] = useState(0);
  const [stats, setStats] = useState({ lifetimeLines: 0, gamesPlayed: 0 });
  const [history, setHistory] = useState([]);
  const [savedGame, setSavedGame] = useState(null); // raw record we can resume
  const [loaded, setLoaded] = useState(false);
  const [isNewHigh, setIsNewHigh] = useState(false);
  const [lastDuration, setLastDuration] = useState(0);
  // Track when the current run began so we can record duration on game-over.
  // Stored as a ref because we never need to re-render from it.
  const gameStartRef = useRef(null);

  // ---- Load persisted data on mount --------------------------------
  useEffect(() => {
    let mounted = true;
    Promise.all([
      db.kv.get(SAVE_KEYS.highScore),
      db.kv.get(SAVE_KEYS.lifetimeLines),
      db.kv.get(SAVE_KEYS.gamesPlayed),
      db.kv.get(SAVE_KEYS.savedGame),
      db.kv.get(SAVE_KEYS.settings),
      db.kv.get(SAVE_KEYS.history),
    ]).then(([hs, ll, gp, sg, st, hist]) => {
      if (!mounted) return;
      setHighScore(hs?.v || 0);
      setStats({ lifetimeLines: ll?.v || 0, gamesPlayed: gp?.v || 0 });
      setSavedGame(sg?.v || null);
      setHistory(Array.isArray(hist?.v) ? hist.v : []);
      if (st?.v) setSettings({ ...DEFAULT_TETRIS_SETTINGS, ...st.v });
      audio.setMuted(!(st?.v?.sound));
      setLoaded(true);
    });
    return () => { mounted = false; };
  }, []);

  useEffect(() => { audio.setMuted(!settings.sound); }, [settings.sound]);

  const persistSettings = useCallback((patch) => {
    setSettings(prev => {
      const next = { ...prev, ...patch };
      db.kv.put({ k: SAVE_KEYS.settings, v: next });
      return next;
    });
  }, []);

  // ---- Game loop: fixed-timestep gravity, rAF-driven --------------
  const rafRef = useRef(null);
  const lastTickRef = useRef(performance.now());

  useEffect(() => {
    if (state.status !== 'playing') {
      if (rafRef.current) cancelAnimationFrame(rafRef.current);
      return;
    }
    lastTickRef.current = performance.now();
    const interval = tetris.gravityMs(state.level);
    const loop = (now) => {
      if (now - lastTickRef.current >= interval) {
        dispatch({ type: 'TICK' });
        lastTickRef.current = now;
      } else if (stateRef.current.lockDelay) {
        // Trigger an extra TICK to commit the lock when delay expires.
        const expired = now - stateRef.current.lockDelay.startedAt >= tetris.LOCK_DELAY_MS;
        if (expired) dispatch({ type: 'TICK' });
      }
      rafRef.current = requestAnimationFrame(loop);
    };
    rafRef.current = requestAnimationFrame(loop);
    return () => cancelAnimationFrame(rafRef.current);
  }, [state.status, state.level]);

  // ---- Line clear animation timer --------------------------------
  useEffect(() => {
    if (state.status !== 'lineclear' || !state.clearAnim) return;
    audio.clear(state.clearAnim.rows.length);
    const t = setTimeout(() => dispatch({ type: 'COMMIT_LINE_CLEAR' }), tetris.CLEAR_ANIM_MS);
    return () => clearTimeout(t);
  }, [state.status, state.clearAnim]);

  // ---- Lock sound ------------------------------------------------
  // Trigger on `lines` increase only when no clear happened (avoids double-sound).
  const prevBoardSig = useRef(null);
  useEffect(() => {
    // Hash current piece presence: when a piece becomes null and status went
    // through, it locked. Easiest: fire when canHold flips from false→true,
    // which only happens on spawn (i.e. just after lock).
    // We side-step it and just fire on TICK→spawn via a no-clear lock.
  }, [state.canHold]);

  // ---- Game over → persist high score, stats; clear saved game ---
  const gameOverProcessedRef = useRef(false);
  useEffect(() => {
    if (state.status !== 'gameover') {
      gameOverProcessedRef.current = false;
      return;
    }
    if (gameOverProcessedRef.current) return;
    gameOverProcessedRef.current = true;
    audio.over();
    const duration = gameStartRef.current ? Date.now() - gameStartRef.current : 0;
    setLastDuration(duration);
    const newHigh = state.score > (highScore || 0);
    setIsNewHigh(newHigh);
    const nextStats = {
      lifetimeLines: (stats.lifetimeLines || 0) + state.lines,
      gamesPlayed: (stats.gamesPlayed || 0) + 1,
    };
    setStats(nextStats);
    db.kv.put({ k: SAVE_KEYS.lifetimeLines, v: nextStats.lifetimeLines });
    db.kv.put({ k: SAVE_KEYS.gamesPlayed, v: nextStats.gamesPlayed });
    if (newHigh) {
      setHighScore(state.score);
      db.kv.put({ k: SAVE_KEYS.highScore, v: state.score });
    }
    // Record the run in history (most recent first, capped).
    const entry = {
      score: state.score,
      lines: state.lines,
      level: state.level,
      duration,
      endedAt: Date.now(),
    };
    const nextHistory = [entry, ...history].slice(0, HISTORY_MAX);
    setHistory(nextHistory);
    db.kv.put({ k: SAVE_KEYS.history, v: nextHistory });
    db.kv.delete(SAVE_KEYS.savedGame);
    setSavedGame(null);
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [state.status]);

  // ---- Save in-progress game on pause + visibility hidden --------
  // Includes gameStartRef so the recorded duration on the next game-over
  // accounts for time spent before the pause.
  useEffect(() => {
    if (state.status === 'paused') {
      const snap = tetris.serialize(state);
      if (snap) {
        snap.startedAt = gameStartRef.current;
        db.kv.put({ k: SAVE_KEYS.savedGame, v: snap });
      }
      setSavedGame(snap);
    }
  }, [state.status, state.score, state.lines, state.level]);

  useEffect(() => {
    const onVisibility = () => {
      if (document.visibilityState === 'hidden') {
        const s = stateRef.current;
        if (s.status === 'playing') {
          dispatch({ type: 'PAUSE' });
          const snap = tetris.serialize(s);
          if (snap) {
            snap.startedAt = gameStartRef.current;
            db.kv.put({ k: SAVE_KEYS.savedGame, v: snap });
          }
        }
      }
    };
    document.addEventListener('visibilitychange', onVisibility);
    return () => document.removeEventListener('visibilitychange', onVisibility);
  }, []);

  // ---- Keyboard input + DAS --------------------------------------
  useEffect(() => {
    const heldKeys = {};         // key → { repeatTimer }
    const repeatTimers = {};

    const action = (e) => {
      const s = stateRef.current;
      const k = e.key;
      // Pause is handled regardless of status (when allowed).
      if (k === 'p' || k === 'P' || k === 'Escape') {
        e.preventDefault();
        if (s.status === 'playing') dispatch({ type: 'PAUSE' });
        else if (s.status === 'paused') dispatch({ type: 'RESUME' });
        return;
      }
      if (s.status !== 'playing') return;

      switch (k) {
        case 'ArrowLeft':
          e.preventDefault();
          dispatch({ type: 'MOVE_LEFT' });
          audio.move();
          startRepeat(k, 'MOVE_LEFT');
          break;
        case 'ArrowRight':
          e.preventDefault();
          dispatch({ type: 'MOVE_RIGHT' });
          audio.move();
          startRepeat(k, 'MOVE_RIGHT');
          break;
        case 'ArrowDown':
          e.preventDefault();
          dispatch({ type: 'SOFT_DROP' });
          startSoftDropRepeat(k);
          break;
        case 'ArrowUp':
        case 'x':
        case 'X':
          e.preventDefault();
          dispatch({ type: 'ROTATE_CW' });
          audio.rotate();
          break;
        case 'z':
        case 'Z':
        case 'Control':
          e.preventDefault();
          dispatch({ type: 'ROTATE_CCW' });
          audio.rotate();
          break;
        case ' ':
          e.preventDefault();
          dispatch({ type: 'HARD_DROP' });
          audio.lock();
          break;
        case 'Shift':
        case 'c':
        case 'C':
          e.preventDefault();
          dispatch({ type: 'HOLD' });
          break;
        case 'r':
        case 'R':
          e.preventDefault();
          if (window.lhDialog) {
            window.lhDialog.confirm({
              title: 'Restart game?',
              message: 'Your current run will end. High score and lifetime stats are kept.',
              confirmLabel: 'Restart',
              icon: 'restart_alt',
            }).then(ok => { if (ok) dispatch({ type: 'START_GAME' }); });
          }
          break;
      }
    };

    function startRepeat(k, action) {
      if (heldKeys[k]) return;
      heldKeys[k] = true;
      const initial = setTimeout(() => {
        const repeat = setInterval(() => {
          if (stateRef.current.status === 'playing') dispatch({ type: action });
        }, DAS_REPEAT);
        repeatTimers[k] = repeat;
      }, DAS_DELAY);
      repeatTimers[k + '_init'] = initial;
    }
    function startSoftDropRepeat(k) {
      if (heldKeys[k]) return;
      heldKeys[k] = true;
      const repeat = setInterval(() => {
        if (stateRef.current.status === 'playing') dispatch({ type: 'SOFT_DROP' });
      }, SOFT_DROP_REPEAT);
      repeatTimers[k] = repeat;
    }
    function stopRepeat(k) {
      heldKeys[k] = false;
      if (repeatTimers[k]) { clearInterval(repeatTimers[k]); delete repeatTimers[k]; }
      if (repeatTimers[k + '_init']) { clearTimeout(repeatTimers[k + '_init']); delete repeatTimers[k + '_init']; }
    }

    const onUp = (e) => stopRepeat(e.key);
    const onDown = (e) => {
      if (e.repeat) {
        // Suppress native key-repeat — we run our own DAS.
        e.preventDefault();
        return;
      }
      action(e);
    };

    window.addEventListener('keydown', onDown);
    window.addEventListener('keyup', onUp);
    window.addEventListener('blur', () => {
      Object.keys(repeatTimers).forEach(k => {
        clearInterval(repeatTimers[k]);
        clearTimeout(repeatTimers[k]);
      });
      Object.keys(heldKeys).forEach(k => heldKeys[k] = false);
    });
    return () => {
      window.removeEventListener('keydown', onDown);
      window.removeEventListener('keyup', onUp);
      Object.values(repeatTimers).forEach(t => { clearInterval(t); clearTimeout(t); });
    };
  }, []);

  // ---- Touch input on the board ----------------------------------
  const boardWrapRef = useRef(null);
  useEffect(() => {
    const el = boardWrapRef.current;
    if (!el) return;
    let touchStart = null;
    let lastMove = null;     // for incremental column shifts
    let hardDropped = false;

    const cellSize = () => {
      const rect = el.getBoundingClientRect();
      return rect.width / tetris.COLS;
    };

    const onStart = (e) => {
      if (stateRef.current.status !== 'playing') return;
      const t = e.touches[0];
      touchStart = { x: t.clientX, y: t.clientY, time: performance.now() };
      lastMove = { x: t.clientX, y: t.clientY };
      hardDropped = false;
    };
    const onMove = (e) => {
      if (!touchStart || stateRef.current.status !== 'playing') return;
      e.preventDefault();
      const t = e.touches[0];
      const cs = cellSize();
      const dx = t.clientX - lastMove.x;
      const dy = t.clientY - lastMove.y;
      // Horizontal: every cellSize px = 1 column.
      if (Math.abs(dx) > cs * 0.6) {
        const steps = Math.trunc(dx / cs);
        for (let i = 0; i < Math.abs(steps); i++) {
          dispatch({ type: steps > 0 ? 'MOVE_RIGHT' : 'MOVE_LEFT' });
        }
        lastMove.x = t.clientX;
      }
      // Vertical down — fast swipe = hard drop, slow = soft drop tick.
      if (dy > cs * 0.6) {
        const speed = dy / (performance.now() - touchStart.time);
        if (speed > 1.2 && !hardDropped && Math.abs(t.clientY - touchStart.y) > cs * 4) {
          dispatch({ type: 'HARD_DROP' });
          audio.lock();
          hardDropped = true;
          touchStart = null;
        } else {
          const steps = Math.trunc(dy / cs);
          for (let i = 0; i < steps; i++) dispatch({ type: 'SOFT_DROP' });
          lastMove.y = t.clientY;
        }
      }
    };
    const onEnd = (e) => {
      if (!touchStart || hardDropped) { touchStart = null; return; }
      const dt = performance.now() - touchStart.time;
      const t = e.changedTouches[0];
      const dx = t.clientX - touchStart.x;
      const dy = t.clientY - touchStart.y;
      const dist = Math.hypot(dx, dy);
      if (dt < 250 && dist < 16) {
        dispatch({ type: 'ROTATE_CW' });
        audio.rotate();
      }
      touchStart = null;
    };

    el.addEventListener('touchstart', onStart, { passive: true });
    el.addEventListener('touchmove', onMove, { passive: false });
    el.addEventListener('touchend', onEnd);
    return () => {
      el.removeEventListener('touchstart', onStart);
      el.removeEventListener('touchmove', onMove);
      el.removeEventListener('touchend', onEnd);
    };
  }, [loaded]);

  // ---- Handlers --------------------------------------------------
  const handleStart = useCallback(() => {
    setIsNewHigh(false);
    gameStartRef.current = Date.now();
    db.kv.delete(SAVE_KEYS.savedGame);
    setSavedGame(null);
    dispatch({ type: 'START_GAME' });
  }, []);

  const handleContinue = useCallback(() => {
    if (!savedGame) return handleStart();
    // Restore the original start time so the recorded duration stays accurate.
    gameStartRef.current = savedGame.startedAt || Date.now();
    dispatch({ type: 'RESUME_SAVED', state: savedGame });
  }, [savedGame, handleStart]);

  const handlePause = useCallback(() => dispatch({ type: 'PAUSE' }), []);
  const handleResume = useCallback(() => dispatch({ type: 'RESUME' }), []);

  const handleQuit = useCallback(() => {
    const s = stateRef.current;
    if (s.status === 'paused') {
      const snap = tetris.serialize(s);
      if (snap) {
        db.kv.put({ k: SAVE_KEYS.savedGame, v: snap });
        setSavedGame(snap);
      }
    }
    dispatch({ type: 'RESET' });
  }, []);

  const handleRestart = useCallback(async () => {
    const ok = await (window.lhDialog?.confirm?.({
      title: 'Restart game?',
      message: 'Your current run will end. High score and lifetime stats are kept.',
      confirmLabel: 'Restart',
      icon: 'restart_alt',
    }) ?? Promise.resolve(true));
    if (!ok) return;
    gameStartRef.current = Date.now();
    db.kv.delete(SAVE_KEYS.savedGame);
    setSavedGame(null);
    dispatch({ type: 'START_GAME' });
  }, []);

  // ---- Render ----------------------------------------------------
  if (!loaded) {
    return <div className="tool"><div className="lh-empty"><Icon name="grid_view" /><div className="title">Loading…</div></div></div>;
  }

  return (
    <div className="tool tt-tool">
      <div className="tt-stage">
        <div className="tt-board-wrap" ref={boardWrapRef}>
          <TetrisBoard state={state} showGhost={settings.ghost} clearAnim={state.clearAnim} />

          {state.status === 'menu' && (
            <StartOverlay
              onStart={handleStart}
              onContinue={handleContinue}
              hasSaved={!!savedGame}
              highScore={highScore}
              stats={stats}
              history={history}
              settings={settings}
              onSetting={persistSettings}
            />
          )}
          {state.status === 'paused' && (
            <PauseOverlay onResume={handleResume} onRestart={handleRestart} onQuit={handleQuit} />
          )}
          {state.status === 'gameover' && (
            <GameOverOverlay
              state={state}
              isNewHigh={isNewHigh}
              duration={lastDuration}
              onPlayAgain={handleStart}
              onQuit={handleQuit}
            />
          )}

          {(state.status === 'playing' || state.status === 'lineclear') && (
            <button
              className="tt-pause-btn"
              onClick={handlePause}
              title="Pause (P / Esc)"
              aria-label="Pause"
            >
              <Icon name="pause" />
            </button>
          )}
        </div>

        <TetrisHud
          state={state}
          settings={settings}
          highScore={highScore}
          lastClearText={state.lastClearText}
          history={history}
        />
      </div>

      {(state.status === 'playing' || state.status === 'lineclear') && (
        <TouchControls dispatch={dispatch} onPause={handlePause} />
      )}
    </div>
  );
}

// ============================================================
// Per-tool settings panel (rendered inside Settings → Tools)
// ============================================================
function TetrisSettings() {
  const [settings, setSettings] = useState(DEFAULT_TETRIS_SETTINGS);
  const [stats, setStats] = useState({ highScore: 0, lifetimeLines: 0, gamesPlayed: 0 });

  useEffect(() => {
    Promise.all([
      db.kv.get(SAVE_KEYS.settings),
      db.kv.get(SAVE_KEYS.highScore),
      db.kv.get(SAVE_KEYS.lifetimeLines),
      db.kv.get(SAVE_KEYS.gamesPlayed),
    ]).then(([s, hs, ll, gp]) => {
      if (s?.v) setSettings({ ...DEFAULT_TETRIS_SETTINGS, ...s.v });
      setStats({
        highScore: hs?.v || 0,
        lifetimeLines: ll?.v || 0,
        gamesPlayed: gp?.v || 0,
      });
    });
  }, []);

  const update = (patch) => {
    setSettings(prev => {
      const next = { ...prev, ...patch };
      db.kv.put({ k: SAVE_KEYS.settings, v: next });
      return next;
    });
  };

  const resetStats = async () => {
    const ok = await window.lhDialog.confirm({
      title: 'Reset Block Classic stats?',
      message: 'High score, lifetime totals, recent-games history, and any in-progress game will be cleared. Settings are kept.',
      confirmLabel: 'Reset stats',
      danger: true,
      icon: 'restart_alt',
    });
    if (!ok) return;
    await Promise.all([
      db.kv.delete(SAVE_KEYS.highScore),
      db.kv.delete(SAVE_KEYS.lifetimeLines),
      db.kv.delete(SAVE_KEYS.gamesPlayed),
      db.kv.delete(SAVE_KEYS.savedGame),
      db.kv.delete(SAVE_KEYS.history),
    ]);
    setStats({ highScore: 0, lifetimeLines: 0, gamesPlayed: 0 });
  };

  return (
    <div className="tt-settings">
      <div className="tt-settings-row">
        <div>
          <div className="tt-settings-label">Ghost piece</div>
          <div className="tt-settings-sub">Faded outline showing where the piece will land.</div>
        </div>
        <Toggle on={settings.ghost} onChange={(v) => update({ ghost: v })} />
      </div>
      <div className="tt-settings-row">
        <div>
          <div className="tt-settings-label">Show hold slot</div>
          <div className="tt-settings-sub">Stash a piece for later (Shift / C).</div>
        </div>
        <Toggle on={settings.showHold} onChange={(v) => update({ showHold: v })} />
      </div>
      <div className="tt-settings-row">
        <div>
          <div className="tt-settings-label">Show next queue</div>
          <div className="tt-settings-sub">Preview the next 3 pieces.</div>
        </div>
        <Toggle on={settings.showNext} onChange={(v) => update({ showNext: v })} />
      </div>
      <div className="tt-settings-row">
        <div>
          <div className="tt-settings-label">Sound effects</div>
          <div className="tt-settings-sub">Soft blips on lock, rotate, and line clear.</div>
        </div>
        <Toggle on={settings.sound} onChange={(v) => update({ sound: v })} />
      </div>

      <div className="tt-settings-stats">
        <div>
          <div className="tt-hud-label">High score</div>
          <div className="tt-settings-stat-value">{stats.highScore.toLocaleString()}</div>
        </div>
        <div>
          <div className="tt-hud-label">Lifetime lines</div>
          <div className="tt-settings-stat-value">{stats.lifetimeLines.toLocaleString()}</div>
        </div>
        <div>
          <div className="tt-hud-label">Games played</div>
          <div className="tt-settings-stat-value">{stats.gamesPlayed.toLocaleString()}</div>
        </div>
      </div>
      <button className="lh-btn danger" onClick={resetStats}>
        <Icon name="delete_sweep" />Reset stats
      </button>
    </div>
  );
}

function Toggle({ on, onChange }) {
  return (
    <div
      className={`tool-row-toggle ${on ? 'is-on' : ''}`}
      onClick={() => onChange(!on)}
      role="switch"
      aria-checked={on}
    >
      <span className="tool-row-toggle-dot" />
    </div>
  );
}

window.TetrisTool = TetrisTool;
window.toolSettingsRegistry = window.toolSettingsRegistry || {};
window.toolSettingsRegistry.tetris = TetrisSettings;
