<script lang="ts">
  import { onMount } from "svelte";
  import * as THREE from "three";
  import { pressureColorPalettes } from "$lib/config/color-map";
  import type { LocaleCode, PressureColorMapPreset } from "$lib/types/hud";

  type CornerId = "tl" | "tr" | "bl" | "br";

  interface CornerMap {
    tl: number;
    tr: number;
    bl: number;
    br: number;
  }

  interface Brick {
    mesh: THREE.Mesh<THREE.BoxGeometry, THREE.MeshStandardMaterial>;
    alive: boolean;
    left: number;
    right: number;
    top: number;
    bottom: number;
  }

  interface Copy {
    title: string;
    start: string;
    restart: string;
    pause: string;
    resume: string;
    running: string;
    paused: string;
    over: string;
    score: string;
    combo: string;
    lives: string;
    level: string;
    bricks: string;
    chase: string;
    pausedOverlay: string;
  }

  export let locale: LocaleCode = "zh-CN";
  export let pressureMatrix: number[] | null = null;
  export let matrixRows = 12;
  export let matrixCols = 7;
  export let rangeMin = 0;
  export let rangeMax = 16000;
  export let colorMapPreset: PressureColorMapPreset = "emerald";

  const FIELD_HALF_W = 46;
  const FIELD_HALF_H = 62;
  const PADDLE_Y = -53;
  const PADDLE_W = 16;
  const PADDLE_H = 2.6;
  const PADDLE_SPEED = 74;
  const BALL_RADIUS = 1.45;
  const BASE_BALL_SPEED = 58;
  const MAX_BALL_SPEED = 106;
  const BRICK_COLS = 12;
  const BRICK_ROWS = 7;
  const BRICK_W = 6.2;
  const BRICK_H = 2.9;
  const BRICK_GAP_X = 0.8;
  const BRICK_GAP_Y = 0.9;
  const BRICK_TOP = 44;
  const START_DELAY_MS = 520;
  const PAUSE_COOLDOWN_MS = 640;
  const UPPER_STALL_THRESHOLD_MS = 1700;
  const UPPER_STALL_Y = 14;
  const ANTI_STALL_COOLDOWN_MS = 420;
  const MAX_BRICK_HITS_WITHOUT_PADDLE = 5;

  const copyByLocale: Record<LocaleCode, Copy> = {
    "zh-CN": {
      title: "NEON BREAKOUT",
      start: "开始",
      restart: "重开",
      pause: "暂停",
      resume: "继续",
      running: "运行中",
      paused: "已暂停",
      over: "结束",
      score: "得分",
      combo: "连击",
      lives: "生命",
      level: "关卡",
      bricks: "剩余",
      chase: "追击",
      pausedOverlay: "已暂停 / 顶部双角施压继续"
    },
    "en-US": {
      title: "NEON BREAKOUT",
      start: "Start",
      restart: "Restart",
      pause: "Pause",
      resume: "Resume",
      running: "Running",
      paused: "Paused",
      over: "Game Over",
      score: "Score",
      combo: "Combo",
      lives: "Lives",
      level: "Level",
      bricks: "Bricks",
      chase: "Chase",
      pausedOverlay: "Paused / Top corners to resume"
    }
  };

  let hostEl: HTMLElement | undefined;
  let canvasEl: HTMLCanvasElement | undefined;
  let rafId: number | null = null;
  let renderer: THREE.WebGLRenderer | null = null;
  let scene: THREE.Scene | null = null;
  let camera: THREE.OrthographicCamera | null = null;
  let paddle: THREE.Mesh<THREE.BoxGeometry, THREE.MeshStandardMaterial> | null = null;
  let ball: THREE.Mesh<THREE.SphereGeometry, THREE.MeshStandardMaterial> | null = null;
  let brickGroup: THREE.Group | null = null;
  let borderLine: THREE.LineLoop | null = null;
  let bgPlane: THREE.Mesh<THREE.PlaneGeometry, THREE.MeshBasicMaterial> | null = null;
  let ambientLight: THREE.AmbientLight | null = null;
  let keyLight: THREE.PointLight | null = null;
  let rimLight: THREE.PointLight | null = null;
  let flashSprite: THREE.Sprite | null = null;
  let flashLife = 0;
  let flashMaxLife = 0.2;

  let keyLeft = false;
  let keyRight = false;
  let ballLaunched = false;
  let nextLaunchAt = 0;
  let paddleX = 0;
  const ballPos = new THREE.Vector2(0, 0);
  const ballVel = new THREE.Vector2(0, 0);
  let bricks: Brick[] = [];

  let gameState: "idle" | "running" | "paused" | "over" = "idle";
  let score = 0;
  let combo = 0;
  let lives = 3;
  let level = 1;
  let bricksLeft = 0;
  let lastFrameTs = 0;
  let prevPauseGesture = false;
  let pauseGestureLockUntil = 0;
  let themedBgTexture: THREE.CanvasTexture | null = null;
  let lastPaddleContactAt = 0;
  let bricksSincePaddle = 0;
  let lastAntiStallAt = 0;

  let cornerForce: CornerMap = { tl: 0, tr: 0, bl: 0, br: 0 };
  $: cornerForce = readCorners(pressureMatrix, matrixRows, matrixCols);
  $: themePalette = pressureColorPalettes[colorMapPreset] ?? pressureColorPalettes.emerald;
  $: ui = copyByLocale[locale];
  $: leftForce = cornerForce.tl + cornerForce.bl;
  $: rightForce = cornerForce.tr + cornerForce.br;
  $: topForce = cornerForce.tl + cornerForce.tr;
  $: pauseGestureThreshold = Math.max(420, Math.round(Math.max(1000, rangeMax - rangeMin) * 0.07));
  $: statusText =
    gameState === "running" ? ui.running : gameState === "paused" ? ui.paused : gameState === "over" ? ui.over : ui.start;

  $: if (scene && renderer) {
    applyTheme();
  }

  function rgbTripletToColor(rgbTriplet: string): THREE.Color {
    return new THREE.Color(`rgb(${rgbTriplet.replace(/\s+/g, ", ")})`);
  }

  function rgbaFromTriplet(rgbTriplet: string, alpha: number): string {
    return `rgba(${rgbTriplet.replace(/\s+/g, ",")},${alpha})`;
  }

  function themedBrickPalette(): string[] {
    const { rangeStops } = themePalette;
    return [rangeStops[1], rangeStops[2], rangeStops[3], rangeStops[4], rangeStops[5], rangeStops[2], rangeStops[3]];
  }

  function applyTheme(): void {
    if (!scene || !renderer) return;
    const uiTheme = themePalette.uiTheme;
    scene.background = new THREE.Color(uiTheme.bg30);

    ambientLight?.color.copy(rgbTripletToColor(uiTheme.textMainRgb));
    keyLight?.color.copy(rgbTripletToColor(uiTheme.glowAltRgb));
    rimLight?.color.copy(rgbTripletToColor(uiTheme.glowRgb));

    const borderMaterial = borderLine?.material as THREE.LineBasicMaterial | undefined;
    if (borderMaterial) {
      borderMaterial.color.copy(rgbTripletToColor(uiTheme.glowRgb));
      borderMaterial.opacity = 0.48;
      borderMaterial.needsUpdate = true;
    }

    if (paddle) {
      paddle.material.color.copy(new THREE.Color(themePalette.rangeStops[2]));
      paddle.material.emissive.copy(new THREE.Color(themePalette.rangeStops[2]));
      paddle.material.emissiveIntensity = 0.72;
      paddle.material.needsUpdate = true;
    }

    if (ball) {
      ball.material.color.copy(new THREE.Color(themePalette.rangeStops[0]));
      ball.material.emissive.copy(new THREE.Color(themePalette.rangeStops[3]));
      ball.material.emissiveIntensity = 0.92;
      ball.material.needsUpdate = true;
    }

    if (bgPlane) {
      const material = bgPlane.material;
      themedBgTexture?.dispose();
      themedBgTexture = createGridTexture(themePalette);
      material.map = themedBgTexture;
      material.opacity = 0.78;
      material.needsUpdate = true;
    }

    const colors = themedBrickPalette();
    for (let index = 0; index < bricks.length; index += 1) {
      const brick = bricks[index];
      const color = colors[index % colors.length] ?? "#7ad0ff";
      brick.mesh.material.color.set(color);
      brick.mesh.material.emissive.set(color);
      brick.mesh.material.emissiveIntensity = 0.7;
      brick.mesh.material.needsUpdate = true;
    }
  }

  onMount(() => {
    if (!hostEl || !canvasEl) return;
    const uiTheme = themePalette.uiTheme;
    const paddleColor = themePalette.rangeStops[2];
    const ballColor = themePalette.rangeStops[0];
    const ballGlow = themePalette.rangeStops[3];

    const nextScene = new THREE.Scene();
    nextScene.background = new THREE.Color(uiTheme.bg30);
    scene = nextScene;

    const nextCamera = new THREE.OrthographicCamera(-60, 60, 70, -70, 0.1, 200);
    nextCamera.position.set(0, 0, 54);
    nextCamera.lookAt(0, 0, 0);
    camera = nextCamera;

    const nextRenderer = new THREE.WebGLRenderer({
      canvas: canvasEl,
      antialias: true,
      alpha: true,
      powerPreference: "high-performance"
    });
    renderer = nextRenderer;
    nextRenderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));

    const ambient = new THREE.AmbientLight(rgbTripletToColor(uiTheme.textMainRgb), 0.44);
    const key = new THREE.PointLight(rgbTripletToColor(uiTheme.glowAltRgb), 1.2, 180, 1.8);
    key.position.set(0, 20, 38);
    const rim = new THREE.PointLight(rgbTripletToColor(uiTheme.glowRgb), 0.65, 160, 1.7);
    rim.position.set(-22, -14, 26);
    nextScene.add(ambient, key, rim);
    ambientLight = ambient;
    keyLight = key;
    rimLight = rim;

    const bgTexture = createGridTexture(themePalette);
    themedBgTexture = bgTexture;
    const bg = new THREE.Mesh(
      new THREE.PlaneGeometry(160, 170),
      new THREE.MeshBasicMaterial({
        map: bgTexture,
        transparent: true,
        opacity: 0.78,
        depthWrite: false
      })
    );
    bg.position.set(0, 0, -12);
    nextScene.add(bg);
    bgPlane = bg;

    const border = new THREE.LineLoop(
      new THREE.BufferGeometry().setFromPoints([
        new THREE.Vector3(-FIELD_HALF_W, -FIELD_HALF_H, 0),
        new THREE.Vector3(FIELD_HALF_W, -FIELD_HALF_H, 0),
        new THREE.Vector3(FIELD_HALF_W, FIELD_HALF_H, 0),
        new THREE.Vector3(-FIELD_HALF_W, FIELD_HALF_H, 0)
      ]),
      new THREE.LineBasicMaterial({ color: rgbTripletToColor(uiTheme.glowRgb), transparent: true, opacity: 0.46 })
    );
    nextScene.add(border);
    borderLine = border;

    const nextBrickGroup = new THREE.Group();
    brickGroup = nextBrickGroup;
    nextScene.add(nextBrickGroup);

    const paddleMesh = new THREE.Mesh(
      new THREE.BoxGeometry(PADDLE_W, PADDLE_H, 2.1),
      new THREE.MeshStandardMaterial({
        color: paddleColor,
        emissive: paddleColor,
        emissiveIntensity: 0.72,
        roughness: 0.34,
        metalness: 0.18
      })
    );
    paddleMesh.position.set(0, PADDLE_Y, 0.4);
    nextScene.add(paddleMesh);
    paddle = paddleMesh;

    const ballMesh = new THREE.Mesh(
      new THREE.SphereGeometry(BALL_RADIUS, 22, 16),
      new THREE.MeshStandardMaterial({
        color: ballColor,
        emissive: ballGlow,
        emissiveIntensity: 0.88,
        roughness: 0.2,
        metalness: 0.04
      })
    );
    nextScene.add(ballMesh);
    ball = ballMesh;

    const flashTexture = createFlashTexture(themePalette);
    const flash = new THREE.Sprite(
      new THREE.SpriteMaterial({
        map: flashTexture,
        color: "#74ffff",
        transparent: true,
        blending: THREE.AdditiveBlending,
        opacity: 0,
        depthWrite: false
      })
    );
    flash.scale.set(1, 1, 1);
    flash.visible = false;
    nextScene.add(flash);
    flashSprite = flash;

    const onKeyDown = (event: KeyboardEvent) => {
      if (event.code === "ArrowLeft" || event.code === "KeyA") keyLeft = true;
      if (event.code === "ArrowRight" || event.code === "KeyD") keyRight = true;
      if (event.code === "Space" && gameState !== "running") restartGame();
    };
    const onKeyUp = (event: KeyboardEvent) => {
      if (event.code === "ArrowLeft" || event.code === "KeyA") keyLeft = false;
      if (event.code === "ArrowRight" || event.code === "KeyD") keyRight = false;
    };

    window.addEventListener("keydown", onKeyDown);
    window.addEventListener("keyup", onKeyUp);

    const resize = () => {
      if (!hostEl || !renderer || !camera) return;
      const width = hostEl.clientWidth;
      const height = hostEl.clientHeight;
      if (width <= 0 || height <= 0) return;
      renderer.setSize(width, height, false);
      const aspect = width / Math.max(1, height);
      const halfH = 70;
      const halfW = halfH * aspect;
      camera.left = -halfW;
      camera.right = halfW;
      camera.top = halfH;
      camera.bottom = -halfH;
      camera.updateProjectionMatrix();
    };

    const observer = new ResizeObserver(() => resize());
    observer.observe(hostEl);
    resize();

    restartGame();
    lastFrameTs = performance.now();
    const loop = (ts: number) => {
      const dt = Math.min((ts - lastFrameTs) / 1000, 0.034);
      lastFrameTs = ts;
      update(dt, ts);
      renderer?.render(nextScene, nextCamera);
      rafId = window.requestAnimationFrame(loop);
    };
    rafId = window.requestAnimationFrame(loop);

    return () => {
      if (rafId != null) window.cancelAnimationFrame(rafId);
      window.removeEventListener("keydown", onKeyDown);
      window.removeEventListener("keyup", onKeyUp);
      observer.disconnect();
      disposeBricks();
      border.geometry.dispose();
      (border.material as THREE.LineBasicMaterial).dispose();
      bg.geometry.dispose();
      (bg.material as THREE.MeshBasicMaterial).map?.dispose();
      (bg.material as THREE.MeshBasicMaterial).dispose();
      paddleMesh.geometry.dispose();
      paddleMesh.material.dispose();
      ballMesh.geometry.dispose();
      ballMesh.material.dispose();
      (flash.material as THREE.SpriteMaterial).map?.dispose();
      (flash.material as THREE.SpriteMaterial).dispose();
      renderer?.dispose();
    };
  });

  function createGridTexture(palette = themePalette): THREE.CanvasTexture {
    const canvas = document.createElement("canvas");
    canvas.width = 512;
    canvas.height = 512;
    const ctx = canvas.getContext("2d");
    if (!ctx) return new THREE.CanvasTexture(canvas);

    const { uiTheme } = palette;
    const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height);
    gradient.addColorStop(0, uiTheme.bg20);
    gradient.addColorStop(0.5, uiTheme.bg10);
    gradient.addColorStop(1, uiTheme.bg30);
    ctx.fillStyle = gradient;
    ctx.fillRect(0, 0, canvas.width, canvas.height);

    ctx.strokeStyle = rgbaFromTriplet(uiTheme.glowRgb, 0.18);
    ctx.lineWidth = 1;
    for (let y = 0; y < canvas.height; y += 24) {
      ctx.beginPath();
      ctx.moveTo(0, y);
      ctx.lineTo(canvas.width, y);
      ctx.stroke();
    }
    ctx.strokeStyle = rgbaFromTriplet(uiTheme.glowAltRgb, 0.08);
    for (let x = 0; x < canvas.width; x += 24) {
      ctx.beginPath();
      ctx.moveTo(x, 0);
      ctx.lineTo(x, canvas.height);
      ctx.stroke();
    }

    const texture = new THREE.CanvasTexture(canvas);
    texture.colorSpace = THREE.SRGBColorSpace;
    texture.needsUpdate = true;
    return texture;
  }

  function createFlashTexture(palette = themePalette): THREE.CanvasTexture {
    const canvas = document.createElement("canvas");
    canvas.width = 128;
    canvas.height = 128;
    const ctx = canvas.getContext("2d");
    if (!ctx) return new THREE.CanvasTexture(canvas);
    const { uiTheme } = palette;
    const gradient = ctx.createRadialGradient(64, 64, 8, 64, 64, 64);
    gradient.addColorStop(0, "rgba(255,255,255,1)");
    gradient.addColorStop(0.25, rgbaFromTriplet(uiTheme.glowAltRgb, 0.86));
      gradient.addColorStop(1, rgbaFromTriplet(uiTheme.glowRgb, 0));
      ctx.fillStyle = gradient;
      ctx.fillRect(0, 0, 128, 128);
    const texture = new THREE.CanvasTexture(canvas);
    texture.colorSpace = THREE.SRGBColorSpace;
    texture.needsUpdate = true;
    return texture;
  }

  function rand(min: number, max: number): number {
    return Math.random() * (max - min) + min;
  }

  function disposeBricks(): void {
    if (!brickGroup) return;
    for (const brick of bricks) {
      brick.mesh.geometry.dispose();
      brick.mesh.material.dispose();
      brickGroup.remove(brick.mesh);
    }
    bricks = [];
    bricksLeft = 0;
  }

  function buildBricks(): void {
    if (!brickGroup) return;
    disposeBricks();
    const palette = themedBrickPalette();
    const totalWidth = BRICK_COLS * BRICK_W + (BRICK_COLS - 1) * BRICK_GAP_X;
    const startX = -totalWidth / 2 + BRICK_W / 2;
    for (let row = 0; row < BRICK_ROWS; row += 1) {
      for (let col = 0; col < BRICK_COLS; col += 1) {
        const color = palette[(row + col + level) % palette.length] ?? "#7ad0ff";
        const material = new THREE.MeshStandardMaterial({
          color,
          emissive: color,
          emissiveIntensity: 0.7,
          roughness: 0.35,
          metalness: 0.15
        });
        const mesh = new THREE.Mesh(new THREE.BoxGeometry(BRICK_W, BRICK_H, 2.4), material);
        const x = startX + col * (BRICK_W + BRICK_GAP_X);
        const y = BRICK_TOP - row * (BRICK_H + BRICK_GAP_Y);
        mesh.position.set(x, y, 0.2);
        brickGroup.add(mesh);
        bricks.push({
          mesh,
          alive: true,
          left: x - BRICK_W / 2,
          right: x + BRICK_W / 2,
          top: y + BRICK_H / 2,
          bottom: y - BRICK_H / 2
        });
      }
    }
    bricksLeft = bricks.length;
  }

  function launchBall(nowMs: number): void {
    const speed = Math.min(MAX_BALL_SPEED, BASE_BALL_SPEED + (level - 1) * 4.2);
    const directionX = rand(-0.72, 0.72);
    ballVel.set(directionX, 1).normalize().multiplyScalar(speed);
    ballLaunched = true;
    nextLaunchAt = nowMs;
    lastPaddleContactAt = nowMs;
    lastAntiStallAt = nowMs;
    bricksSincePaddle = 0;
  }

  function prepareBall(nowMs: number, delayMs: number): void {
    ballLaunched = false;
    ballVel.set(0, 0);
    ballPos.set(paddleX, PADDLE_Y + PADDLE_H * 0.5 + BALL_RADIUS + 0.4);
    nextLaunchAt = nowMs + delayMs;
    bricksSincePaddle = 0;
  }

  function restartGame(): void {
    if (!paddle || !ball) return;
    score = 0;
    combo = 0;
    lives = 3;
    level = 1;
    gameState = "running";
    prevPauseGesture = false;
    pauseGestureLockUntil = 0;
    lastPaddleContactAt = performance.now();
    lastAntiStallAt = 0;
    bricksSincePaddle = 0;
    paddleX = 0;
    paddle.position.x = 0;
    buildBricks();
    prepareBall(performance.now(), START_DELAY_MS);
  }

  function togglePause(): void {
    if (gameState === "running") {
      gameState = "paused";
      return;
    }
    if (gameState === "paused") {
      gameState = "running";
      lastFrameTs = performance.now();
    }
  }

  function readCorners(matrix: number[] | null, rows: number, cols: number): CornerMap {
    if (!matrix || matrix.length === 0) return { tl: 0, tr: 0, bl: 0, br: 0 };
    const r = Math.max(Math.floor(rows), 1);
    const c = Math.max(Math.floor(cols), 1);
    const rs = Math.min(2, r);
    const cs = Math.min(2, c);
    const avg = (r0: number, r1: number, c0: number, c1: number): number => {
      let sum = 0;
      let count = 0;
      for (let i = r0; i < r1; i += 1) {
        for (let j = c0; j < c1; j += 1) {
          const v = Number(matrix[i * c + j] ?? 0);
          if (!Number.isFinite(v)) continue;
          sum += Math.max(0, v);
          count += 1;
        }
      }
      return count ? sum / count : 0;
    };
    return {
      tl: avg(0, rs, 0, cs),
      tr: avg(0, rs, c - cs, c),
      bl: avg(r - rs, r, 0, cs),
      br: avg(r - rs, r, c - cs, c)
    };
  }

  function sensorControlAxis(): number {
    const span = Math.max(1200, (rangeMax - rangeMin) * 0.22);
    const raw = (rightForce - leftForce) / span;
    if (Math.abs(raw) < 0.045) return 0;
    return Math.min(1, Math.max(-1, raw));
  }

  function handleSensorPauseGesture(nowMs: number): void {
    if (gameState !== "running" && gameState !== "paused") {
      prevPauseGesture = false;
      return;
    }

    const active = topForce >= pauseGestureThreshold;
    if (active && !prevPauseGesture && nowMs >= pauseGestureLockUntil) {
      togglePause();
      pauseGestureLockUntil = nowMs + PAUSE_COOLDOWN_MS;
    }
    prevPauseGesture = active;
  }

  function update(dt: number, nowMs: number): void {
    if (!paddle || !ball) return;
    handleSensorPauseGesture(nowMs);

    const keyAxis = (keyRight ? 1 : 0) - (keyLeft ? 1 : 0);
    const axis = keyAxis !== 0 ? keyAxis : sensorControlAxis();
    paddleX += axis * PADDLE_SPEED * dt;
    paddleX = Math.min(FIELD_HALF_W - PADDLE_W * 0.5, Math.max(-FIELD_HALF_W + PADDLE_W * 0.5, paddleX));
    paddle.position.x = paddleX;

    if (gameState !== "running") {
      ball.position.set(ballPos.x, ballPos.y, 1.2);
      updateFlash(dt);
      return;
    }

    if (!ballLaunched) {
      ballPos.set(paddleX, PADDLE_Y + PADDLE_H * 0.5 + BALL_RADIUS + 0.4);
      if (nowMs >= nextLaunchAt) launchBall(nowMs);
    } else {
      ballPos.addScaledVector(ballVel, dt);
      resolveWallCollision(nowMs);
      resolvePaddleCollision();
      resolveBrickCollision(nowMs);
      applyAntiStall(nowMs);
    }

    ball.position.set(ballPos.x, ballPos.y, 1.2);
    updateFlash(dt);
  }

  function resolveWallCollision(nowMs: number): void {
    if (ballPos.x - BALL_RADIUS <= -FIELD_HALF_W) {
      ballPos.x = -FIELD_HALF_W + BALL_RADIUS;
      ballVel.x = Math.abs(ballVel.x);
    } else if (ballPos.x + BALL_RADIUS >= FIELD_HALF_W) {
      ballPos.x = FIELD_HALF_W - BALL_RADIUS;
      ballVel.x = -Math.abs(ballVel.x);
    }

    if (ballPos.y + BALL_RADIUS >= FIELD_HALF_H) {
      ballPos.y = FIELD_HALF_H - BALL_RADIUS;
      ballVel.y = -Math.abs(ballVel.y);
      if (nowMs - lastPaddleContactAt > UPPER_STALL_THRESHOLD_MS * 0.6) {
        forceBallDownward(0.62);
      }
    }

    if (ballPos.y - BALL_RADIUS < -FIELD_HALF_H) {
      lives -= 1;
      combo = 0;
      if (lives <= 0) {
        gameState = "over";
        ballLaunched = false;
      } else {
        prepareBall(nowMs, 440);
      }
    }
  }

  function resolvePaddleCollision(): void {
    if (!ballLaunched || ballVel.y >= 0) return;
    const paddleTop = PADDLE_Y + PADDLE_H * 0.5;
    const paddleBottom = PADDLE_Y - PADDLE_H * 0.5;
    const left = paddleX - PADDLE_W * 0.5;
    const right = paddleX + PADDLE_W * 0.5;
    const hit =
      ballPos.y - BALL_RADIUS <= paddleTop &&
      ballPos.y + BALL_RADIUS >= paddleBottom &&
      ballPos.x >= left - BALL_RADIUS &&
      ballPos.x <= right + BALL_RADIUS;

    if (!hit) return;
    ballPos.y = paddleTop + BALL_RADIUS + 0.04;
    const offset = (ballPos.x - paddleX) / (PADDLE_W * 0.5);
    const speed = Math.min(MAX_BALL_SPEED, Math.max(BASE_BALL_SPEED, ballVel.length()));
    ballVel.set(offset * speed * 0.9, Math.abs(ballVel.y) + 4).normalize().multiplyScalar(speed);
    combo = 0;
    lastPaddleContactAt = performance.now();
    bricksSincePaddle = 0;
  }

  function resolveBrickCollision(nowMs: number): void {
    if (!ballLaunched) return;
    for (const brick of bricks) {
      if (!brick.alive) continue;
      const closestX = Math.max(brick.left, Math.min(ballPos.x, brick.right));
      const closestY = Math.max(brick.bottom, Math.min(ballPos.y, brick.top));
      const dx = ballPos.x - closestX;
      const dy = ballPos.y - closestY;
      if (dx * dx + dy * dy > BALL_RADIUS * BALL_RADIUS) continue;

      const overlapX = Math.min(ballPos.x + BALL_RADIUS - brick.left, brick.right - (ballPos.x - BALL_RADIUS));
      const overlapY = Math.min(ballPos.y + BALL_RADIUS - brick.bottom, brick.top - (ballPos.y - BALL_RADIUS));
      if (overlapX < overlapY) {
        ballVel.x *= -1;
      } else {
        ballVel.y *= -1;
      }

      brick.alive = false;
      brick.mesh.visible = false;
      bricksLeft -= 1;
      combo += 1;
      score += 40 + combo * 5;
      bricksSincePaddle += 1;

      const nextSpeed = Math.min(MAX_BALL_SPEED, ballVel.length() * 1.014);
      ballVel.normalize().multiplyScalar(nextSpeed);
      if (bricksSincePaddle >= MAX_BRICK_HITS_WITHOUT_PADDLE && ballPos.y > UPPER_STALL_Y) {
        forceBallDownward(1);
        lastAntiStallAt = nowMs;
      }
      triggerFlash(brick.mesh.position.x, brick.mesh.position.y, 10 + combo * 0.4);

      if (bricksLeft <= 0) {
        level += 1;
        buildBricks();
        prepareBall(nowMs, 560);
      }
      break;
    }
  }

  function triggerFlash(x: number, y: number, scale: number): void {
    if (!flashSprite) return;
    flashLife = flashMaxLife;
    flashSprite.visible = true;
    flashSprite.position.set(x, y, 2.5);
    flashSprite.scale.set(scale, scale, 1);
    const material = flashSprite.material as THREE.SpriteMaterial;
    material.opacity = 0.76;
  }

  function updateFlash(dt: number): void {
    if (!flashSprite || flashLife <= 0) return;
    flashLife -= dt;
    const t = Math.max(0, flashLife / flashMaxLife);
    const material = flashSprite.material as THREE.SpriteMaterial;
    material.opacity = t * 0.78;
    flashSprite.scale.multiplyScalar(1 + dt * 2.6);
    if (flashLife <= 0) flashSprite.visible = false;
  }

  function forceBallDownward(forceScale: number): void {
    const speed = Math.min(MAX_BALL_SPEED, Math.max(BASE_BALL_SPEED, ballVel.length()));
    let dirX = ballVel.x + rand(-8, 8);
    if (Math.abs(dirX) < 6) dirX = (Math.random() < 0.5 ? -1 : 1) * 6;
    const dirY = -Math.abs(ballVel.y) - 6 * forceScale;
    ballVel.set(dirX, dirY).normalize().multiplyScalar(speed);
  }

  function applyAntiStall(nowMs: number): void {
    if (!ballLaunched) return;
    if (ballPos.y < UPPER_STALL_Y) return;
    if (nowMs - lastPaddleContactAt < UPPER_STALL_THRESHOLD_MS) return;
    if (nowMs - lastAntiStallAt < ANTI_STALL_COOLDOWN_MS) return;
    forceBallDownward(0.85);
    lastAntiStallAt = nowMs;
  }
