// pixel-runner.jsx — Tiny Mario-style pixel runner, recolored to the page accent.
// Renders to a canvas. Mario auto-runs, auto-jumps over Goombas. Pure CSS-pixel
// look (image-rendering: pixelated, integer scale). No sprite sheet — drawn
// from compact 1-char-per-pixel string arrays.

(function () {
  const { useEffect, useRef, useMemo } = React;

  // ── Sprites (1 char = 1 pixel; '.' = transparent) ────────────────────────
  // 16x16 — Mario stand
  const SPR_STAND = [
    "................",
    "................",
    "....AAAAA.......",
    "...AAAAAAAA.....",
    "...BBBcccBc.....",
    "..BcBcccccBc....",
    "..BcBBccccccc...",
    "..BBccccBBB.....",
    "....AAAAAA......",
    "...AABAAABA.....",
    "..AAABAAABAA....",
    ".CCABBABBABCC...",
    ".CCABBBBBBABCC..",
    ".CCBBBBBBBBBCC..",
    "...BBB...BBB....",
    "..DDD.....DDD...",
  ];
  // 16x16 — Mario run (frame 1)
  const SPR_RUN1 = [
    "................",
    "................",
    "....AAAAA.......",
    "...AAAAAAAA.....",
    "...BBBcccBc.....",
    "..BcBcccccBc....",
    "..BcBBccccccc...",
    "..BBccccBBB.....",
    "...AAABAAA......",
    "..AAABAAABA.....",
    "..AAABBABBA.....",
    "..AABBBBBB......",
    "...BBBBBBB......",
    "...BBB.BBBB.....",
    "..DDD....DDD....",
    "..DD......DD....",
  ];
  // 16x16 — Mario run (frame 2) — opposite legs
  const SPR_RUN2 = [
    "................",
    "................",
    "....AAAAA.......",
    "...AAAAAAAA.....",
    "...BBBcccBc.....",
    "..BcBcccccBc....",
    "..BcBBccccccc...",
    "..BBccccBBB.....",
    "....AAAAAB......",
    "...AABAAABA.....",
    "..AABBABBAA.....",
    "..BBBBBBBB......",
    ".BBBBBBBB.......",
    "BBBB.BBB........",
    "DDD....DDD......",
    "DD......DD......",
  ];
  // 16x16 — Mario jump
  const SPR_JUMP = [
    "................",
    "................",
    "....AAAAA.......",
    "...AAAAAAAA.....",
    "...BBBcccBc.....",
    "..BcBcccccBc....",
    "..BcBBccccccc...",
    "..BBccccBBB.....",
    "..A.AAAAAA.A....",
    ".AA.BAAAAAB.AA..",
    "AAA.BBABBAB.AAA.",
    "DDD.BBBBBBB.DDD.",
    "....BBBBBBB.....",
    "....BBB.BBB.....",
    "...BBB...BBB....",
    "..DDD.....DDD...",
  ];

  // 16x16 — Goomba
  const SPR_GOOMBA = [
    "................",
    "................",
    "....EEEEEEE.....",
    "...EEEEEEEEE....",
    "..EEEEEEEEEEE...",
    "..FFFFEEEFFFF...",
    "..FBFFEEEFFBF...",
    "..FBBFEEEFBBF...",
    "..FFFFEEEFFFF...",
    "..EEEEEEEEEEE...",
    "..EEEEEEEEEEE...",
    "...EEEEEEEEE....",
    "...DD.....DD....",
    "..DDD.....DDD...",
    "..DD.......DD...",
    "................",
  ];

  // 16x16 — Pikachu: tall pointy ears, round face, chubby body
  const SPR_PIKACHU = [
    "..E........E....",  // ear tips
    "..E........E....",
    ".EEB......BEE...",  // ear black tips
    ".EE........EE...",
    "..EEEEEEEEEE....",  // head
    ".EEEEEEEEEEEE...",
    ".EE.EE..EE.EE...",  // eyes
    ".EEEEEEEEEEEE...",
    ".EEFEEEEEEFEEE..",  // cheeks
    "..EEEEEEEEEE....",
    "..FFFFFFFFFF....",  // body
    ".FFFFFFFFFFFF...",
    ".FFFFFFFFFFFF...",
    "..FFFF..FFFF....",
    "..FFF....FFF....",
    "................",
  ];

  // 16x16 — Charizard: spread wings, stocky body, tail
  const SPR_CHARIZARD = [
    "EE..........EE..",  // wing tips
    "EEE........EEE..",  // wings
    "EEEE..EE..EEEE..",  // wings spread
    ".EEE.EEEE.EEE...",
    "..EEEEEEEEEEE...",  // neck
    "..EFFFFFFFF E...",  // body top
    "..FFFFFFFFFF....",
    "..FFFFFFFFFF....",
    "..FFBBBBBFFF....",  // belly
    "..FFFFFFFFFF....",
    "...FFFFFFFF.....",
    "...FFF..FFF.....",  // legs
    "...FF....FF.....",
    "....E....EEE....",  // tail
    ".........EEEE...",
    "..........EEE...",
  ];

  const ENEMY_SPRITES = [SPR_GOOMBA, SPR_PIKACHU, SPR_CHARIZARD];

  // 16x16 — Question block
  const SPR_QBLOCK = [
    "GGGGGGGGGGGGGGGG",
    "GHHHHHHHHHHHHHHG",
    "GH...HHHHHH...HG",
    "GH..HHHHHH....HG",
    "GH..HHH...HHH.HG",
    "GH..HH....HHH.HG",
    "GH....HH..HH..HG",
    "GH...HH...HH..HG",
    "GH..HH....HH..HG",
    "GH...HHHHHHHH.HG",
    "GH...HHHHHH...HG",
    "GH....HHHH....HG",
    "GH....HHHH....HG",
    "GH...HH..HH...HG",
    "GHHHHHHHHHHHHHHG",
    "GGGGGGGGGGGGGGGG",
  ];

  // 16x16 — Spent / used block (rivets, no question mark)
  const SPR_QBLOCK_USED = [
    "GGGGGGGGGGGGGGGG",
    "GHHHHHHHHHHHHHHG",
    "GH.HHHHHHHHHHH.G",
    "GHHHHHHHHHHHHHHG",
    "GHHHHHHHHHHHHHHG",
    "GHHHHHHHHHHHHHHG",
    "GHHHHHHHHHHHHHHG",
    "GHHHHHHHHHHHHHHG",
    "GHHHHHHHHHHHHHHG",
    "GHHHHHHHHHHHHHHG",
    "GHHHHHHHHHHHHHHG",
    "GHHHHHHHHHHHHHHG",
    "GHHHHHHHHHHHHHHG",
    "GH.HHHHHHHHHHH.G",
    "GHHHHHHHHHHHHHHG",
    "GGGGGGGGGGGGGGGG",
  ];

  // 8x8 Coin sprite (drawn at SCALE size)
  const SPR_COIN = [
    "..GGGG..",
    ".GHHHHG.",
    "GH.HH.HG",
    "GH.HH.HG",
    "GH.HH.HG",
    "GH.HH.HG",
    ".GHHHHG.",
    "..GGGG..",
  ];

  // 16x16 — Pipe top
  const SPR_PIPE_TOP = [
    "JJJJJJJJJJJJJJJJ",
    "JKKKKKKKKKKKKKKJ",
    "JKKLLLLLLLLLLKKJ",
    "JKKLLLLLLLLLLKKJ",
    "JKKKKKKKKKKKKKKJ",
    "JJJJJJJJJJJJJJJJ",
    ".JKKKKKKKKKKKKJ.",
    ".JKKLLLLLLLLKKJ.",
    ".JKKLLLLLLLLKKJ.",
    ".JKKLLLLLLLLKKJ.",
    ".JKKLLLLLLLLKKJ.",
    ".JKKLLLLLLLLKKJ.",
    ".JKKLLLLLLLLKKJ.",
    ".JKKLLLLLLLLKKJ.",
    ".JKKLLLLLLLLKKJ.",
    ".JJJJJJJJJJJJJJ.",
  ];

  // ── Palette (auto-derived from accent so it always matches) ─────────────
  function makePalette(accent) {
    // Convert accent to hsl-ish lighter/darker via simple shading
    const c = (h) => h.replace("#","");
    const hex2 = (h) => parseInt(c(h), 16);
    const r = (hex2(accent) >> 16) & 255;
    const g = (hex2(accent) >> 8) & 255;
    const b = hex2(accent) & 255;
    const mix = (a, t) => Math.round(a * (1 - t));
    const tint = (a, t) => Math.round(a + (255 - a) * t);
    const rgb = (r,g,b) => `rgb(${r},${g},${b})`;
    const main = rgb(r, g, b);
    const dark = rgb(mix(r,0.45), mix(g,0.45), mix(b,0.45));
    const darker = rgb(mix(r,0.7), mix(g,0.7), mix(b,0.7));
    const light = rgb(tint(r,0.5), tint(g,0.5), tint(b,0.5));
    const lighter = rgb(tint(r,0.8), tint(g,0.8), tint(b,0.8));
    return {
      "A": main,        // hat / shirt outline
      "B": dark,        // overalls
      "c": lighter,     // skin highlight
      "C": main,        // shoes
      "D": darker,      // shadow / boots
      "E": main,        // goomba body
      "F": lighter,     // goomba feet
      "G": darker,      // block border
      "H": main,        // block fill
      "J": darker,      // pipe outline
      "K": main,        // pipe body
      "L": dark,        // pipe shading
    };
  }

  function PixelRunner({ accent = "#00ff88", height = 64, intensity = 1 }) {
    const ref = useRef(null);
    const wrapRef = useRef(null);
    const stateRef = useRef(null);
    const palette = useMemo(() => makePalette(accent), [accent]);

    useEffect(() => {
      const cv = ref.current; if (!cv) return;
      const ctx = cv.getContext("2d");
      ctx.imageSmoothingEnabled = false;

      const SCALE = 2; // pixel scale
      const SPR = 16;  // sprite source size
      const TILE = SPR * SCALE; // 48px on screen

      // World height in pixels (canvas height) is fixed by `height` prop;
      // the ground line sits a few px from the bottom.
      const groundY = () => cv.height - 8;

      // Build a randomized world: Mario, Goombas, ? blocks, pipes — all
      // spaced apart with a minimum gap so collisions feel earned, not
      // packed together. Mix obstacles randomly across the world length.
      const buildWorld = (W) => {
        const items = []; // scenery
        const enemies = [];
        const minGap = TILE * 4.5; // ≥ 4.5 tiles between any two things
        const startSafe = TILE * 6; // empty runway at world start
        const endSafe = TILE * 4;
        const used = []; // already-placed x positions (for spacing checks)

        const farEnough = (x) => used.every(u => Math.abs(u - x) > minGap);

        // Try to place ~10–14 things across the world
        const targetCount = 10 + Math.floor(Math.random() * 5);
        let attempts = 0;
        while (used.length < targetCount && attempts < 400) {
          attempts++;
          const x = startSafe + Math.random() * (W - startSafe - endSafe);
          if (!farEnough(x)) continue;
          used.push(x);
          // weighted pick: 50% goomba, 30% qblock, 20% pipe
          const r = Math.random();
          if (r < 0.5) {
            const spriteIdx = Math.floor(Math.random() * ENEMY_SPRITES.length);
            enemies.push({ x, alive: true, spriteIdx });
          } else if (r < 0.8) {
            // qblock with small height variation so jump arcs differ
            const heightMul = 2.0 + Math.random() * 0.4; // 2.0–2.4 tiles up
            items.push({ kind: "qblock", x, y: groundY() - TILE * heightMul });
          } else {
            items.push({ kind: "pipe", x, y: groundY() - TILE });
          }
        }
        return { scenery: items, enemies };
      };

      const resize = () => {
        const r = wrapRef.current.getBoundingClientRect();
        const dpr = 1; // we want crisp integer pixels — no DPR scaling
        cv.width = Math.max(1, Math.floor(r.width));
        cv.height = Math.max(1, Math.floor(r.height));
        cv.style.width = r.width + "px";
        cv.style.height = r.height + "px";
        ctx.imageSmoothingEnabled = false;

        // World loop length — longer so spacing feels like a level
        const W = Math.max(cv.width * 3.5, 1600);
        const world = buildWorld(W);
        stateRef.current = {
          W,
          cameraX: 0,
          marioX: 80,
          marioY: groundY() - TILE,
          vy: 0,
          onGround: true,
          frame: 0,
          time: 0,
          enemies: world.enemies,
          scenery: world.scenery,
        };
      };
      resize();
      const ro = new ResizeObserver(resize);
      ro.observe(wrapRef.current);

      const drawSprite = (sprite, x, y) => {
        for (let py = 0; py < sprite.length; py++) {
          const row = sprite[py];
          for (let px = 0; px < row.length; px++) {
            const ch = row[px];
            if (ch === "." || !palette[ch]) continue;
            ctx.fillStyle = palette[ch];
            ctx.fillRect(x + px * SCALE, y + py * SCALE, SCALE, SCALE);
          }
        }
      };

      let raf;
      let last = performance.now();
      const tick = (now) => {
        const dt = Math.min(48, now - last); last = now;
        const st = stateRef.current;
        if (!st) { raf = requestAnimationFrame(tick); return; }

        // Update
        const speed = 0.13 * intensity; // px/ms
        st.cameraX = (st.cameraX + speed * dt) % st.W;
        st.time += dt;
        st.frame = Math.floor(st.time / 90) % 2;

        // Helper: closest obstacle ahead of Mario (Goomba OR pipe OR qblock)
        // Returns {dx, kind, ref} or null. dx is positive distance ahead in px.
        const screenX = (worldX) => {
          const x = worldX - st.cameraX;
          return ((x % st.W) + st.W) % st.W;
        };
        const findAhead = () => {
          let best = null;
          st.enemies.forEach((e) => {
            if (!e.alive) return;
            const dx = screenX(e.x) - st.marioX;
            if (dx > 0 && dx < 200 && (!best || dx < best.dx)) best = { dx, kind: "goomba", ref: e };
          });
          st.scenery.forEach((s) => {
            if (s.kind === "pipe") {
              const dx = screenX(s.x) - st.marioX;
              if (dx > 0 && dx < 200 && (!best || dx < best.dx)) best = { dx, kind: "pipe", ref: s };
            } else if (s.kind === "qblock" && !s.spent) {
              // Treat unspent ? blocks as targets ~50% of the time
              if (s.targetMe === undefined) s.targetMe = Math.random() < 0.55;
              if (s.targetMe) {
                const dx = screenX(s.x) - st.marioX;
                if (dx > 0 && dx < 200 && (!best || dx < best.dx)) best = { dx, kind: "qblock", ref: s };
              }
            }
          });
          return best;
        };

        // Auto-jump when something is close ahead. For ? blocks, sometimes
        // jump up to bonk it (only if Mario will actually fit underneath).
        const ahead = findAhead();
        if (ahead && st.onGround && ahead.dx < (ahead.kind === "pipe" ? 70 : ahead.kind === "qblock" ? 60 : 90)) {
          st.vy = ahead.kind === "pipe" ? -0.78 : ahead.kind === "qblock" ? -0.86 : -0.62;
          st.onGround = false;
        }
        // Gravity
        if (!st.onGround) {
          st.vy += 0.0018 * dt;
          st.marioY += st.vy * dt;
          const gy = groundY() - TILE;
          if (st.marioY >= gy) {
            st.marioY = gy; st.vy = 0; st.onGround = true;
          }
        }

        // ── Collision: stomp Goombas, bonk ? blocks, land on pipe top ──
        const marioFootY = st.marioY + TILE;
        const marioHeadY = st.marioY;
        const marioRight = st.marioX + TILE - 4;
        const marioLeft = st.marioX + 4;

        st.enemies.forEach((e) => {
          if (!e.alive) return;
          const ex = screenX(e.x);
          const overlapX = marioRight > ex + 2 && marioLeft < ex + TILE - 2;
          if (!overlapX) return;
          const enemyTop = groundY() - TILE;
          const overlapY = marioFootY > enemyTop + 2 && marioHeadY < enemyTop + TILE - 2;
          if (!overlapY) return;
          // Stomp: coming down on top of enemy
          if (st.vy >= 0 && marioFootY <= enemyTop + TILE * 0.6) {
            e.alive = false;
            e.respawnAt = st.time + 4000;
            st.vy = -0.5;
            st.marioY = enemyTop - TILE;
          } else {
            // Side hit — push camera back and force jump
            st.cameraX = Math.max(0, st.cameraX - TILE * 0.6);
            if (st.vy >= -0.1) { st.vy = -0.92; st.onGround = false; }
          }
        });

        // Question block bonk: if head crosses block from below
        st.scenery.forEach((s) => {
          if (s.kind !== "qblock") return;
          const sx = screenX(s.x);
          const overlapX = marioRight > sx + 2 && marioLeft < sx + TILE - 2;
          if (!overlapX) return;
          if (st.vy < 0 && marioHeadY < s.y + TILE && marioHeadY > s.y + TILE * 0.4) {
            // Bounce off the bottom
            st.vy = 0.2;
            st.marioY = s.y + TILE;
            // Briefly push the block up
            s.bumpAt = st.time;
            // Spawn a coin if the block hasn't been spent yet
            if (!s.spent) {
              s.spent = true;
              if (!st.coins) st.coins = [];
              st.coins.push({
                x: s.x + TILE * 0.5,
                y: s.y,
                vy: -1.6,
                spawnedAt: st.time,
                collected: false,
                collectedAt: 0,
              });
              if (!st.score) st.score = 0;
            }
          }
        });

        // Pipe collision
        st.scenery.forEach((s) => {
          if (s.kind !== "pipe") return;
          const sx = screenX(s.x);
          const overlapX = marioRight > sx + 2 && marioLeft < sx + TILE - 2;
          const prevFootY = marioFootY - st.vy * dt;

          // Land on top of pipe
          if (overlapX && marioFootY >= s.y && prevFootY <= s.y + 4) {
            st.marioY = s.y - TILE;
            st.vy = 0;
            st.onGround = true;
            return;
          }

          // Side wall: Mario hits left face of pipe
          const overlapY = marioFootY > s.y + 4 && marioHeadY < s.y + TILE * 2 - 4;
          const hittingLeft = overlapY && marioRight > sx && marioRight < sx + TILE * 0.5;
          if (hittingLeft) {
            // Push camera back so Mario can't enter pipe
            st.cameraX = Math.max(0, st.cameraX - (marioRight - sx) - 1);
            if (st.vy >= -0.1) { st.vy = -0.92; st.onGround = false; }
          }

          // Land on top of pipe (standard)
          const overlapXFull = marioRight > sx && marioLeft < sx + TILE;
          if (overlapXFull && (st.vy >= 0 || prevFootY <= s.y) && marioFootY >= s.y && marioHeadY < s.y + TILE) {
            st.marioY = s.y - TILE;
            st.vy = 0;
            st.onGround = true;
          }
        });
        st.enemies.forEach((e) => {
          if (!e.alive && st.time > e.respawnAt) {
            e.alive = true;
            // respawn far ahead
            e.x = (st.cameraX + st.marioX + cv.width * 1.5 + Math.random() * 200) % st.W;
          }
        });

        // ── Draw ──────────────────────────────────────────────
        ctx.clearRect(0, 0, cv.width, cv.height);

        // Ground line (pixel-thick)
        ctx.fillStyle = palette.D;
        ctx.fillRect(0, groundY(), cv.width, 2);
        // dotted under-ground texture
        ctx.fillStyle = palette.A;
        for (let x = (-st.cameraX % 12); x < cv.width; x += 12) {
          ctx.globalAlpha = 0.4;
          ctx.fillRect(x, groundY() + 4, 3, 1);
        }
        ctx.globalAlpha = 1;

        // Scenery (parallax = same as world; loops with cameraX)
        st.scenery.forEach((s) => {
          const sx = ((s.x - st.cameraX) % st.W + st.W) % st.W;
          if (sx > -TILE && sx < cv.width + TILE) {
            if (s.kind === "qblock") {
              // little bounce when bumped
              const t = s.bumpAt ? Math.max(0, 200 - (st.time - s.bumpAt)) : 0;
              const dy = t > 0 ? -Math.sin((200 - t) / 200 * Math.PI) * 6 : 0;
              drawSprite(s.spent ? SPR_QBLOCK_USED : SPR_QBLOCK, sx, s.y + dy);
            }
            if (s.kind === "pipe") drawSprite(SPR_PIPE_TOP, sx, s.y);
          }
        });

        // Enemies
        st.enemies.forEach((e) => {
          if (!e.alive) return;
          const ex = ((e.x - st.cameraX) % st.W + st.W) % st.W;
          if (ex > -TILE && ex < cv.width + TILE) {
            const spr = ENEMY_SPRITES[e.spriteIdx ?? 0];
            drawSprite(spr, ex, groundY() - TILE);
          }
        });

        // Coins: physics, collection, draw
        if (st.coins && st.coins.length) {
          // cap list size
          if (st.coins.length > 24) st.coins.splice(0, st.coins.length - 24);
          const marioCx = st.marioX + TILE * 0.5;
          const marioCy = st.marioY + TILE * 0.5;
          for (let i = 0; i < st.coins.length; i++) {
            const co = st.coins[i];
            if (co.collected) {
              // float up + fade for ~400ms then ignore
              continue;
            }
            // physics in world coords
            co.vy += 0.06; // gravity
            co.y += co.vy;
            // collection check vs Mario (screen-space)
            const csx = screenX(co.x);
            const dx = csx - marioCx;
            const dy = co.y - marioCy;
            if (Math.abs(dx) < TILE * 0.7 && Math.abs(dy) < TILE * 0.7) {
              co.collected = true;
              co.collectedAt = st.time;
              st.score = (st.score || 0) + 1;
              continue;
            }
            // expire if it falls past ground without being grabbed
            if (co.y > groundY() + TILE) {
              st.coins.splice(i, 1); i--;
            }
          }
          // Draw active coins (spinning via frame parity)
          st.coins.forEach((co) => {
            if (co.collected) {
              // Brief "+1" rise
              const age = st.time - co.collectedAt;
              if (age > 500) return;
              const alpha = 1 - age / 500;
              const csx = screenX(co.x);
              ctx.globalAlpha = Math.max(0, alpha);
              ctx.fillStyle = palette.G || "#fff";
              ctx.font = "bold 12px ui-monospace, Menlo, monospace";
              ctx.textAlign = "center";
              ctx.fillText("+1", csx, co.y - age * 0.04);
              ctx.globalAlpha = 1;
              return;
            }
            const csx = screenX(co.x);
            // tiny spin: alternate sprite columns each frame for shimmer
            drawSprite(SPR_COIN, csx - TILE * 0.5, co.y - TILE * 0.5);
          });
          // prune the long-faded collected coins
          st.coins = st.coins.filter(c => !(c.collected && st.time - c.collectedAt > 500));
        }

        // Mario
        let marioSprite;
        if (!st.onGround) marioSprite = SPR_JUMP;
        else marioSprite = st.frame === 0 ? SPR_RUN1 : SPR_RUN2;
        drawSprite(marioSprite, st.marioX, st.marioY);

        // HUD: coin counter (top-left)
        if (st.score && st.score > 0) {
          drawSprite(SPR_COIN, 8, 12);
          ctx.fillStyle = palette.G || "#fff";
          ctx.font = "bold 14px ui-monospace, Menlo, monospace";
          ctx.textAlign = "left";
          ctx.textBaseline = "middle";
          ctx.fillText("× " + String(st.score).padStart(2, "0"), 28, 20);
        }

        raf = requestAnimationFrame(tick);
      };
      raf = requestAnimationFrame(tick);

      return () => { cancelAnimationFrame(raf); ro.disconnect(); };
    }, [accent, intensity, palette]);

    // Extend the canvas upward so jumps have room to peek above the band.
    // The canvas is sized from the wrapper, so we render at `height + extra`
    // and pull the wrapper up with a negative margin to keep the visual ground
    // exactly where the caller asked for it. The mask fades the top to
    // transparent so Mario's jump arc bleeds into whatever is above.
    const extra = Math.round(height * 0.9);
    const totalH = height + extra;

    return (
      <div ref={wrapRef} style={{
        position: "relative",
        width: "100%",
        height: totalH,
        marginTop: -extra,
        imageRendering: "pixelated",
        overflow: "visible",
        pointerEvents: "none",
        WebkitMaskImage: "none",
        maskImage: "none",
      }}>
        <canvas ref={ref} style={{
          display: "block",
          width: "100%",
          height: "100%",
          imageRendering: "pixelated",
        }} />
      </div>
    );
  }

  window.PixelRunner = PixelRunner;
})();
