import React, { useEffect, useRef } from 'react';
import BrowserOnly from '@docusaurus/BrowserOnly';

// =============================================================================
// CONFIG — 預設值，可以從 props 覆蓋
// =============================================================================
const DEFAULT_CONFIG = {
  GAME_DURATION: 60,
  COOLDOWN: 0.5,
  JUMP_DURATION: 0.5,
  JUMP_HEIGHT_RATIO: 0.25,  
  PERFECT_WINDOW_T: 0.28,
  PERFECT_TOLERANCE: 0.001,
  MAX_DRIFT_T: 0.35,
  GRAVITY_RATIO: 5,   
  BALL_RADIUS_RATIO: 0.03,
  RESTITUTION_RIM: 0.65,
  RESTITUTION_BOARD: 0.7,
  RESTITUTION_BALL: 0.7,
  AIR_DRAG: 0,
  SHOT_TARGET_TIME: 0.75,
  SHOT_DRIFT_VEL: 0.4,
  SHOT_DRIFT_ANGLE: 0.6,
  BALL_IMG_SRC: null,
  PERFECT_JITTER_VEL: 0.0025,    // perfect 時速度的隨機浮動範圍（±1.5%）
  PERFECT_JITTER_ANGLE: 0.003,  // perfect 時角度的隨機浮動（±0.012 弧度，約 0.7 度）
  EULER_COMPENSATION: 0.012,  // semi-implicit Euler 的初幀重力補償（秒）。偏短就調大、偏長就調小。
};

const COLORS = {
  ink: '#1a1a1a',
  paper: '#fafaf7',
  ball: '#c2410c',
  rim: '#991b1b',
  net: '#999',
  gray: '#ccc',
};

// 遊戲階段
const PHASE = {
  TITLE: 'title',         // 模式選擇畫面
  PLAYING: 'playing',     // 遊戲中
  OVER: 'over',           // 結束畫面（只有限時模式會用到）
};

// 模式
const MODE = {
  TIMED: 'timed',
  PRACTICE: 'practice',
};

// =============================================================================
// Inline styles — 不需要額外 CSS 檔
// =============================================================================
const styles = {
  stage: {
    display: 'block',
    width: '100%',
    padding: '1rem 0 2rem',
    touchAction: 'none',
    userSelect: 'none',
    WebkitTapHighlightColor: 'transparent',
  },
  wrap: {
    position: 'relative',
    width: '100%',
    margin: '0 auto',
  },
  canvas: {
    display: 'block',
    background: COLORS.paper,
    cursor: 'pointer',
    fontFamily: 'ui-monospace, "SF Mono", Menlo, Consolas, monospace',
    width: '100%',
    height: 'auto',
    touchAction: 'none', // 確保 pointer events 能完整接管,瀏覽器不會搶去做手勢
  },
  hint: {
    position: 'absolute',
    left: '50%',
    bottom: '-24px',
    transform: 'translateX(-50%)',
    fontSize: '11px',
    letterSpacing: '0.2em',
    textTransform: 'uppercase',
    opacity: 0.45,
    whiteSpace: 'nowrap',
    fontFamily: 'ui-monospace, "SF Mono", Menlo, Consolas, monospace',
    color: COLORS.ink,
  },
};

