// tobead — MiniBeadGame
// A lightweight, original arcade maze. A glossy yellow "bead" mascot roams an
// abstract neon maze eating bead-dots. Controls: arrow keys, WASD, on-screen
// joystick / d-pad (wired through the onReady api). Auto-demo roams when idle.
// Pauses when scrolled off-screen for performance.
//
// Two variants share the same engine, selected by the `variant` prop:
//   - "full"    → intro cabinet: detailed 13x15 maze (default)
//   - "compact" → homepage preview: fewer, larger cells so the maze reads
//                 clearly at a small on-screen size.

const { useRef, useEffect, useState, useCallback } = React;

// iOS WebKit: canvas inside 3D/filter ancestors often paints black; intro CSS flattens
// the cabinet, and we also skip offscreen drawImage + repaint after layout settles.
const IS_IOS = typeof window !== 'undefined' && !!window.isIOSWebKit;

// ---- Maze authoring: each maze is a left half that is mirrored to full width ----
function mirrorMaze(half) {
  return half.map(row => {
    const left = row.split('');
    const right = left.slice(0, left.length - 1).reverse(); // mirror all but the seam col
    return left.concat(right).join('');
  });
}

// Full maze (intro) — 7-col half → 13 wide, 15 tall.
const MAZE_FULL = mirrorMaze([
  "#######",
  "#......",
  "#.###.#",
  "#......",
  "#.#.##.",
  "#...#..",
  "#.#.#.#",
  "#.#....",
  "#.#.#.#",
  "#...#..",
  "#.#.##.",
  "#......",
  "#.###.#",
  "#......",
  "#######",
]);

// Compact maze (homepage preview) — 5-col half → 9 wide, 9 tall.
const MAZE_COMPACT = mirrorMaze([
  "#####",
  "#....",
  "#.##.",
  "#....",
  "#.#..",
  "#....",
  "#.##.",
  "#....",
  "#####",
]);

// Per-variant configuration. Sizes derive from `tile`, so bead dots and the
// bead mascot grow with the larger compact tiles automatically.
const VARIANTS = {
  full:    { maze: MAZE_FULL,    tile: 22, power: [[1,1],[11,1],[1,13],[11,13]], start: { c: 6, r: 13 }, speed: 96 },
  compact: { maze: MAZE_COMPACT, tile: 34, power: [[1,1],[7,1],[1,7],[7,7]],     start: { c: 4, r: 7 },  speed: 88 },
};

function makeConfig(variant, tileOverride) {
  const base = VARIANTS[variant] || VARIANTS.full;
  const tile = tileOverride ?? base.tile;
  const rows = base.maze.length;
  const cols = base.maze[0].length;
  return {
    ...base,
    tile,
    rows, cols,
    w: cols * tile,
    h: rows * tile,
    beadSize: Math.max(4, Math.round(tile * 0.2)),
    powerSize: Math.max(6, Math.round(tile * 0.27)),
  };
}

function tileForViewport(viewport, cols, rows, fallback) {
  const { width, height } = viewport.getBoundingClientRect();
  if (width < 8 || height < 8) return fallback;
  return Math.max(10, Math.min(44, Math.floor(Math.min(width / cols, height / rows))));
}

function isPath(cfg, c, r) {
  if (r < 0 || r >= cfg.rows || c < 0 || c >= cfg.cols) return false;
  return cfg.maze[r][c] !== '#';
}

