// engine.jsx — Number Snake game engine (Canvas + game logic)
// Uses a ref-held mutable state object. Renders via requestAnimationFrame.
// Movement is timed by setInterval (interval shrinks as round grows).

const CELL = 32;
const COLS = 20;
const ROWS = 16;
const CANVAS_W = COLS * CELL; // 640
const CANVAS_H = ROWS * CELL; // 512

// Z-gen palette per spec §8.5 — fluorescent
const BUBBLE_PALETTE = [
  // [hexFill, hexGlow]
  ["#4fffb0", "#10b981"], // 1-5  fluorescent green
  ["#38bdf8", "#0284c7"], // 6-10 fluorescent blue
  ["#c084fc", "#a855f7"], // 11-15 purple
  ["#f472b6", "#ec4899"], // 16-20 pink
];
const colorForValue = (v) => {
  if (v <= 5) return BUBBLE_PALETTE[0];
  if (v <= 10) return BUBBLE_PALETTE[1];
  if (v <= 15) return BUBBLE_PALETTE[2];
  return BUBBLE_PALETTE[3];
};

const SNAKE_COLORS = {
  head: "#a855f7",
  near: "#c084fc",
  far:  "#ddd6fe",
};

const randInt = (lo, hi) => Math.floor(Math.random() * (hi - lo + 1)) + lo;
const shuffle = (arr) => {
  const a = [...arr];
  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;
};

// -------------------- bubble generation --------------------
function generateBubbles(target, op, snakeHead, difficulty) {
  // Number range and pool size by difficulty
  const maxVal   = difficulty <= 1 ? 5 : difficulty === 2 ? 10 : 20;
  const poolSize = difficulty <= 1 ? 5 : 8;

  // Choose solver pair — ensure n1 < n2 for "+" so both are distinct pool entries
  let n1, n2;
  if (op === "+") {
    n1 = randInt(1, Math.floor((target - 1) / 2));
    n2 = target - n1;
  } else {
    n2 = randInt(1, maxVal - target);
    n1 = n2 + target;
  }

  // Build pool of unique numbers including the pair
  const pool = new Set([n1, n2]);
  const allowed = [];
  for (let v = 1; v <= maxVal; v++) allowed.push(v);
  for (const v of shuffle(allowed)) {
    if (pool.size >= poolSize) break;
    pool.add(v);
  }
  const values = shuffle([...pool]);

  // Place each value on grid with constraints
  const placed = [];
  for (const v of values) {
    let placedOk = false;
    for (let attempt = 0; attempt < 60; attempt++) {
      const gx = randInt(1, COLS - 2);
      const gy = randInt(2, ROWS - 2);

      const tooCloseToSnake = Math.hypot(gx - snakeHead.x, gy - snakeHead.y) < 4;
      const overlap = placed.some(
        (b) => Math.hypot(b.gridX - gx, b.gridY - gy) < 2.2
      );
      if (!tooCloseToSnake && !overlap) {
        placed.push({
          id: `b-${v}-${Math.random().toString(36).slice(2, 7)}`,
          gridX: gx,
          gridY: gy,
          value: v,
          radius: randInt(14, 18),
          floatOffset: Math.random() * Math.PI * 2,
          glowing: false,
          collected: false,
          // entrance animation
          spawnT: performance.now(),
        });
        placedOk = true;
        break;
      }
    }
    // if fail, drop this value silently
  }

  return placed;
}

function verifySolution(bubbles, target, op) {
  const vals = bubbles.map((b) => b.value);
  for (let i = 0; i < vals.length; i++) {
    for (let j = i + 1; j < vals.length; j++) {
      if (op === "+" && vals[i] + vals[j] === target) return true;
      if (op === "-" && Math.abs(vals[i] - vals[j]) === target) return true;
    }
  }
  return false;
}