// =============================================================================
// 遊戲主體 — 接收 canvas + config,回傳 cleanup function
// =============================================================================
function initGame(canvas, userConfig) {
  const CONFIG = { ...DEFAULT_CONFIG, ...userConfig };
  const ctx = canvas.getContext('2d');
  let W = 0, H = 0, DPR = 1;
  
  function resize() {
      const parent = canvas.parentElement;
      const parentW = parent ? parent.clientWidth : 600;
      const px = Math.max(280, Math.floor(parentW));
      DPR = Math.max(1, Math.min(2, window.devicePixelRatio || 1));
      canvas.style.width = px + 'px';
      canvas.style.height = px + 'px';
      canvas.width = Math.floor(px * DPR);
      canvas.height = Math.floor(px * DPR);
      ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
      W = px; H = px;
  }
  resize();

  // Ball image (optional)
  let ballImg = null;
  if (CONFIG.BALL_IMG_SRC) {
    const img = new Image();
    img.onload = () => { ballImg = img; };
    img.src = CONFIG.BALL_IMG_SRC;
  }

  // Audio
  let actx = null;
  function audio() {
    if (!actx) {
      try { actx = new (window.AudioContext || window.webkitAudioContext)(); }
      catch (e) { actx = null; }
    }
    return actx;
  }
  function blip({ freq = 440, dur = 0.08, type = 'sine', vol = 0.15, sweep = 0, delay = 0 } = {}) {
    const a = audio(); if (!a) return;
    const t0 = a.currentTime + delay;
    const o = a.createOscillator();
    const g = a.createGain();
    o.type = type;
    o.frequency.setValueAtTime(freq, t0);
    if (sweep) o.frequency.exponentialRampToValueAtTime(Math.max(40, freq + sweep), t0 + dur);
    g.gain.setValueAtTime(0.0001, t0);
    g.gain.exponentialRampToValueAtTime(vol, t0 + 0.005);
    g.gain.exponentialRampToValueAtTime(0.0001, t0 + dur);
    o.connect(g).connect(a.destination);
    o.start(t0); o.stop(t0 + dur + 0.02);
  }
  const SFX = {
    jump: () => blip({ freq: 220, dur: 0.06, type: 'triangle', vol: 0.08, sweep: 60 }),
    shoot: () => blip({ freq: 380, dur: 0.09, type: 'square', vol: 0.07, sweep: 200 }),
    rim:   () => blip({ freq: 900, dur: 0.05, type: 'square', vol: 0.06, sweep: -300 }),
    board: () => blip({ freq: 140, dur: 0.07, type: 'sine', vol: 0.12, sweep: -40 }),
    swish: () => {
      blip({ freq: 660, dur: 0.08, type: 'sine', vol: 0.10 });
      blip({ freq: 990, dur: 0.10, type: 'sine', vol: 0.08, delay: 0.04 });
      blip({ freq: 1320, dur: 0.12, type: 'sine', vol: 0.06, delay: 0.09 });
    },
    score: () => {
      blip({ freq: 523, dur: 0.10, type: 'triangle', vol: 0.10 });
      blip({ freq: 784, dur: 0.14, type: 'triangle', vol: 0.10, delay: 0.08 });
    },
    end: () => {
      blip({ freq: 220, dur: 0.18, type: 'sawtooth', vol: 0.08 });
      blip({ freq: 165, dur: 0.22, type: 'sawtooth', vol: 0.08, delay: 0.12 });
    },
  };

  function geom() {
      const hoopX = W * 0.78;
      const hoopW = W * 0.085;
      const rimR = W * 0.008;
      const armLength = W * 0.018;  // 連接臂的長度（rim 後緣到籃板的距離）
  
      return {
        ballR:    W * CONFIG.BALL_RADIUS_RATIO,
        ballX:    W * 0.18,
        groundY:  H * 0.72,
        hoopX:    hoopX,
        hoopY:    H * 0.42,
        hoopW:    hoopW,
        rimR:     rimR,
        armLength: armLength,
        // 籃板往右推，騰出空間讓 rim 後緣有「後面」可以彈
        boardX:   hoopX + hoopW / 2 + rimR + armLength,
        boardTop: H * 0.42 - H * 0.18,
        boardBot: H * 0.42 + H * 0.04,
        netDepth: H * 0.06,
      };
    }

  const state = {
    phase: PHASE.TITLE,
    mode: MODE.TIMED,        // 目前選的模式
    timeLeft: CONFIG.GAME_DURATION,
    score: 0,
    attempts: 0,             // 已出手球數（球真的飛出去才算）
    lastScoreFlash: 0,
    holding: false,
    holdT: 0,
    holdStartTime: 0,
    jumpDone: false,
    cooldown: 0,
    ready: true,
    balls: [],
    netPhase: 0,
    netImpulse: 0,
    nextBallId: 1,
  };

  // 給想作弊的讀者
    if (typeof window !== 'undefined') {
      window.__hoops = { state, CONFIG };
    }

  // ===== 標題畫面按鈕區域（給 pointer 用） =====
  // 在 drawTitle 計算過後存起來，pressDown 時用來判斷點到哪個按鈕
  let titleButtons = null; // { timed: {x, y, w, h}, practice: {x, y, w, h} }
  // 練習模式右下角的「回主畫面」按鈕
  let exitButton = null;   // { x, y, w, h }

  function startGame(mode) {
    state.phase = PHASE.PLAYING;
    state.mode = mode;
    state.timeLeft = CONFIG.GAME_DURATION;
    state.score = 0;
    state.attempts = 0;
    state.lastScoreFlash = 0;
    state.holding = false;
    state.holdT = 0;
    state.holdStartTime = 0;
    state.jumpDone = false;
    state.cooldown = 0;
    state.ready = true;
    state.balls = [];
    state.netImpulse = 0;
  }

  function backToTitle() {
    state.phase = PHASE.TITLE;
    state.holding = false;
    state.jumpDone = false;
    state.balls = [];
  }

  function pressDown(pointerPos) {
    audio();

    // 結束畫面（限時模式）：任何輸入都回標題
    if (state.phase === PHASE.OVER) {
      backToTitle();
      return;
    }

    // 標題畫面：用 pointer 位置判斷點到哪個按鈕；沒有 pointerPos（鍵盤）就忽略
    if (state.phase === PHASE.TITLE) {
      if (pointerPos && titleButtons) {
        const { x, y } = pointerPos;
        const inBtn = (b) => x >= b.x && x <= b.x + b.w && y >= b.y && y <= b.y + b.h;
        if (inBtn(titleButtons.timed)) { startGame(MODE.TIMED); return; }
        if (inBtn(titleButtons.practice)) { startGame(MODE.PRACTICE); return; }
      }
      return;
    }

    // 遊戲中：練習模式下，先檢查是否點到「回主畫面」按鈕
    if (state.mode === MODE.PRACTICE && pointerPos && exitButton) {
      const { x, y } = pointerPos;
      const b = exitButton;
      if (x >= b.x && x <= b.x + b.w && y >= b.y && y <= b.y + b.h) {
        backToTitle();
        return;
      }
    }

    if (!state.ready || state.holding) return;
    state.holding = true;
    state.holdT = 0;
    state.holdStartTime = performance.now();
    state.jumpDone = false;
    SFX.jump();
  }
  function pressUp() {
    if (!state.holding) return;
    state.holding = false;
    if (state.jumpDone) return;
    const realHoldT = (performance.now() - state.holdStartTime) / 1000;
    releaseShot(Math.min(realHoldT, CONFIG.JUMP_DURATION));
  }

 // ===== Event handlers =====
  // 鍵盤:留在 window(canvas 拿不到鍵盤焦點)。
  // 關鍵:只要是空白鍵且不在輸入框,就先無條件 preventDefault,
  // 否則 cooldown 中或按住不放(repeat)時,瀏覽器會拿去捲動頁面。
  const isEditableTarget = (target) => {
    if (!target) return false;
    const tag = target.tagName || '';
    return tag === 'INPUT' || tag === 'TEXTAREA' || target.isContentEditable;
  };
  
  const onKeyDown = (e) => {
    if (isEditableTarget(e.target)) return;
    if (!inViewport) return;

    // 標題畫面：1 / 2 選模式
    if (state.phase === PHASE.TITLE) {
      if (e.code === 'Digit1' || e.code === 'Numpad1') {
        e.preventDefault();
        if (e.repeat) return;
        audio();
        startGame(MODE.TIMED);
        return;
      }
      if (e.code === 'Digit2' || e.code === 'Numpad2') {
        e.preventDefault();
        if (e.repeat) return;
        audio();
        startGame(MODE.PRACTICE);
        return;
      }
      // 標題畫面下空白鍵不做事，但仍 preventDefault 避免捲動
      if (e.code === 'Space') {
        e.preventDefault();
      }
      return;
    }

    // 結束畫面（限時模式）：空白鍵回標題
    if (state.phase === PHASE.OVER) {
      if (e.code === 'Space') {
        e.preventDefault();
        if (e.repeat) return;
        audio();
        backToTitle();
      }
      return;
    }

    // 遊戲中：練習模式可用 Esc 直接回主畫面
    if (state.mode === MODE.PRACTICE && e.code === 'Escape') {
      e.preventDefault();
      if (e.repeat) return;
      backToTitle();
      return;
    }

    // 遊戲中：空白鍵蓄力
    if (e.code !== 'Space') return;
    e.preventDefault();
    if (e.repeat) return;
    pressDown();
  };
  const onKeyUp = (e) => {
    if (e.code !== 'Space') return;
    if (isEditableTarget(e.target)) return;
    if (!inViewport) return;
    e.preventDefault();
    if (state.phase !== PHASE.PLAYING) return;
    pressUp();
  };
  
  // Pointer events:用 setPointerCapture 把後續事件鎖在 canvas 上,
  // 滑出邊界放開也還是會送回 canvas,window 完全不需要監聽,
  // 也就不會干擾頁面其他連結的點擊(這就是 Android 連結失效的原因)。
  const getPointerPos = (e) => {
    const rect = canvas.getBoundingClientRect();
    return {
      x: e.clientX - rect.left,
      y: e.clientY - rect.top,
    };
  };

  const onPointerDown = (e) => {
    // 只接受主要按鍵(滑鼠左鍵、手指、筆)
    if (e.pointerType === 'mouse' && e.button !== 0) return;
    e.preventDefault();
    try {
      canvas.setPointerCapture(e.pointerId);
    } catch (err) {
      // 某些舊瀏覽器或特殊情境會丟錯,忽略即可
    }
    pressDown(getPointerPos(e));
  };
  const onPointerUp = (e) => {
    // 沒在蓄力就不該攔截事件,直接讓事件正常冒泡
    if (!state.holding) return;
    e.preventDefault();
    pressUp();
  };
  const onPointerCancel = () => {
    // 系統取消觸控(來電、手勢中斷等),靜默結束蓄力,不投籃
    if (state.holding) {
      state.holding = false;
      state.jumpDone = false;
    }
  };
  
  const onResize = () => resize();

  // ===== Viewport 偵測:canvas 不在畫面上時,就不要攔截空白鍵 =====
  // 否則玩家捲到文章下方想用空白鍵翻頁時,會被遊戲吃掉。
  let inViewport = true; // 預設視為在畫面內,IntersectionObserver 第一次回呼會修正
  let io = null;
  if (typeof IntersectionObserver !== 'undefined') {
    io = new IntersectionObserver(
      (entries) => {
        for (const entry of entries) {
          inViewport = entry.isIntersecting;
          // 離開畫面時,如果正在蓄力就靜默結束(避免捲回來時球自己飛出去)
          if (!inViewport && state.holding) {
            state.holding = false;
            state.jumpDone = false;
          }
        }
      },
      { threshold: 0 } // 只要有一個像素在畫面內就算
    );
    io.observe(canvas);
  }
  
  window.addEventListener('keydown', onKeyDown);
  window.addEventListener('keyup', onKeyUp);
  window.addEventListener('resize', onResize);
  canvas.addEventListener('pointerdown', onPointerDown);
  canvas.addEventListener('pointerup', onPointerUp);
  canvas.addEventListener('pointercancel', onPointerCancel);

  function releaseShot(holdT) {
    const g = geom();
    const tNorm = Math.min(1, holdT / CONFIG.JUMP_DURATION);
    const offset = tNorm - CONFIG.PERFECT_WINDOW_T;
    // const offset = -0.008; // 測試用
    const absOff = Math.abs(offset);


    const jumpY = jumpOffset(tNorm) * (W * CONFIG.JUMP_HEIGHT_RATIO);
    const bx = g.ballX;
    const by = g.groundY - jumpY;

    const tx = g.hoopX;
    const ty = g.hoopY;
    const T = CONFIG.SHOT_TARGET_TIME;
    const vx0 = (tx - bx) / T;
    const G = W * CONFIG.GRAVITY_RATIO;
    const vy0 = (ty - by - 0.5 * G * T * T) / T - G * CONFIG.EULER_COMPENSATION;

    const driftMag = absOff < CONFIG.PERFECT_TOLERANCE
      ? 0
      : Math.min(1, (absOff - CONFIG.PERFECT_TOLERANCE) / (CONFIG.MAX_DRIFT_T - CONFIG.PERFECT_TOLERANCE));

    const sign = Math.sign(offset) || (Math.random() < 0.5 ? -1 : 1);
    const rand = (Math.random() - 0.5) * 0.4;

    const speed = Math.hypot(vx0, vy0);
    const angle = Math.atan2(vy0, vx0);

    // 基礎 jitter — 所有球都會有，讓每次投籃略微不同
    const baseJitter = (Math.random() - 0.5) * 2; // -1 ~ 1
    const baseJitter2 = (Math.random() - 0.5) * 2;

	// 曲線的 driftMag
    const driftMagCurved = driftMag ** 0.4;

    // drift — 釋放時機不準時的額外偏移
    const driftSpeed = (sign * driftMag * CONFIG.SHOT_DRIFT_VEL) + rand * 1 * (driftMag ** 3);
    const driftAngle = (sign * driftMag * CONFIG.SHOT_DRIFT_ANGLE) + rand * 0.4 * driftMagCurved;
    
	// console.log(driftMag, driftSpeed, driftAngle);
	
	// console.log 顯示投籃資訊
    console.log(`[shot] 按了 ${(tNorm * CONFIG.JUMP_DURATION * 1000).toFixed(0)}ms ${absOff < CONFIG.PERFECT_TOLERANCE ? '✨ PERFECT' : offset < 0 ? `⏪ 太早 ${Math.abs(offset * CONFIG.JUMP_DURATION * 1000).toFixed(0)}ms` : `⏩ 太晚 ${(offset * CONFIG.JUMP_DURATION * 1000).toFixed(0)}ms`} | 偏移力度 ${(driftSpeed + baseJitter * CONFIG.PERFECT_JITTER_VEL).toFixed(4)} 偏移角度 ${(driftAngle + baseJitter2 * CONFIG.PERFECT_JITTER_ANGLE).toFixed(4)}`);
    
    const newSpeed = speed
      * (1 + baseJitter * CONFIG.PERFECT_JITTER_VEL)
      * (1 + driftSpeed);
    const newAngle = angle
      + baseJitter2 * CONFIG.PERFECT_JITTER_ANGLE
      + driftAngle;

	const vx = Math.cos(newAngle) * newSpeed;
    const vy = Math.sin(newAngle) * newSpeed;

    state.balls.push({
      id: state.nextBallId++,
      x: bx, y: by,
      vx, vy,
      r: g.ballR,
      spin: (Math.random() - 0.5) * 8,
      rot: 0,
      passedRimTopAt: null,
      scored: false,
      lastBoardHit: 0,
      lastRimHit: 0,
      bornAt: performance.now(),
    });

    state.attempts += 1;

    state.cooldown = CONFIG.COOLDOWN;
    state.ready = false;
    SFX.shoot();
  }

  function jumpOffset(tNorm) {
    if (tNorm <= 0 || tNorm >= 1) return 0;
    return Math.sin(tNorm * Math.PI);
  }

  function step(dt) {
    if (state.phase !== PHASE.PLAYING) return;

    // 限時模式：扣時間
    if (state.mode === MODE.TIMED) {
      state.timeLeft -= dt;
      if (state.timeLeft <= 0) {
        state.timeLeft = 0;
        state.phase = PHASE.OVER;
        SFX.end();
      }
    }

    if (state.holding) {
      state.holdT += dt;
      if (state.holdT >= CONFIG.JUMP_DURATION) {
        state.holdT = CONFIG.JUMP_DURATION;
        state.jumpDone = true;
      }
    }

    if (state.cooldown > 0) {
      state.cooldown -= dt;
      if (state.cooldown <= 0) { state.cooldown = 0; state.ready = true; }
    }

    const g = geom();
    for (const b of state.balls) {
      b.prevX = b.x;
      b.prevY = b.y;
      b.vy += W * CONFIG.GRAVITY_RATIO * dt;
      b.vx *= (1 - CONFIG.AIR_DRAG * dt * 60);
      b.vy *= (1 - CONFIG.AIR_DRAG * dt * 60);
      b.x += b.vx * dt;
      b.y += b.vy * dt;
      b.rot += b.spin * dt;

      collideBackboard(b, g);
      collideRim(b, g);
      detectScore(b, g);
    }

    for (let i = 0; i < state.balls.length; i++) {
      for (let j = i + 1; j < state.balls.length; j++) {
        collideBalls(state.balls[i], state.balls[j]);
      }
    }

    state.balls = state.balls.filter(b => {
      const margin = b.r * 4;
      return b.x > -margin && b.x < W + margin && b.y < H + margin;
    });

    state.netImpulse *= Math.exp(-dt * 4);
    state.netPhase += dt * 14;

    if (state.lastScoreFlash > 0) state.lastScoreFlash -= dt;
  }

  function collideBackboard(b, g) {
    if (b.x + b.r < g.boardX) return;
    if (b.x - b.r > g.boardX) return;
    if (b.y < g.boardTop - b.r || b.y > g.boardBot + b.r) return;

    const cy = Math.max(g.boardTop, Math.min(g.boardBot, b.y));
    const dx = b.x - g.boardX;
    const dy = b.y - cy;
    const d = Math.hypot(dx, dy);
    if (d > b.r) return;
    if (d === 0) return;

    const nx = dx / d, ny = dy / d;
    const overlap = b.r - d;
    b.x += nx * overlap;
    b.y += ny * overlap;
    const vn = b.vx * nx + b.vy * ny;
    if (vn < 0) {
      b.vx -= (1 + CONFIG.RESTITUTION_BOARD) * vn * nx;
      b.vy -= (1 + CONFIG.RESTITUTION_BOARD) * vn * ny;
      const now = performance.now();
      if (now - b.lastBoardHit > 80) {
        SFX.board();
        b.lastBoardHit = now;
      }
    }
  }

 function collideRim(b, g) {
      const front = { x: g.hoopX - g.hoopW / 2, y: g.hoopY };
      const back  = { x: g.hoopX + g.hoopW / 2, y: g.hoopY };
  
      // 兩個 rim 端點的圓形碰撞
      for (const p of [front, back]) {
        const dx = b.x - p.x;
        const dy = b.y - p.y;
        const d = Math.hypot(dx, dy);
        const minD = b.r + g.rimR;
        if (d < minD && d > 0) {
          const nx = dx / d, ny = dy / d;
          const overlap = minD - d;
          b.x += nx * overlap;
          b.y += ny * overlap;
          const vn = b.vx * nx + b.vy * ny;
          if (vn < 0) {
            b.vx -= (1 + CONFIG.RESTITUTION_RIM) * vn * nx;
            b.vy -= (1 + CONFIG.RESTITUTION_RIM) * vn * ny;
            const now = performance.now();
            if (now - b.lastRimHit > 80) {
              SFX.rim();
              b.lastRimHit = now;
            }
          }
        }
      }
  
      // 連接臂：從 rim 後緣 (back.x, back.y) 到籃板 (boardX, hoopY) 的水平線段
      // 球從上方或下方接近時要反彈
      const armLeft = back.x;
      const armRight = g.boardX;
      if (b.x > armLeft - b.r && b.x < armRight + b.r) {
        // 找出球到這條水平線段的最近點
        const cx = Math.max(armLeft, Math.min(armRight, b.x));
        const dx = b.x - cx;
        const dy = b.y - g.hoopY;
        const d = Math.hypot(dx, dy);
        // 連接臂視為很細的線（半徑近 0），所以最小距離就是 b.r
        if (d < b.r && d > 0) {
          const nx = dx / d, ny = dy / d;
          const overlap = b.r - d;
          b.x += nx * overlap;
          b.y += ny * overlap;
          const vn = b.vx * nx + b.vy * ny;
          if (vn < 0) {
            b.vx -= (1 + CONFIG.RESTITUTION_RIM) * vn * nx;
            b.vy -= (1 + CONFIG.RESTITUTION_RIM) * vn * ny;
            const now = performance.now();
            if (now - b.lastRimHit > 80) {
              SFX.rim();
              b.lastRimHit = now;
            }
          }
        }
      }
    }

 function detectScore(b, g) {
      if (b.scored) return;
      const rimY = g.hoopY;
      const xL = g.hoopX - g.hoopW / 2 + g.rimR;
      const xR = g.hoopX + g.hoopW / 2 - g.rimR;
  
      // 階段一：放寬條件——只要球中心曾經在籃框平面上方就算
      if (b.passedRimTopAt === null && b.y < rimY && b.x > xL - b.r && b.x < xR + b.r) {
        b.passedRimTopAt = b.y;
      }
  
      // 階段二：用實際的前一幀位置判斷下穿，不再用固定 0.016 回推
      const prevY = b.prevY != null ? b.prevY : b.y;
      if (
        !b.scored &&
        b.passedRimTopAt !== null &&
        b.vy > 0 &&
        prevY <= rimY &&
        b.y > rimY
      ) {
        if (b.x > xL && b.x < xR) {
          b.scored = true;
          state.score += 1;
          state.lastScoreFlash = 0.5;
          state.netImpulse = 1;
          const now = performance.now();
          const recentlyHit = (now - b.lastRimHit < 250) || (now - b.lastBoardHit < 250);
          if (recentlyHit) SFX.score();
          else SFX.swish();
        }
      }
    }

  function collideBalls(a, b) {
    const dx = b.x - a.x;
    const dy = b.y - a.y;
    const d = Math.hypot(dx, dy);
    const minD = a.r + b.r;
    if (d >= minD || d === 0) return;
    const nx = dx / d, ny = dy / d;
    const overlap = minD - d;
    a.x -= nx * overlap / 2;
    a.y -= ny * overlap / 2;
    b.x += nx * overlap / 2;
    b.y += ny * overlap / 2;
    const rvx = b.vx - a.vx, rvy = b.vy - a.vy;
    const vn = rvx * nx + rvy * ny;
    if (vn > 0) return;
    const e = CONFIG.RESTITUTION_BALL;
    const j = -(1 + e) * vn / 2;
    a.vx -= j * nx;
    a.vy -= j * ny;
    b.vx += j * nx;
    b.vy += j * ny;
  }

  // ===== Rendering =====
  function draw() {
    const g = geom();
    ctx.clearRect(0, 0, W, H);
    drawFrame();
    drawScore();
    drawBottomRight();
    drawHoop(g);
    drawShooterBall(g);
    for (const b of state.balls) drawBall(b);
    if (state.phase === PHASE.PLAYING) drawAccuracyBottomLeft();
    if (state.phase === PHASE.TITLE) drawTitle();
    else if (state.phase === PHASE.OVER) drawGameOver();
  }

  function drawFrame() {
    ctx.save();
    ctx.strokeStyle = COLORS.ink;
    ctx.lineWidth = 2;
    ctx.strokeRect(0.5, 0.5, W - 1, H - 1);
    ctx.restore();
  }

  function drawScore() {
    ctx.save();
    ctx.fillStyle = COLORS.ink;
    const flash = Math.max(0, state.lastScoreFlash);
    const scale = 1 + flash * 0.4;
    const fontSize = Math.floor(W * 0.085 * scale);
    ctx.font = `700 ${fontSize}px ui-monospace, "SF Mono", Menlo, monospace`;
    ctx.textAlign = 'center';
    ctx.textBaseline = 'top';
    ctx.fillText(String(state.score), W / 2, H * 0.05);
    ctx.restore();
  }

  // 右下角：限時模式顯示倒數，練習模式顯示「回主畫面 (esc)」按鈕
  function drawBottomRight() {
    if (state.phase !== PHASE.PLAYING) {
      exitButton = null;
      return;
    }
    ctx.save();

    if (state.mode === MODE.TIMED) {
      ctx.fillStyle = COLORS.ink;
      const fontSize = Math.floor(W * 0.05);
      ctx.font = `500 ${fontSize}px ui-monospace, "SF Mono", Menlo, monospace`;
      ctx.textAlign = 'right';
      ctx.textBaseline = 'bottom';
      const t = Math.max(0, state.timeLeft);
      const m = Math.floor(t / 60);
      const s = Math.floor(t % 60);
      const txt = `${m}:${String(s).padStart(2, '0')}`;
      ctx.fillText(txt, W - W * 0.05, H - H * 0.04);
      exitButton = null;
    } else {
      // 練習模式：小小的「回主畫面 (esc)」按鈕
      const label = '回主畫面 (esc)';
      const fontSize = Math.floor(W * 0.026);
      ctx.font = `500 ${fontSize}px ui-monospace, "SF Mono", Menlo, monospace`;
      const padX = W * 0.018;
      const padY = W * 0.012;
      const textW = ctx.measureText(label).width;
      const btnW = textW + padX * 2;
      const btnH = fontSize + padY * 2;
      const btnX = W - W * 0.05 - btnW;
      const btnY = H * 0.04;

      exitButton = { x: btnX, y: btnY, w: btnW, h: btnH };

      ctx.lineWidth = Math.max(1, W * 0.002);
      ctx.strokeStyle = COLORS.gray;
      ctx.fillStyle = COLORS.paper;
      ctx.beginPath();
      ctx.rect(btnX, btnY, btnW, btnH);
      ctx.fill();
      ctx.stroke();

      ctx.fillStyle = COLORS.gray;
      ctx.textAlign = 'center';
      ctx.textBaseline = 'middle';
      ctx.fillText(label, btnX + btnW / 2, btnY + btnH / 2);
    }

    ctx.restore();
  }

  // 左下角：命中率（遊戲中才顯示）
  function drawAccuracyBottomLeft() {
    ctx.save();
    ctx.fillStyle = COLORS.gray;
    const fontSize = Math.floor(W * 0.035);
    ctx.font = `500 ${fontSize}px ui-monospace, "SF Mono", Menlo, monospace`;
    ctx.textAlign = 'left';
    ctx.textBaseline = 'bottom';
    const pct = state.attempts > 0
      ? Math.round((state.score / state.attempts) * 100)
      : 0;
    const txt = `${state.score}/${state.attempts} (${pct}%)`;
    ctx.fillText(txt, W * 0.05, H - H * 0.04);
    ctx.restore();
  }

  function drawHoop(g) {
    ctx.save();
    ctx.strokeStyle = COLORS.ink;
    ctx.lineWidth = Math.max(2, W * 0.005);
    ctx.lineCap = 'round';
    ctx.beginPath();
    ctx.moveTo(g.boardX, g.boardTop);
    ctx.lineTo(g.boardX, g.boardBot);
    ctx.stroke();

    // 連接臂：rim 後緣到籃板（用 ink 色，視覺上跟籃板統一）
    ctx.strokeStyle = COLORS.ink;
    ctx.lineWidth = Math.max(1, W * 0.003);
    ctx.beginPath();
    ctx.moveTo(g.hoopX + g.hoopW / 2, g.hoopY);
    ctx.lineTo(g.boardX, g.hoopY);
    ctx.stroke();

    // 籃框（rim）— 比連接臂粗、用 rim 色
    ctx.strokeStyle = COLORS.rim;
    ctx.lineWidth = Math.max(2, W * 0.006);
    ctx.beginPath();
    ctx.moveTo(g.hoopX - g.hoopW / 2, g.hoopY);
    ctx.lineTo(g.hoopX + g.hoopW / 2, g.hoopY);
    ctx.stroke();

    ctx.strokeStyle = COLORS.net;
    ctx.lineWidth = Math.max(1, W * 0.0025);
    const netLines = 7;
    const baseY = g.hoopY;
    const bottomY = g.hoopY + g.netDepth;
    const leftX = g.hoopX - g.hoopW / 2;
    const rightX = g.hoopX + g.hoopW / 2;
    const wiggle = state.netImpulse * 4;
    for (let i = 0; i <= netLines; i++) {
      const t = i / netLines;
      const topX = leftX + t * (rightX - leftX);
      const bx = leftX + g.hoopW * 0.2 + t * (g.hoopW * 0.6);
      const phaseOff = i * 0.7;
      const dx = Math.sin(state.netPhase + phaseOff) * wiggle;
      ctx.beginPath();
      ctx.moveTo(topX, baseY);
      ctx.lineTo(bx + dx, bottomY + Math.abs(dx) * 0.3);
      ctx.stroke();
    }
    for (let k = 1; k <= 2; k++) {
      const yy = baseY + (g.netDepth * k / 3);
      ctx.beginPath();
      const dx = Math.sin(state.netPhase + k) * wiggle * 0.6;
      ctx.moveTo(leftX + g.hoopW * 0.08 * k, yy);
      ctx.lineTo(rightX - g.hoopW * 0.08 * k + dx, yy);
      ctx.stroke();
    }
    ctx.restore();
  }

  function drawShooterBall(g) {
    if (state.phase !== PHASE.PLAYING) return;
    if (!state.ready && !state.holding) return;
    let yOff = 0;
    if (state.holding) {
      const tNorm = Math.min(1, state.holdT / CONFIG.JUMP_DURATION);
      yOff = jumpOffset(tNorm) * (W * CONFIG.JUMP_HEIGHT_RATIO);
    }
    drawBallAt(g.ballX, g.groundY - yOff, g.ballR, 0);
  }

  function drawBall(b) {
    drawBallAt(b.x, b.y, b.r, b.rot);
  }

  function drawBallAt(x, y, r, rot) {
    ctx.save();
    if (ballImg) {
      ctx.translate(x, y);
      ctx.rotate(rot);
      ctx.drawImage(ballImg, -r, -r, r * 2, r * 2);
    } else {
      ctx.fillStyle = COLORS.ball;
      ctx.beginPath();
      ctx.arc(x, y, r, 0, Math.PI * 2);
      ctx.fill();
      ctx.strokeStyle = 'rgba(0,0,0,0.25)';
      ctx.lineWidth = Math.max(1, r * 0.08);
      ctx.beginPath();
      ctx.arc(x, y, r * 0.75, rot, rot + Math.PI);
      ctx.stroke();
    }
    ctx.restore();
  }

  function drawTitle() {
    ctx.save();
    ctx.fillStyle = 'rgba(250,250,247,0.85)';
    ctx.fillRect(0, 0, W, H);
    ctx.fillStyle = COLORS.ink;
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';

    // 標題 emoji
    ctx.font = `700 ${Math.floor(W * 0.14)}px sans-serif`;
    ctx.fillText('🏀🏀🏀', W / 2, H * 0.22);

    // 兩個按鈕：限時模式 / 練習模式
    const btnW = W * 0.7;
    const btnH = H * 0.11;
    const btnX = (W - btnW) / 2;
    const btnGap = H * 0.025;
    const btn1Y = H * 0.4;
    const btn2Y = btn1Y + btnH + btnGap;

    titleButtons = {
      timed:    { x: btnX, y: btn1Y, w: btnW, h: btnH },
      practice: { x: btnX, y: btn2Y, w: btnW, h: btnH },
    };

    const drawBtn = (b, label, sub) => {
      ctx.lineWidth = Math.max(1.5, W * 0.003);
      ctx.strokeStyle = COLORS.ink;
      ctx.fillStyle = COLORS.paper;
      ctx.beginPath();
      ctx.rect(b.x, b.y, b.w, b.h);
      ctx.fill();
      ctx.stroke();

      ctx.fillStyle = COLORS.ink;
      ctx.textAlign = 'center';
      ctx.textBaseline = 'middle';
      ctx.font = `600 ${Math.floor(W * 0.04)}px ui-monospace, "SF Mono", Menlo, monospace`;
      ctx.fillText(label, b.x + b.w / 2, b.y + b.h / 2 - W * 0.012);
      ctx.font = `400 ${Math.floor(W * 0.026)}px ui-monospace, "SF Mono", Menlo, monospace`;
      ctx.fillText(sub, b.x + b.w / 2, b.y + b.h / 2 + W * 0.022);
    };

    drawBtn(
      titleButtons.timed,
      '1. 限時模式',
      `${CONFIG.GAME_DURATION} 秒內投進越多越好`
    );
    drawBtn(
      titleButtons.practice,
      '2. 練習模式',
      '不限時，自由輕鬆投'
    );

    // 提示
    ctx.font = `400 ${Math.floor(W * 0.027)}px ui-monospace, "SF Mono", Menlo, monospace`;
    ctx.fillStyle = 'rgba(26,26,26,0.55)';
    ctx.fillText('鍵盤 1 / 2，或點按鈕', W / 2, H * 0.88);

    ctx.restore();
  }

  function drawGameOver() {
    ctx.save();
    ctx.fillStyle = 'rgba(250,250,247,0.9)';
    ctx.fillRect(0, 0, W, H);
    ctx.fillStyle = COLORS.ink;
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    // 限時模式：原本的畫面，完全不動
    ctx.font = `500 ${Math.floor(W * 0.045)}px ui-monospace, "SF Mono", Menlo, monospace`;
    ctx.fillText('時間到', W / 2, H * 0.36);
    ctx.font = `700 ${Math.floor(W * 0.22)}px ui-monospace, "SF Mono", Menlo, monospace`;
    ctx.fillText(String(state.score), W / 2, H * 0.5);
    ctx.font = `400 ${Math.floor(W * 0.03)}px ui-monospace, "SF Mono", Menlo, monospace`;
    ctx.fillText('空白鍵 / 滑鼠 / 觸控再玩一次', W / 2, H * 0.66);
    ctx.restore();
  }

  // ===== Loop =====
  let last = performance.now();
  let rafId = 0;
  function frame(now) {
    let dt = (now - last) / 1000;
    last = now;
    if (dt > 0.05) dt = 0.05;
    step(dt);
    draw();
    rafId = requestAnimationFrame(frame);
  }
  rafId = requestAnimationFrame(frame);

  // ===== Cleanup =====
  return () => {
    cancelAnimationFrame(rafId);
    if (io) io.disconnect(); 
    window.removeEventListener('keydown', onKeyDown);
    window.removeEventListener('keyup', onKeyUp);
    window.removeEventListener('resize', onResize);
    canvas.removeEventListener('pointerdown', onPointerDown);
    canvas.removeEventListener('pointerup', onPointerUp);
    canvas.removeEventListener('pointercancel', onPointerCancel);
    if (actx && actx.state !== 'closed') {
      actx.close().catch(() => {});
    }
  };
}

// =============================================================================
// React component
// =============================================================================
function HoopsInner({ config }) {
  const canvasRef = useRef(null);

  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const cleanup = initGame(canvas, config || {});
    return cleanup;
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return (
    <div style={styles.stage}>
      <div style={styles.wrap}>
        <canvas ref={canvasRef} style={styles.canvas} />
        <div style={styles.hint}>投籃：按鍵後放開</div>
      </div>
    </div>
  );
}

export default function Hoops(props) {
  return (
    <BrowserOnly fallback={<div style={{ height: 400 }} />}>
      {() => <HoopsInner {...props} />}
    </BrowserOnly>
  );
}