function MiniBeadGame({ onBeads, onReady, paused, variant = 'full', stepSfx = false, fitViewport = false }) {
  const canvasRef = useRef(null);
  const stateRef = useRef(null);
  const rafRef = useRef(0);
  const visibleRef = useRef(true);
  const pausedRef = useRef(paused);
  pausedRef.current = paused;
  const baseVariant = VARIANTS[variant] || VARIANTS.full;
  const [dynTile, setDynTile] = useState(() => baseVariant.tile);

  useEffect(() => {
    if (!fitViewport) {
      setDynTile(baseVariant.tile);
      return;
    }
    const canvas = canvasRef.current;
    if (!canvas) return;
    const viewport = canvas.closest('.maze-viewport');
    if (!viewport) return;
    const cols = baseVariant.maze[0].length;
    const rows = baseVariant.maze.length;
    let timer = 0;
    function measure() {
      clearTimeout(timer);
      timer = setTimeout(() => {
        const t = tileForViewport(viewport, cols, rows, baseVariant.tile);
        setDynTile((prev) => (prev === t ? prev : t));
      }, 40);
    }
    measure();
    let ro = null;
    if (typeof ResizeObserver !== 'undefined') {
      ro = new ResizeObserver(measure);
      ro.observe(viewport);
    }
    requestAnimationFrame(measure);
    return () => {
      if (ro) ro.disconnect();
      clearTimeout(timer);
    };
  }, [fitViewport, variant]);

  // init game state once
  function freshBeads(cfg) {
    const set = new Set();
    for (let r = 0; r < cfg.rows; r++)
      for (let c = 0; c < cfg.cols; c++)
        if (isPath(cfg, c, r)) set.add(c + ',' + r);
    return set;
  }

  useEffect(() => {
    const cfg = makeConfig(variant, fitViewport ? dynTile : null);
    const canvas = canvasRef.current;
    if (!canvas) return;

    const dpr = Math.min(window.devicePixelRatio || 1, 2);

    function bindContext() {
      canvas.width = cfg.w * dpr;
      canvas.height = cfg.h * dpr;
      const ctx = canvas.getContext('2d', { alpha: false });
      ctx.scale(dpr, dpr);
      return ctx;
    }

    let ctx = bindContext();

    function buildWall() {
      if (IS_IOS) return null;
      const w = document.createElement('canvas');
      w.width = cfg.w * dpr;
      w.height = cfg.h * dpr;
      const wctx = w.getContext('2d');
      wctx.scale(dpr, dpr);
      drawWalls(cfg, wctx);
      return w;
    }

    let wall = buildWall();

    // starting tile: bottom corridor center
    const startC = cfg.start.c, startR = cfg.start.r;
    stateRef.current = {
      c: startC, r: startR,
      px: startC * cfg.tile + cfg.tile / 2,
      py: startR * cfg.tile + cfg.tile / 2,
      dir: { x: 0, y: 0 },
      want: { x: 0, y: 0 },
      moving: false,
      tx: 0, ty: 0,
      beads: freshBeads(cfg),
      eaten: 0,
      lastInput: -9999,
      t: 0,
      auto: true,
      mouth: 0,
      lastReport: -1,
    };
    stateRef.current.beads.delete(startC + ',' + startR);

    function paint() {
      render(cfg, ctx, wall, stateRef.current);
    }

    paint();

    // Repaint when the CRT viewport gets size (hero mounts after START; mobile
    // layout CSS can settle a frame late — especially on iOS WebKit).
    let ro = null;
    const viewport = canvas.closest('.maze-viewport');
    if (viewport && typeof ResizeObserver !== 'undefined') {
      ro = new ResizeObserver(() => {
        ctx = bindContext();
        wall = buildWall();
        paint();
      });
      ro.observe(viewport);
    }
    requestAnimationFrame(() => {
      paint();
      requestAnimationFrame(paint);
    });

    let last = performance.now();
    function frame(now) {
      const st = stateRef.current;
      const dt = Math.min(0.05, (now - last) / 1000);
      last = now;
      rafRef.current = requestAnimationFrame(frame);

      if (!pausedRef.current && visibleRef.current) {
        st.t += dt;
        st.auto = (st.t - st.lastInput) > 3.5;
        step(cfg, st, dt, stepSfx);
        if (st.eaten !== st.lastReport) {
          st.lastReport = st.eaten;
          onBeads && onBeads(st.eaten);
        }
      }

      paint();
    }
    rafRef.current = requestAnimationFrame(frame);

    const io = new IntersectionObserver((entries) => {
      visibleRef.current = entries[0].isIntersecting;
    }, { threshold: 0 });
    io.observe(canvas);
    const onVis = () => { if (document.hidden) visibleRef.current = false; };
    document.addEventListener('visibilitychange', onVis);

    onReady && onReady({
      setDir: (x, y) => {
        const st = stateRef.current;
        if (!st) return;
        st.want = { x, y };
        st.lastInput = st.t;
        st.auto = false;
      },
      reset: () => {
        const st = stateRef.current;
        st.beads = freshBeads(cfg);
        st.eaten = 0;
      },
    });

    return () => {
      cancelAnimationFrame(rafRef.current);
      io.disconnect();
      if (ro) ro.disconnect();
      document.removeEventListener('visibilitychange', onVis);
    };
  }, [variant, stepSfx, fitViewport, dynTile]);

  // ---- keyboard ----
  const [engaged, setEngaged] = useState(false);
  useEffect(() => {
    function onKey(e) {
      const st = stateRef.current;
      if (!st) return;
      let d = null;
      switch (e.key) {
        case 'ArrowUp': case 'w': case 'W':    d = { x: 0, y: -1 }; break;
        case 'ArrowDown': case 's': case 'S':  d = { x: 0, y: 1 };  break;
        case 'ArrowLeft': case 'a': case 'A':  d = { x: -1, y: 0 }; break;
        case 'ArrowRight': case 'd': case 'D':  d = { x: 1, y: 0 };  break;
        default: return;
      }
      // only hijack scroll when the game is engaged (hovered/focused)
      if (engaged && (e.key.startsWith('Arrow'))) e.preventDefault();
      st.want = d; st.lastInput = st.t; st.auto = false;
    }
    window.addEventListener('keydown', onKey, { passive: false });
    return () => window.removeEventListener('keydown', onKey);
  }, [engaged]);

  const cols = (VARIANTS[variant] || VARIANTS.full).maze[0].length;
  const rows = (VARIANTS[variant] || VARIANTS.full).maze.length;
  const canvasStyle = fitViewport
    ? { width: '100%', height: '100%', display: 'block', outline: 'none', imageRendering: 'pixelated' }
    : {
        width: '100%', height: '100%', maxWidth: '100%', maxHeight: '100%',
        objectFit: 'contain', aspectRatio: `${cols} / ${rows}`,
        display: 'block', outline: 'none', imageRendering: 'pixelated',
      };

  return (
    <canvas
      ref={canvasRef}
      className={`maze-canvas maze-canvas--${variant}${fitViewport ? ' maze-canvas--fit' : ''}`}
      tabIndex={0}
      onMouseEnter={() => setEngaged(true)}
      onMouseLeave={() => setEngaged(false)}
      onFocus={() => setEngaged(true)}
      onBlur={() => setEngaged(false)}
      style={canvasStyle}
      aria-label="Playable bead maze"
    />
  );
}
window.MiniBeadGame = MiniBeadGame;