// -------------------- Engine class --------------------
class SnakeEngine {
  constructor(canvas, callbacks) {
    this.canvas = canvas;
    this.ctx = canvas.getContext("2d");
    this.cb = callbacks; // { onUiUpdate, onResult, onSound, onGameOver }
    this.dpr = Math.max(1, window.devicePixelRatio || 1);
    canvas.width = CANVAS_W * this.dpr;
    canvas.height = CANVAS_H * this.dpr;
    canvas.style.width = CANVAS_W + "px";
    canvas.style.height = CANVAS_H + "px";
    this.ctx.scale(this.dpr, this.dpr);

    this.state = this._freshState();
    this.running = false;
    this.paused = false;
    this.moveTimer = null;
    this.rafId = null;
    this.lastMove = 0;
    this.totalSeconds = 180;
    this.endsAt = 0;
    this.score = 0;
    this.round = 1;
    this.pops = []; // floating +1/-1 toasts
  }

  _freshState() {
    const snake = [];
    const startX = 5, startY = 8;
    for (let i = 0; i < 5; i++) snake.push({ x: startX - i, y: startY });
    return {
      snake,
      direction: { x: 1, y: 0 },
      nextDirection: { x: 1, y: 0 },
      bubbles: [],
      collectedNumbers: [],
      target: 5,
      operation: "+",
      snakeLength: 5,
      transparentTimer: 0,
      hintTimer: 0,
      hintShown: false,
      showingResult: false,
    };
  }

  // ---- lifecycle ----
  start(totalSeconds, difficulty = 3) {
    this.difficulty = difficulty;
    this.totalSeconds = totalSeconds;
    this.endsAt = Date.now() + totalSeconds * 1000;
    this.score = 0;
    this.round = 1;
    this.state = this._freshState();
    this._newRound();
    this.running = true;
    this.paused = false;
    this._scheduleMove();
    this._renderLoop();
    this._emitUi();
    this.cb.onSound?.("start");
  }

  pause() {
    if (!this.running || this.paused) return;
    this.paused = true;
    this._remainingMs = this.endsAt - Date.now();
    clearInterval(this.moveTimer);
  }

  resume() {
    if (!this.running || !this.paused) return;
    this.paused = false;
    this.endsAt = Date.now() + this._remainingMs;
    this._scheduleMove();
  }

  destroy() {
    this.running = false;
    clearInterval(this.moveTimer);
    clearInterval(this.renderInterval);
    cancelAnimationFrame(this.rafId);
  }

  setDirection(dx, dy) {
    const s = this.state;
    if (dx === -s.direction.x && dy === -s.direction.y) return; // no reverse
    s.nextDirection = { x: dx, y: dy };
  }

  undoLastCollect() {
    const s = this.state;
    if (s.collectedNumbers.length === 0 || s.showingResult) return;
    s.collectedNumbers.pop();
    // Restore the most recently collected bubble
    for (let i = s.bubbles.length - 1; i >= 0; i--) {
      if (s.bubbles[i].collected) {
        s.bubbles[i].collected = false;
        s.bubbles[i].glowing = false;
        s.bubbles[i].spawnT = performance.now();
        break;
      }
    }
    // Clear all glow and hint state
    for (const b of s.bubbles) b.glowing = false;
    s.hintTimer = 0;
    s.hintShown = false;
    this._hintCountdownStart = null;
    this.cb.onSound?.("bump");
    this._emitUi();
  }

  // ---- timing ----
  _interval() {
    const [base, dec, floor] =
      this.difficulty <= 1 ? [500, 5, 220] :
      this.difficulty === 2 ? [380, 7, 150] :
                              [300, 8, 120];
    return Math.max(floor, base - this.round * dec);
  }
  _scheduleMove() {
    clearInterval(this.moveTimer);
    this.moveTimer = setInterval(() => this._tick(), this._interval());
  }