</script>

<section class="breakout-root" bind:this={hostEl}>
  <canvas class="breakout-canvas" bind:this={canvasEl} aria-label="Neon Breakout"></canvas>

  <div class="breakout-overlay">
    <header class="overlay-head">
      <div class="title-block">
        <p class="title">{ui.title}</p>
      </div>
      <div class="action-group">
        <button type="button" on:click={restartGame}>{gameState === "idle" ? ui.start : ui.restart}</button>
        <button type="button" on:click={togglePause} disabled={gameState === "idle" || gameState === "over"}>
          {gameState === "paused" ? ui.resume : ui.pause}
        </button>
      </div>
    </header>

    <div class="hud-grid">
      <article><span>{ui.score}</span><strong>{Math.round(score)}</strong></article>
      <article><span>{ui.combo}</span><strong>{combo}</strong></article>
      <article><span>{ui.lives}</span><strong>{lives}</strong></article>
      <article><span>{ui.level}</span><strong>{level}</strong></article>
      <article><span>{ui.bricks}</span><strong>{Math.max(0, bricksLeft)}</strong></article>
    </div>

    <footer class="overlay-foot">
      <p class="status">{ui.chase} / {statusText}</p>
    </footer>
  </div>

  {#if gameState === "paused"}
    <div class="pause-mask" aria-live="polite">
      <div class="pause-panel">
        <p>{ui.pausedOverlay}</p>
      </div>
    </div>
  {/if}

  {#if gameState === "over"}
    <div class="pause-mask is-over" aria-live="assertive">
      <div class="pause-panel is-over">
        <p>{ui.over}</p>
        <button type="button" class="overlay-restart-btn" on:click={restartGame}>{ui.restart}</button>
      </div>
    </div>
  {/if}
</section>

<style>
  .breakout-root {
    position: absolute;
    inset: 0;
    overflow: hidden;
    border-radius: 0.58rem;
    border: 1px solid rgb(var(--hud-border-rgb) / 0.28);
    background: linear-gradient(180deg, rgb(var(--hud-surface-deep-rgb) / 0.92), rgb(var(--hud-surface-rgb) / 0.86));
    box-shadow:
      inset 0 1px 0 rgb(var(--hud-border-strong-rgb) / 0.08),
      0 0 24px rgb(var(--hud-glow-rgb) / 0.12);
  }

  .breakout-canvas {
    position: absolute;
    inset: 0;
    inline-size: 100%;
    block-size: 100%;
    display: block;
  }

  .breakout-overlay {
    position: absolute;
    inset: 0;
    pointer-events: none;
    display: grid;
    grid-template-rows: auto auto 1fr auto;
    gap: 0.42rem;
    padding: 0.5rem 0.58rem 0.42rem;
  }

  .overlay-head {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    gap: 0.5rem;
  }

  .title {
    margin: 0;
    color: rgb(var(--hud-text-main-rgb) / 0.96);
    font-size: 0.68rem;
    letter-spacing: 0.12em;
    text-transform: uppercase;
  }

  .action-group {
    display: inline-flex;
    gap: 0.28rem;
    pointer-events: auto;
  }

  .action-group button {
    min-block-size: 1.65rem;
    border: 1px solid rgb(var(--hud-cyan-rgb) / 0.56);
    border-radius: 999px;
    padding: 0.14rem 0.52rem;
    background: linear-gradient(180deg, rgb(var(--hud-surface-alt-rgb) / 0.92), rgb(var(--hud-surface-rgb) / 0.86));
    color: rgb(var(--hud-text-main-rgb) / 0.96);
    font: inherit;
    font-size: 0.6rem;
    letter-spacing: 0.06em;
    cursor: pointer;
  }

  .action-group button:disabled {
    opacity: 0.42;
    cursor: not-allowed;
  }

  .hud-grid {
    display: grid;
    grid-template-columns: repeat(5, minmax(0, 1fr));
    gap: 0.24rem;
  }

  .hud-grid article {
    min-block-size: 1.88rem;
    border: 1px solid rgb(var(--hud-border-rgb) / 0.24);
    border-radius: 0.42rem;
    padding: 0.16rem 0.34rem 0.24rem;
    background: rgb(var(--hud-surface-rgb) / 0.68);
  }

  .hud-grid span {
    display: block;
    color: rgb(var(--hud-text-dim-rgb) / 0.82);
    font-size: 0.5rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
  }

  .hud-grid strong {
    color: rgb(var(--hud-text-main-rgb) / 0.98);
    font-size: 0.84rem;
    line-height: 1;
  }

  .overlay-foot {
    display: flex;
    justify-content: flex-start;
    align-items: center;
    gap: 0.52rem;
  }

  .status {
    margin: 0;
    border: 1px solid rgb(var(--hud-border-rgb) / 0.3);
    border-radius: 999px;
    padding: 0.18rem 0.56rem;
    background: rgb(var(--hud-surface-deep-rgb) / 0.66);
    color: rgb(var(--hud-text-main-rgb) / 0.94);
    font-size: 0.56rem;
    letter-spacing: 0.07em;
    text-transform: uppercase;
  }

  .pause-mask {
    position: absolute;
    inset: 0;
    z-index: 12;
    pointer-events: none;
    display: grid;
    place-items: center;
    background:
      radial-gradient(circle at center, rgb(var(--hud-surface-rgb) / 0.24), rgb(var(--hud-bg-30) / 0.72)),
      repeating-linear-gradient(
        180deg,
        rgb(var(--hud-glow-alt-rgb) / 0.08) 0,
        rgb(var(--hud-glow-alt-rgb) / 0.08) 1px,
        transparent 1px,
        transparent 4px
      );
    backdrop-filter: blur(3px);
  }

  .pause-panel {
    border: 1px solid rgb(var(--hud-lime-rgb) / 0.56);
    border-radius: 0.66rem;
    padding: 0.62rem 0.92rem;
    background: rgb(var(--hud-surface-deep-rgb) / 0.8);
    box-shadow:
      inset 0 1px 0 rgb(var(--hud-border-strong-rgb) / 0.1),
      0 0 26px rgb(var(--hud-lime-rgb) / 0.22);
    pointer-events: auto;
  }

  .pause-panel p {
    margin: 0;
    color: rgb(var(--hud-text-main-rgb) / 0.98);
    font-size: clamp(0.76rem, 1.5vw, 1.08rem);
    letter-spacing: 0.08em;
    text-transform: uppercase;
    text-shadow: 0 0 10px rgb(var(--hud-lime-rgb) / 0.28);
  }

  .pause-mask.is-over {
    background:
      radial-gradient(circle at center, rgb(var(--hud-surface-rgb) / 0.22), rgb(var(--hud-bg-30) / 0.8)),
      repeating-linear-gradient(
        180deg,
        rgb(var(--hud-orange-rgb) / 0.1) 0,
        rgb(var(--hud-orange-rgb) / 0.1) 1px,
        transparent 1px,
        transparent 4px
      );
  }

  .pause-panel.is-over {
    border-color: rgb(var(--hud-orange-rgb) / 0.62);
    box-shadow:
      inset 0 1px 0 rgb(var(--hud-border-strong-rgb) / 0.1),
      0 0 32px rgb(var(--hud-orange-rgb) / 0.24);
    min-inline-size: min(24rem, 72%);
    display: grid;
    gap: 0.72rem;
    justify-items: center;
  }

  .pause-panel.is-over p {
    color: rgb(var(--hud-orange-rgb) / 0.98);
    text-shadow: 0 0 12px rgb(var(--hud-orange-rgb) / 0.36);
  }

  .overlay-restart-btn {
    min-block-size: clamp(2.2rem, 4.2vh, 2.8rem);
    min-inline-size: clamp(8.8rem, 18vw, 13rem);
    border: 1px solid rgb(var(--hud-cyan-rgb) / 0.62);
    border-radius: 999px;
    padding: 0.24rem 1rem;
    background: linear-gradient(180deg, rgb(var(--hud-surface-alt-rgb) / 0.96), rgb(var(--hud-surface-rgb) / 0.9));
    color: rgb(var(--hud-text-main-rgb) / 0.98);
    font: inherit;
    font-size: clamp(0.74rem, 1.3vw, 0.96rem);
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    cursor: pointer;
    box-shadow: 0 0 16px rgb(var(--hud-cyan-rgb) / 0.2);
    transition: transform 120ms ease, box-shadow 120ms ease, border-color 120ms ease;
  }

  .overlay-restart-btn:hover {
    transform: translateY(-1px);
    border-color: rgb(var(--hud-lime-rgb) / 0.66);
    box-shadow: 0 0 20px rgb(var(--hud-lime-rgb) / 0.28);
  }

  @media (max-width: 1120px) {
    .hud-grid {
      grid-template-columns: repeat(3, minmax(0, 1fr));
    }
  }
</style>