// ---- movement (tile-stepping with turns only at tile centers) ----
function step(cfg, st, dt, stepSfx) {
  if (!st.moving) {
    // at a tile center → decide direction
    decide(cfg, st);
    if (st.dir.x !== 0 || st.dir.y !== 0) {
      st.moving = true;
      if (stepSfx && window.AudioManager) window.AudioManager.playMove();
      st.tx = (st.c + st.dir.x) * cfg.tile + cfg.tile / 2;
      st.ty = (st.r + st.dir.y) * cfg.tile + cfg.tile / 2;
    }
  }
  if (st.moving) {
    const dist = cfg.speed * dt;
    st.px += st.dir.x * dist;
    st.py += st.dir.y * dist;
    st.mouth += dt * 12;
    // reached/overshoot target?
    const reachedX = st.dir.x === 0 || (st.dir.x > 0 ? st.px >= st.tx : st.px <= st.tx);
    const reachedY = st.dir.y === 0 || (st.dir.y > 0 ? st.py >= st.ty : st.py <= st.ty);
    if (reachedX && reachedY) {
      st.px = st.tx; st.py = st.ty;
      st.c += st.dir.x; st.r += st.dir.y;
      st.moving = false;
      eat(cfg, st);
    }
  }
}

function decide(cfg, st) {
  const opts = [];
  const dirs = [ {x:1,y:0},{x:-1,y:0},{x:0,y:1},{x:0,y:-1} ];
  for (const d of dirs) if (isPath(cfg, st.c + d.x, st.r + d.y)) opts.push(d);

  // prefer the queued want if open
  if ((st.want.x || st.want.y) && isPath(cfg, st.c + st.want.x, st.r + st.want.y)) {
    st.dir = { ...st.want };
    return;
  }
  if (st.auto) {
    // AI roam: avoid reversing unless dead-end; bias toward bead-rich cells
    const rev = { x: -st.dir.x, y: -st.dir.y };
    let choices = opts.filter(d => !(d.x === rev.x && d.y === rev.y));
    if (choices.length === 0) choices = opts;
    // prefer a neighbor that still has a bead
    const withBead = choices.filter(d => st.beads.has((st.c + d.x) + ',' + (st.r + d.y)));
    const pool = withBead.length ? withBead : choices;
    st.dir = pool[Math.floor(Math.random() * pool.length)] || { x: 0, y: 0 };
    return;
  }
  // not auto: keep going straight if possible, else stop
  if (st.dir.x || st.dir.y) {
    if (isPath(cfg, st.c + st.dir.x, st.r + st.dir.y)) return; // keep dir
  }
  st.dir = { x: 0, y: 0 };
}

function eat(cfg, st) {
  const key = st.c + ',' + st.r;
  if (st.beads.has(key)) {
    st.beads.delete(key);
    const isPower = cfg.power.some(([c, r]) => c === st.c && r === st.r);
    st.eaten += isPower ? 5 : 1;
  }
  if (st.beads.size === 0) {
    // refill — endless loop
    for (let r = 0; r < cfg.rows; r++)
      for (let c = 0; c < cfg.cols; c++)
        if (isPath(cfg, c, r)) st.beads.add(c + ',' + r);
    st.beads.delete(st.c + ',' + st.r);
  }
}