  // ---- new round ----
  _newRound() {
    const d = this.difficulty;
    let op, target;

    if (d <= 1) {
      // 入门 (3–5 岁): 只有加法，数字 1–5，目标 3–5
      op = "+";
      target = randInt(3, Math.min(5, 3 + Math.floor(this.round / 3)));
    } else if (d === 2) {
      // 进阶 (6–8 岁): 加减法，数字 1–10，目标 3–10
      const tMax = Math.min(10, 4 + this.round);
      op = Math.random() < 0.6 ? "+" : "-";
      target = op === "+" ? randInt(3, Math.max(4, tMax))
                          : randInt(1, Math.max(2, tMax - 1));
    } else {
      // 挑战 (9–12 岁): 加减法，数字 1–20，目标 3–20
      const tMax = Math.min(10 + this.round * 2, 20);
      op = Math.random() < 0.55 ? "+" : "-";
      target = op === "+" ? randInt(3, Math.max(4, tMax))
                          : randInt(1, Math.max(2, tMax - 1));
    }

    this.state.target = target;
    this.state.operation = op;
    this.state.collectedNumbers = [];
    this.state.hintTimer = 0;
    this.state.hintShown = false;

    // generate bubbles, ensure solvable
    let bubbles = [];
    for (let tries = 0; tries < 8; tries++) {
      bubbles = generateBubbles(target, op, this.state.snake[0], d);
      if (verifySolution(bubbles, target, op)) break;
    }
    this.state.bubbles = bubbles;
  }

  // ---- main movement tick ----
  _tick() {
    if (!this.running || this.paused || this.state.showingResult) return;

    const now = Date.now();
    const timeLeft = Math.max(0, Math.ceil((this.endsAt - now) / 1000));
    if (timeLeft <= 0) {
      this.running = false;
      clearInterval(this.moveTimer);
      this.cb.onSound?.("gameover");
      this.cb.onGameOver?.({ score: this.score, round: this.round });
      return;
    }

    const s = this.state;
    s.direction = s.nextDirection;

    let head = s.snake[0];
    let nx = head.x + s.direction.x;
    let ny = head.y + s.direction.y;

    // Wall bounce
    if (nx < 0 || nx >= COLS || ny < 0 || ny >= ROWS) {
      const cands = [
        { x: -s.direction.y, y: s.direction.x },
        { x: s.direction.y, y: -s.direction.x },
        { x: -s.direction.x, y: -s.direction.y },
      ];
      let chosen = null;
      for (const d of cands) {
        const tx = head.x + d.x, ty = head.y + d.y;
        if (tx >= 0 && tx < COLS && ty >= 0 && ty < ROWS) { chosen = d; break; }
      }
      if (chosen) {
        s.direction = chosen;
        s.nextDirection = chosen;
        nx = head.x + chosen.x;
        ny = head.y + chosen.y;
        this.cb.onSound?.("bump");
      }
    }

    // Self collision -> transparent for 60 frames (~1s)
    if (s.transparentTimer === 0) {
      for (let i = 1; i < s.snake.length; i++) {
        if (s.snake[i].x === nx && s.snake[i].y === ny) {
          s.transparentTimer = 60;
          break;
        }
      }
    }
    if (s.transparentTimer > 0) s.transparentTimer--;

    // Advance snake
    s.snake.unshift({ x: nx, y: ny });
    while (s.snake.length > s.snakeLength) s.snake.pop();

    // Bubble pickup
    const head2 = s.snake[0];
    for (const b of s.bubbles) {
      if (b.collected) continue;
      if (Math.hypot(b.gridX - head2.x, b.gridY - head2.y) < 0.9) {
        b.collected = true;
        s.collectedNumbers.push(b.value);
        this.cb.onSound?.("pickup");
        // start hint timer when first collected
        if (s.collectedNumbers.length === 1) {
          s.hintTimer = 20;
          this._hintCountdownStart = Date.now();
        }
        this._emitUi();
        if (s.collectedNumbers.length === 2) {
          this._evaluateRound();
        }
        break;
      }
    }

    // Hint countdown
    if (s.collectedNumbers.length === 1 && this._hintCountdownStart) {
      const elapsed = (Date.now() - this._hintCountdownStart) / 1000;
      if (elapsed >= 20 && !s.hintShown) {
        s.hintShown = true;
        this.cb.onSound?.("hint");
        // glow the correct partner
        const need = s.collectedNumbers[0];
        for (const b of s.bubbles) {
          if (b.collected) continue;
          let ok = false;
          if (s.operation === "+" && b.value + need === s.target) ok = true;
          if (s.operation === "-" && Math.abs(b.value - need) === s.target) ok = true;
          b.glowing = ok;
        }
      }
    }

    this._emitUi(timeLeft);
  }

  _evaluateRound() {
    const s = this.state;
    const [a, b] = s.collectedNumbers;
    let correct = false;
    let equation;
    if (s.operation === "+") {
      correct = a + b === s.target;
      equation = `${a} + ${b} = ${a + b}`;
    } else {
      correct = Math.abs(a - b) === s.target;
      equation = `${Math.max(a,b)} − ${Math.min(a,b)} = ${Math.abs(a-b)}`;
    }

    s.showingResult = true;
    if (correct) {
      this.score += 1;
      s.snakeLength += 3;
      this.cb.onSound?.("correct");
      // floating +1 from snake head
      const head = s.snake[0];
      this.pops.push({
        x: head.x * CELL + CELL/2,
        y: head.y * CELL + CELL/2,
        text: "+1", color: "#10b981", t: performance.now(),
      });
    } else {
      this.score = Math.max(0, this.score - 1);
      s.snakeLength = Math.max(5, s.snakeLength - 1);
      this.cb.onSound?.("wrong");
      const head = s.snake[0];
      this.pops.push({
        x: head.x * CELL + CELL/2,
        y: head.y * CELL + CELL/2,
        text: "−1", color: "#f43f5e", t: performance.now(),
      });
    }

    this.cb.onResult?.({ equation, isCorrect: correct });
    this._emitUi();

    setTimeout(() => {
      s.showingResult = false;
      if (correct) {
        this.round += 1;
        this._scheduleMove();
        this._newRound();
      } else {
        // keep same target, regenerate bubbles
        s.collectedNumbers = [];
        s.hintTimer = 0;
        s.hintShown = false;
        let bubbles = [];
        for (let tries = 0; tries < 8; tries++) {
          bubbles = generateBubbles(s.target, s.operation, s.snake[0], this.difficulty);
          if (verifySolution(bubbles, s.target, s.operation)) break;
        }
        s.bubbles = bubbles;
      }
      this._emitUi();
    }, 1800);
  }

  // ---- React UI sync ----
  _emitUi(forcedTimeLeft) {
    const s = this.state;
    const timeLeft = forcedTimeLeft ?? Math.max(0, Math.ceil((this.endsAt - Date.now()) / 1000));
    this.cb.onUiUpdate?.({
      score: this.score,
      round: this.round,
      timeLeft,
      collected: [...s.collectedNumbers],
      target: s.target,
      operation: s.operation,
    });
  }

  // ---- Rendering ----
  _renderLoop() {
    // Use setInterval to keep rendering even when iframe is briefly hidden
    // (rAF pauses when document.hidden). 60fps target.
    const tick = () => {
      if (!this.canvas.isConnected) return;
      this._render();
    };
    this.renderInterval = setInterval(tick, 16);
    tick();
  }