// ---- rendering ---- classic arcade screen: navy/electric-blue maze, cyan glow, yellow bead mascot
const COL = { wall: '#0A1A4D', wallStroke: '#2D6FE0', wallGlow: 'rgba(45,225,255,0.6)', bead: '#FFFFFF', power: '#FFD23D', powerGlow: '#FFD23D', body0: '#FFE070', body1: '#FFC91E', bodyGlow: 'rgba(255,210,61,0.9)', hole: 'rgba(0,0,0,0.5)' };

function drawWalls(cfg, ctx) {
  const { w, h, rows, cols, tile } = cfg;
  ctx.clearRect(0, 0, w, h);
  // faint cyan inner grid
  ctx.strokeStyle = 'rgba(45,225,255,0.08)';
  ctx.lineWidth = 1;
  for (let x = 0; x <= cols; x++) { ctx.beginPath(); ctx.moveTo(x*tile,0); ctx.lineTo(x*tile,h); ctx.stroke(); }
  for (let y = 0; y <= rows; y++) { ctx.beginPath(); ctx.moveTo(0,y*tile); ctx.lineTo(w,y*tile); ctx.stroke(); }

  // neon maze walls (navy fill, violet glowing edge) — hard pixel squares
  for (let r = 0; r < rows; r++) {
    for (let c = 0; c < cols; c++) {
      if (cfg.maze[r][c] !== '#') continue;
      const x = c * tile, y = r * tile;
      ctx.fillStyle = COL.wall;
      ctx.fillRect(x + 2, y + 2, tile - 4, tile - 4);
      ctx.save();
      ctx.shadowColor = COL.wallGlow;
      ctx.shadowBlur = 7;
      ctx.strokeStyle = COL.wallStroke;
      ctx.lineWidth = 2;
      ctx.strokeRect(x + 3, y + 3, tile - 6, tile - 6);
      ctx.restore();
    }
  }
}

function render(cfg, ctx, wall, st) {
  const { w, h, tile } = cfg;
  ctx.clearRect(0, 0, w, h);
  if (wall) ctx.drawImage(wall, 0, 0, w, h);
  else drawWalls(cfg, ctx);

  // beads
  const dot = cfg.beadSize;
  for (const key of st.beads) {
    const [c, r] = key.split(',').map(Number);
    const cx = c * tile + tile / 2, cy = r * tile + tile / 2;
    const isPower = cfg.power.some(([pc, pr]) => pc === c && pr === r);
    if (isPower) {
      const pulse = 1 + Math.sin(st.t * 5) * 0.25;
      ctx.save();
      ctx.shadowColor = COL.powerGlow; ctx.shadowBlur = 12;
      ctx.fillStyle = COL.power;
      const s = cfg.powerSize * pulse;
      ctx.fillRect(cx - s/2, cy - s/2, s, s);  // square power-bead (pixel)
      ctx.restore();
    } else {
      ctx.fillStyle = COL.bead;
      ctx.fillRect(cx - dot/2, cy - dot/2, dot, dot);  // square pixel dot
    }
  }

  // player — violet glowing bead with chomp
  const px = st.px, py = st.py;
  let ang = 0;
  if (st.dir.x === 1) ang = 0;
  else if (st.dir.x === -1) ang = Math.PI;
  else if (st.dir.y === 1) ang = Math.PI / 2;
  else if (st.dir.y === -1) ang = -Math.PI / 2;
  const moving = st.moving && (st.dir.x || st.dir.y);
  const mouth = moving ? (Math.abs(Math.sin(st.mouth)) * 0.32 + 0.04) : 0.06;
  const R = tile / 2 - 2.5;

  ctx.save();
  ctx.translate(px, py);
  ctx.rotate(ang);
  ctx.shadowColor = COL.bodyGlow;
  ctx.shadowBlur = 14;
  const grad = ctx.createRadialGradient(-R*0.3, -R*0.3, R*0.2, 0, 0, R);
  grad.addColorStop(0, COL.body0);
  grad.addColorStop(1, COL.body1);
  ctx.fillStyle = grad;
  ctx.beginPath();
  ctx.moveTo(0, 0);
  ctx.arc(0, 0, R, mouth * Math.PI, (2 - mouth) * Math.PI);
  ctx.closePath();
  ctx.fill();
  ctx.restore();
  // bead hole
  ctx.save();
  ctx.translate(px, py);
  ctx.fillStyle = COL.hole;
  ctx.beginPath(); ctx.arc(0, 0, R * 0.26, 0, Math.PI * 2); ctx.fill();
  ctx.restore();
}

function roundRect(ctx, x, y, w, h, r) {
  ctx.beginPath();
  ctx.moveTo(x + r, y);
  ctx.arcTo(x + w, y, x + w, y + h, r);
  ctx.arcTo(x + w, y + h, x, y + h, r);
  ctx.arcTo(x, y + h, x, y, r);
  ctx.arcTo(x, y, x + w, y, r);
  ctx.closePath();
}