  _render() {
    const ctx = this.ctx;
    const s = this.state;

    // clear
    ctx.clearRect(0, 0, CANVAS_W, CANVAS_H);

    // Background fill (translucent so mesh shows through)
    ctx.fillStyle = "rgba(255,255,255,0.18)";
    ctx.fillRect(0, 0, CANVAS_W, CANVAS_H);

    // Grid lines
    ctx.strokeStyle = "rgba(0,0,0,0.035)";
    ctx.lineWidth = 1;
    for (let x = 0; x <= COLS; x++) {
      ctx.beginPath(); ctx.moveTo(x * CELL, 0); ctx.lineTo(x * CELL, CANVAS_H); ctx.stroke();
    }
    for (let y = 0; y <= ROWS; y++) {
      ctx.beginPath(); ctx.moveTo(0, y * CELL); ctx.lineTo(CANVAS_W, y * CELL); ctx.stroke();
    }

    // Inner border
    ctx.strokeStyle = "rgba(255,255,255,0.85)";
    ctx.lineWidth = 2;
    ctx.strokeRect(1, 1, CANVAS_W - 2, CANVAS_H - 2);

    // Bubbles
    const now = performance.now();
    for (const b of s.bubbles) {
      if (b.collected) continue;
      b.floatOffset += 0.04;
      const cx = b.gridX * CELL + CELL / 2;
      const cy = b.gridY * CELL + CELL / 2 + Math.sin(b.floatOffset) * 4;
      const [fill, glow] = colorForValue(b.value);

      // Spawn pop scaling
      const sp = Math.min(1, (now - b.spawnT) / 320);
      const scale = sp < 1 ? 0.4 + 1.0 * easeOutBack(sp) : 1;

      // Glow rings if hinted
      if (b.glowing || s.hintShown) {
        const pulse = 0.5 + 0.5 * Math.sin(now * 0.006);
        for (let i = 3; i >= 1; i--) {
          ctx.beginPath();
          ctx.arc(cx, cy, b.radius * scale + i * 5 + pulse * 3, 0, Math.PI * 2);
          ctx.fillStyle = hexA(glow, 0.08 + i * 0.04);
          ctx.fill();
        }
      }

      // soft outer glow
      const grad0 = ctx.createRadialGradient(cx, cy, b.radius * 0.4, cx, cy, b.radius * 2 * scale);
      grad0.addColorStop(0, hexA(fill, 0.5));
      grad0.addColorStop(1, hexA(fill, 0));
      ctx.fillStyle = grad0;
      ctx.beginPath();
      ctx.arc(cx, cy, b.radius * 1.9 * scale, 0, Math.PI * 2);
      ctx.fill();

      // bubble fill
      const grad = ctx.createRadialGradient(
        cx - b.radius * 0.35, cy - b.radius * 0.35, b.radius * 0.1,
        cx, cy, b.radius * scale
      );
      grad.addColorStop(0, "#ffffff");
      grad.addColorStop(0.18, lightenHex(fill, 0.35));
      grad.addColorStop(0.7, fill);
      grad.addColorStop(1, darkenHex(fill, 0.18));
      ctx.fillStyle = grad;
      ctx.beginPath();
      ctx.arc(cx, cy, b.radius * scale, 0, Math.PI * 2);
      ctx.fill();

      // highlight crescent
      ctx.beginPath();
      ctx.fillStyle = "rgba(255,255,255,0.55)";
      ctx.ellipse(cx - b.radius * 0.35, cy - b.radius * 0.4, b.radius * 0.42, b.radius * 0.22, -0.4, 0, Math.PI * 2);
      ctx.fill();

      // number text
      ctx.font = `900 ${Math.round(b.radius * 1.1)}px "Space Grotesk", sans-serif`;
      ctx.textAlign = "center";
      ctx.textBaseline = "middle";
      ctx.lineWidth = 4;
      ctx.strokeStyle = hexA(glow, 0.95);
      ctx.fillStyle = "#ffffff";
      ctx.strokeText(String(b.value), cx, cy + 1);
      ctx.fillText(String(b.value), cx, cy + 1);
    }

    // Snake
    const alpha = s.transparentTimer > 0 ? 0.4 : 1;
    ctx.save();
    ctx.globalAlpha = alpha;

    for (let i = s.snake.length - 1; i >= 0; i--) {
      const seg = s.snake[i];
      let color = SNAKE_COLORS.far;
      if (i === 0) color = SNAKE_COLORS.head;
      else if (i < 4) color = SNAKE_COLORS.near;

      const x = seg.x * CELL + 2;
      const y = seg.y * CELL + 2;
      const size = CELL - 4;
      const r = 9;

      // soft shadow
      ctx.fillStyle = "rgba(124, 58, 237, 0.15)";
      drawRoundRect(ctx, x + 1, y + 3, size, size, r);
      ctx.fill();

      // body
      const bodyGrad = ctx.createLinearGradient(x, y, x, y + size);
      bodyGrad.addColorStop(0, lightenHex(color, 0.1));
      bodyGrad.addColorStop(1, color);
      ctx.fillStyle = bodyGrad;
      drawRoundRect(ctx, x, y, size, size, r);
      ctx.fill();

      // shine line
      ctx.strokeStyle = "rgba(255,255,255,0.5)";
      ctx.lineWidth = 1.5;
      ctx.beginPath();
      ctx.moveTo(x + 4, y + 4);
      ctx.lineTo(x + size - 6, y + 4);
      ctx.stroke();
    }

    // head eyes
    if (s.snake.length) {
      const head = s.snake[0];
      const hx = head.x * CELL + CELL / 2;
      const hy = head.y * CELL + CELL / 2;
      const offX = s.direction.x * 6;
      const offY = s.direction.y * 6;
      const perpX = -s.direction.y * 5;
      const perpY = s.direction.x * 5;
      const drawEye = (ex, ey) => {
        ctx.beginPath();
        ctx.fillStyle = "#fff";
        ctx.arc(ex, ey, 3.6, 0, Math.PI * 2);
        ctx.fill();
        ctx.beginPath();
        ctx.fillStyle = "#1a1530";
        ctx.arc(ex + s.direction.x * 1.2, ey + s.direction.y * 1.2, 1.7, 0, Math.PI * 2);
        ctx.fill();
      };
      drawEye(hx + offX + perpX * 0.6, hy + offY + perpY * 0.6);
      drawEye(hx + offX - perpX * 0.6, hy + offY - perpY * 0.6);
    }
    ctx.restore();

    // Floating "+1 / -1" pops
    this.pops = this.pops.filter((p) => now - p.t < 900);
    for (const p of this.pops) {
      const t = (now - p.t) / 900;
      const y = p.y - t * 50;
      ctx.globalAlpha = 1 - t;
      ctx.font = `900 28px "Space Grotesk", sans-serif`;
      ctx.fillStyle = p.color;
      ctx.textAlign = "center";
      ctx.strokeStyle = "rgba(255,255,255,0.95)";
      ctx.lineWidth = 4;
      ctx.strokeText(p.text, p.x, y);
      ctx.fillText(p.text, p.x, y);
      ctx.globalAlpha = 1;
    }

    // Hint overlay text
    if (s.hintShown) {
      ctx.fillStyle = "rgba(124, 58, 237, 0.9)";
      ctx.font = `700 14px "Space Mono", monospace`;
      ctx.textAlign = "center";
      ctx.fillText("✦ 找发光的那个 ✦", CANVAS_W / 2, CANVAS_H - 14);
    }
  }
}
function drawRoundRect(ctx, x, y, w, h, r) {
  ctx.beginPath();
  ctx.moveTo(x + r, y);
  ctx.lineTo(x + w - r, y);
  ctx.quadraticCurveTo(x + w, y, x + w, y + r);
  ctx.lineTo(x + w, y + h - r);
  ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
  ctx.lineTo(x + r, y + h);
  ctx.quadraticCurveTo(x, y + h, x, y + h - r);
  ctx.lineTo(x, y + r);
  ctx.quadraticCurveTo(x, y, x + r, y);
  ctx.closePath();
}

function easeOutBack(t) {
  const c1 = 1.70158;
  const c3 = c1 + 1;
  return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2);
}

function hexA(hex, a) {
  const { r, g, b } = hexRgb(hex);
  return `rgba(${r},${g},${b},${a})`;
}
function hexRgb(hex) {
  const h = hex.replace("#", "");
  return {
    r: parseInt(h.slice(0,2), 16),
    g: parseInt(h.slice(2,4), 16),
    b: parseInt(h.slice(4,6), 16),
  };
}
function lightenHex(hex, amt) {
  const { r, g, b } = hexRgb(hex);
  return `rgb(${Math.min(255, r + (255-r)*amt) | 0}, ${Math.min(255, g + (255-g)*amt) | 0}, ${Math.min(255, b + (255-b)*amt) | 0})`;
}
function darkenHex(hex, amt) {
  const { r, g, b } = hexRgb(hex);
  return `rgb(${Math.max(0, r * (1-amt)) | 0}, ${Math.max(0, g * (1-amt)) | 0}, ${Math.max(0, b * (1-amt)) | 0})`;
}

window.SnakeEngine = SnakeEngine;
window.SNAKE_CONST = { CELL, COLS, ROWS, CANVAS_W, CANVAS_H };
window.colorForValue = colorForValue;
