"use client";

import { useCallback, useEffect, useRef, useState } from "react";
import {
  BOOST_DASH_DISTANCE,
  BOOST_DASH_MS,
  BOOST_MAX_CHARGES,
  BOOST_MAX_COLUMN_INTERVAL,
  BOOST_MIN_CHARGES,
  BOOST_MIN_COLUMN_INTERVAL,
  BOOST_RADIUS,
  BOOST_SPEED_MULTIPLIER,
  COLUMN_GAP,
  COLUMN_GAP_MARGIN,
  COLUMN_SPACING,
  COLUMN_WIDTH,
  DIFFICULTY_RAMP_SPAN,
  DIFFICULTY_START_SCORE,
  FIXED_DT_MS,
  FLAP_IMPULSE,
  FORWARD_SPEED,
  GAP_MIN_MULTIPLIER,
  GROUND_Y,
  ICARUS_RADIUS,
  ICARUS_X,
  MAX_FRAME_DT_MS,
  SHATTER_ANIM_MS,
  SPEED_MAX_MULTIPLIER,
  TILT_MAX_RADIANS,
  VIEWPORT_HEIGHT,
  VIEWPORT_WIDTH,
  WING_FLAP_ANIM_MS,
  WING_FLAP_MAX_RADIANS,
  WING_IDLE_PEAK_RADIANS,
  WING_IDLE_PERIOD_MS,
  WING_REST_RADIANS,
} from "@/lib/icarus/constants";
import {
  collidesWithBoost,
  collidesWithColumn,
  createInitialIcarus,
  hasHitGround,
  stepIcarus,
} from "@/lib/icarus/physics";
import type { Boost, Column, IcarusState, ShatterEffect } from "@/lib/icarus/types";
import { playBoostSound } from "@/lib/icarus/sound";
import { Confetti } from "@/components/confetti";

type Status = "idle" | "flying" | "dead";

// 0 avant DIFFICULTY_START_SCORE, monte jusqu'à 1 sur DIFFICULTY_RAMP_SPAN
// points, puis reste à 1 (palier, pour ne pas devenir injouable).
function difficultyProgress(score: number): number {
  if (score <= DIFFICULTY_START_SCORE) return 0;
  return Math.min(1, (score - DIFFICULTY_START_SCORE) / DIFFICULTY_RAMP_SPAN);
}

function randomGapCenter(gap: number): number {
  const minCenter = gap / 2 + COLUMN_GAP_MARGIN;
  const maxCenter = GROUND_Y - gap / 2 - COLUMN_GAP_MARGIN;
  return minCenter + Math.random() * Math.max(0, maxCenter - minCenter);
}

// Prochain boost dans BOOST_MIN..MAX_COLUMN_INTERVAL colonnes (tirage
// entier inclusif des deux bornes).
function randomBoostInterval(): number {
  return (
    BOOST_MIN_COLUMN_INTERVAL +
    Math.floor(Math.random() * (BOOST_MAX_COLUMN_INTERVAL - BOOST_MIN_COLUMN_INTERVAL + 1))
  );
}

// Distance du boost (en colonnes), tirée à chaque cueillette (tirage
// entier inclusif des deux bornes) : le bouclier dure le temps des N
// prochaines colonnes rencontrées, qu'elles soient cassées (touchées) ou
// simplement franchies sans contact — pas un budget de casses.
function randomBoostDistance(): number {
  return BOOST_MIN_CHARGES + Math.floor(Math.random() * (BOOST_MAX_CHARGES - BOOST_MIN_CHARGES + 1));
}

// Une aile, dessinée pointant vers -x ; side=-1 la reflète pour l'aile
// opposée. scale(side,1) inverse déjà le sens de rotation pour le côté
// reflété — passer le même wingAngle (sans le renverser à la main) aux deux
// appels donne un battement symétrique ; les deux ailes sont dessinées avec
// la même couleur et par-dessus le corps pour qu'aucune des deux ne soit
// partiellement cachée (ce qui donnait l'impression qu'une seule bougeait).
function drawWing(ctx: CanvasRenderingContext2D, side: 1 | -1, wingAngle: number) {
  ctx.save();
  ctx.scale(side, 1);
  ctx.rotate(wingAngle);
  ctx.fillStyle = "#F4ECD8";
  ctx.beginPath();
  ctx.moveTo(-1, -2);
  ctx.quadraticCurveTo(-ICARUS_RADIUS * 1.6, -ICARUS_RADIUS * 1.9, -ICARUS_RADIUS * 2.3, -ICARUS_RADIUS * 0.8);
  ctx.quadraticCurveTo(-ICARUS_RADIUS * 1.3, -ICARUS_RADIUS * 0.15, -ICARUS_RADIUS * 0.3, ICARUS_RADIUS * 0.55);
  ctx.closePath();
  ctx.fill();

  ctx.strokeStyle = "#C9A227";
  ctx.lineWidth = 1;
  for (let i = 1; i <= 2; i++) {
    ctx.beginPath();
    ctx.moveTo(-1, -2);
    ctx.lineTo(-ICARUS_RADIUS * (0.9 + i * 0.5), -ICARUS_RADIUS * (0.5 + i * 0.35));
    ctx.stroke();
  }
  ctx.restore();
}

function drawIcarus(
  ctx: CanvasRenderingContext2D,
  x: number,
  y: number,
  tilt: number,
  wingAngle: number,
  boosted: boolean,
) {
  ctx.save();
  ctx.translate(x, y);
  ctx.rotate(tilt);

  if (boosted) {
    ctx.save();
    ctx.strokeStyle = "#E7C560";
    ctx.lineWidth = 2;
    ctx.beginPath();
    ctx.arc(0, 0, ICARUS_RADIUS + 5, 0, Math.PI * 2);
    ctx.stroke();
    ctx.restore();
  }

  ctx.fillStyle = "#E7C560";
  ctx.beginPath();
  ctx.ellipse(0, 0, ICARUS_RADIUS * 0.85, ICARUS_RADIUS, 0, 0, Math.PI * 2);
  ctx.fill();
  ctx.beginPath();
  ctx.arc(ICARUS_RADIUS * 0.5, -ICARUS_RADIUS * 0.4, ICARUS_RADIUS * 0.48, 0, Math.PI * 2);
  ctx.fill();

  drawWing(ctx, -1, wingAngle);
  drawWing(ctx, 1, wingAngle);

  ctx.restore();
}

function drawColumnShaft(
  ctx: CanvasRenderingContext2D,
  x: number,
  top: number,
  height: number,
  capAtBottom: boolean,
) {
  if (height <= 0) return;

  ctx.fillStyle = "#F4ECD8";
  ctx.fillRect(x, top, COLUMN_WIDTH, height);

  ctx.strokeStyle = "#C9A227";
  ctx.lineWidth = 1;
  for (let i = 1; i < 4; i++) {
    const lineX = x + (COLUMN_WIDTH / 4) * i;
    ctx.beginPath();
    ctx.moveTo(lineX, top);
    ctx.lineTo(lineX, top + height);
    ctx.stroke();
  }

  const capHeight = Math.min(10, height);
  const capY = capAtBottom ? top + height - capHeight : top;
  ctx.fillStyle = "#E7C560";
  ctx.fillRect(x - 4, capY, COLUMN_WIDTH + 8, capHeight);
}

function drawColumn(ctx: CanvasRenderingContext2D, column: Column) {
  const gapTop = column.gapCenterY - column.gap / 2;
  const gapBottom = column.gapCenterY + column.gap / 2;
  drawColumnShaft(ctx, column.x, 0, gapTop, true);
  drawColumnShaft(ctx, column.x, gapBottom, GROUND_Y - gapBottom, false);
}

// Éclair de Zeus ramassable : halo pulsant + éclair sombre par-dessus, pour
// rester lisible sur le halo doré.
function drawBoost(ctx: CanvasRenderingContext2D, boost: Boost, now: number) {
  const pulse = 0.5 + 0.5 * Math.sin(now / 200);

  ctx.save();
  ctx.translate(boost.x, boost.y);

  const glow = ctx.createRadialGradient(0, 0, 2, 0, 0, BOOST_RADIUS + 6 + pulse * 3);
  glow.addColorStop(0, "#E7C560");
  glow.addColorStop(1, "rgba(231, 197, 96, 0)");
  ctx.fillStyle = glow;
  ctx.beginPath();
  ctx.arc(0, 0, BOOST_RADIUS + 6 + pulse * 3, 0, Math.PI * 2);
  ctx.fill();

  ctx.fillStyle = "#C9A227";
  ctx.beginPath();
  ctx.arc(0, 0, BOOST_RADIUS, 0, Math.PI * 2);
  ctx.fill();

  ctx.fillStyle = "#0A1B33";
  ctx.beginPath();
  ctx.moveTo(-2, -9);
  ctx.lineTo(4, -1);
  ctx.lineTo(0, -1);
  ctx.lineTo(3, 9);
  ctx.lineTo(-5, 0);
  ctx.lineTo(-1, 0);
  ctx.closePath();
  ctx.fill();

  ctx.restore();
}

// Fragments qui s'écartent du centre d'une colonne détruite par un boost,
// en s'estompant — purement décoratif, dérivé du temps écoulé (pas de
// state React par effet).
function drawShatterEffect(ctx: CanvasRenderingContext2D, effect: ShatterEffect, now: number) {
  const progress = Math.min(1, (now - effect.createdAtMs) / SHATTER_ANIM_MS);
  if (progress >= 1) return;

  ctx.save();
  ctx.translate(effect.x, effect.y);
  ctx.globalAlpha = 1 - progress;
  ctx.strokeStyle = "#E7C560";
  ctx.lineWidth = 2;

  const pieces = 8;
  for (let i = 0; i < pieces; i++) {
    const angle = (i / pieces) * Math.PI * 2;
    const dist = progress * 34;
    ctx.beginPath();
    ctx.moveTo(Math.cos(angle) * dist, Math.sin(angle) * dist);
    ctx.lineTo(Math.cos(angle) * (dist + 9), Math.sin(angle) * (dist + 9));
    ctx.stroke();
  }
  ctx.restore();
}

// Filets de vitesse continus tant que le bouclier est actif : Icare est
// figé (pas de mouvement propre), donc sans ça la sensation de vitesse ne
// venait que du défilement du décor — ces traits, qui naissent devant lui
// et filent vers l'arrière en boucle, rendent la vitesse lisible même à
// l'arrêt visuel d'Icare. Purement dérivé de "now" (pas de state), 6
// lignes déphasées pour ne pas paraître synchronisées.
function drawSpeedLines(ctx: CanvasRenderingContext2D, centerX: number, centerY: number, now: number) {
  const lineCount = 6;
  const cycleMs = 260;

  ctx.save();
  ctx.strokeStyle = "#E7C560";
  ctx.lineCap = "round";
  ctx.lineWidth = 2;

  for (let i = 0; i < lineCount; i++) {
    const seed = i * 137;
    const phase = ((now + seed) % cycleMs) / cycleMs;
    const yOffset = ((i % 3) - 1) * 14 + (((i * 53) % 20) - 10);
    const length = 14 + (i % 3) * 6;
    const startX = centerX + 36 - phase * 100;
    const alpha = 0.6 * Math.sin(phase * Math.PI);
    if (alpha <= 0) continue;

    ctx.globalAlpha = alpha;
    ctx.beginPath();
    ctx.moveTo(startX, centerY + yOffset);
    ctx.lineTo(startX - length, centerY + yOffset);
    ctx.stroke();
  }
  ctx.restore();
}

export type IcarusRunTelemetry = {
  v: 1;
  duration_ms: number;
  actions: number;
};

export function IcarusGame({
  onStart,
  onFinish,
}: {
  onStart: () => void;
  onFinish: (score: number, run: IcarusRunTelemetry) => void;
}) {
  const canvasRef = useRef<HTMLCanvasElement | null>(null);
  const containerRef = useRef<HTMLDivElement | null>(null);
  const [status, setStatus] = useState<Status>("idle");
  const [score, setScore] = useState(0);

  const onStartRef = useRef(onStart);
  useEffect(() => {
    onStartRef.current = onStart;
  }, [onStart]);

  const onFinishRef = useRef(onFinish);
  useEffect(() => {
    onFinishRef.current = onFinish;
  }, [onFinish]);

  const icarusRef = useRef<IcarusState>(createInitialIcarus());
  const columnsRef = useRef<Column[]>([]);
  const statusRef = useRef<Status>("idle");
  const scoreRef = useRef(0);
  const flapRequestedRef = useRef(false);
  const lastFlapAtRef = useRef<number | null>(null);
  const boostsRef = useRef<Boost[]>([]);
  const columnsSinceBoostRef = useRef(0);
  const nextBoostThresholdRef = useRef(randomBoostInterval());
  const boostColumnsRemainingRef = useRef(0);
  const boostDashAtRef = useRef<number | null>(null);
  const shatterEffectsRef = useRef<ShatterEffect[]>([]);
  const nextEntityIdRef = useRef(0);
  const runStartedAtRef = useRef<number | null>(null);
  const actionCountRef = useRef(0);

  const resetGame = useCallback(() => {
    icarusRef.current = createInitialIcarus();
    columnsRef.current = [];
    scoreRef.current = 0;
    statusRef.current = "idle";
    flapRequestedRef.current = false;
    lastFlapAtRef.current = null;
    boostsRef.current = [];
    columnsSinceBoostRef.current = 0;
    nextBoostThresholdRef.current = randomBoostInterval();
    boostColumnsRemainingRef.current = 0;
    boostDashAtRef.current = null;
    shatterEffectsRef.current = [];
    runStartedAtRef.current = null;
    actionCountRef.current = 0;
    setStatus("idle");
    setScore(0);
  }, []);

  const requestFlap = useCallback(() => {
    if (statusRef.current === "dead") {
      resetGame();
      return;
    }

    if (statusRef.current === "idle") {
      runStartedAtRef.current = Date.now();
      actionCountRef.current = 1;
      statusRef.current = "flying";
      setStatus("flying");
      onStartRef.current();
    } else {
      actionCountRef.current += 1;
    }

    flapRequestedRef.current = true;
  }, [resetGame]);

  // Sur ordinateur (pas de tap tactile) : la barre d'espace fait la même
  // chose qu'un tap. event.repeat ignoré pour ne pas enchaîner des
  // battements tant que la touche reste appuyée (même sémantique qu'un tap
  // isolé) ; preventDefault pour empêcher le défilement de la page.
  useEffect(() => {
    function handleKeyDown(event: KeyboardEvent) {
      if (event.code !== "Space" || event.repeat) return;
      event.preventDefault();
      requestFlap();
    }
    window.addEventListener("keydown", handleKeyDown);
    return () => window.removeEventListener("keydown", handleKeyDown);
  }, [requestFlap]);

  useEffect(() => {
    const canvas = canvasRef.current;
    const container = containerRef.current;
    if (!canvas || !container) return;
    const ctx = canvas.getContext("2d");
    if (!ctx) return;

    // "Contain" dans le conteneur (largeur ET hauteur, contrairement à avant
    // où seule la largeur comptait) : nécessaire en plein écran, où la
    // hauteur disponible varie et n'est plus déduite de la largeur.
    function resize() {
      const dpr = window.devicePixelRatio || 1;
      const availableWidth = container!.clientWidth;
      const availableHeight = container!.clientHeight;
      let cssWidth = availableWidth;
      let cssHeight = (cssWidth * VIEWPORT_HEIGHT) / VIEWPORT_WIDTH;
      if (cssHeight > availableHeight) {
        cssHeight = availableHeight;
        cssWidth = (cssHeight * VIEWPORT_WIDTH) / VIEWPORT_HEIGHT;
      }
      canvas!.width = cssWidth * dpr;
      canvas!.height = cssHeight * dpr;
      canvas!.style.width = `${cssWidth}px`;
      canvas!.style.height = `${cssHeight}px`;
    }

    resize();
    const observer = new ResizeObserver(resize);
    observer.observe(container);

    let rafId: number;
    let lastFrame: number | null = null;
    let accumulator = 0;

    function finishRun() {
      statusRef.current = "dead";
      setStatus("dead");
      const startedAt = runStartedAtRef.current;
      onFinishRef.current(scoreRef.current, {
        v: 1,
        duration_ms: startedAt === null ? 0 : Math.max(0, Date.now() - startedAt),
        actions: actionCountRef.current,
      });
    }

    function frame(now: number) {
      if (lastFrame === null) lastFrame = now;
      const frameDt = Math.min(now - lastFrame, MAX_FRAME_DT_MS);
      lastFrame = now;

      if (statusRef.current === "flying") {
        accumulator += frameDt;
        // Capturé puis effacé une fois pour toutes : évite qu'un même tap ne
        // déclenche plusieurs battements si l'accumulateur itère plus d'une
        // fois dans la même frame (après un ralentissement ponctuel).
        let shouldFlap = flapRequestedRef.current;
        flapRequestedRef.current = false;

        while (accumulator >= FIXED_DT_MS) {
          // Tant que le bouclier est actif, Icare est immobile (pas de
          // gravité, les taps n'ont aucun effet) et le décor défile vite à
          // sa place — impossible de toucher au mouvement pendant cette
          // période.
          if (boostColumnsRemainingRef.current > 0) {
            shouldFlap = false;
          } else {
            if (shouldFlap) {
              icarusRef.current = { ...icarusRef.current, velocityY: FLAP_IMPULSE };
              lastFlapAtRef.current = now;
              shouldFlap = false;
            }
            icarusRef.current = stepIcarus(icarusRef.current, FIXED_DT_MS);
          }

          // Difficulté progressive : vitesse en hausse et écart resserré au
          // fil du score, jusqu'à un palier (voir difficultyProgress).
          const progress = difficultyProgress(scoreRef.current);
          const baseSpeed = FORWARD_SPEED * (1 + (SPEED_MAX_MULTIPLIER - 1) * progress);
          // Défilement accéléré tant que le bouclier du boost est actif
          // (sans risque : une colonne touchée casse au lieu de tuer).
          const currentSpeed = boostColumnsRemainingRef.current > 0 ? baseSpeed * BOOST_SPEED_MULTIPLIER : baseSpeed;
          const currentGap = COLUMN_GAP * (1 - (1 - GAP_MIN_MULTIPLIER) * progress);

          for (const column of columnsRef.current) {
            column.x -= currentSpeed * (FIXED_DT_MS / 1000);
            if (!column.scored && column.x + COLUMN_WIDTH < ICARUS_X - ICARUS_RADIUS) {
              column.scored = true;
              scoreRef.current += 1;
              setScore(scoreRef.current);
              // Colonne franchie sans contact pendant le boost : compte
              // quand même dans la distance du bouclier (voir plus bas).
              if (boostColumnsRemainingRef.current > 0) {
                boostColumnsRemainingRef.current -= 1;
              }
            }
          }
          columnsRef.current = columnsRef.current.filter((c) => c.x + COLUMN_WIDTH > 0);

          const last = columnsRef.current[columnsRef.current.length - 1];
          if (!last || last.x < VIEWPORT_WIDTH - COLUMN_SPACING) {
            const spawned: Column = {
              x: last ? last.x + COLUMN_SPACING : VIEWPORT_WIDTH,
              gapCenterY: randomGapCenter(currentGap),
              gap: currentGap,
              scored: false,
            };
            columnsRef.current.push(spawned);

            // Rare et exceptionnel : intervalle large (voir constants.ts),
            // pas de plafond dur — placé au centre du trou de la colonne
            // qui le porte, toujours atteignable en volant simplement au
            // milieu du passage.
            columnsSinceBoostRef.current += 1;
            if (columnsSinceBoostRef.current >= nextBoostThresholdRef.current) {
              columnsSinceBoostRef.current = 0;
              nextBoostThresholdRef.current = randomBoostInterval();
              boostsRef.current.push({
                id: nextEntityIdRef.current++,
                x: spawned.x + COLUMN_WIDTH / 2,
                y: spawned.gapCenterY,
              });
            }
          }

          for (const boost of boostsRef.current) {
            boost.x -= currentSpeed * (FIXED_DT_MS / 1000);
          }
          boostsRef.current = boostsRef.current.filter((b) => b.x + BOOST_RADIUS > 0);

          const collectedIndex = boostsRef.current.findIndex((b) => collidesWithBoost(icarusRef.current.y, b));
          if (collectedIndex !== -1) {
            boostsRef.current.splice(collectedIndex, 1);
            playBoostSound();
            // Active le bouclier pour les N prochaines colonnes (distance
            // aléatoire, voir plus bas) et déclenche l'animation de bond en
            // avant. Icare se fige (vitesse verticale annulée, plus de
            // gravité ni de prise en compte des taps tant que dure le
            // bouclier, voir plus haut).
            boostColumnsRemainingRef.current = randomBoostDistance();
            boostDashAtRef.current = now;
            icarusRef.current = { ...icarusRef.current, velocityY: 0 };
          }

          // Colonnes réellement touchées (mortelles en temps normal). Tant
          // que la distance du bouclier n'est pas épuisée, une colonne
          // touchée se casse au lieu de tuer et compte dans cette distance
          // (comme une colonne franchie sans contact, voir plus haut) ;
          // sinon mort normale.
          const touchedColumns = columnsRef.current.filter((c) => collidesWithColumn(icarusRef.current.y, c));
          if (touchedColumns.length > 0) {
            if (boostColumnsRemainingRef.current > 0) {
              const broken = touchedColumns.slice(0, boostColumnsRemainingRef.current);
              boostColumnsRemainingRef.current -= broken.length;
              for (const column of broken) {
                if (!column.scored) {
                  column.scored = true;
                  scoreRef.current += 1;
                }
                shatterEffectsRef.current.push({
                  id: nextEntityIdRef.current++,
                  x: column.x + COLUMN_WIDTH / 2,
                  y: column.gapCenterY,
                  createdAtMs: now,
                });
              }
              columnsRef.current = columnsRef.current.filter((c) => !broken.includes(c));
              setScore(scoreRef.current);
            } else {
              finishRun();
              break;
            }
          }

          if (boostColumnsRemainingRef.current === 0 && hasHitGround(icarusRef.current)) {
            finishRun();
            break;
          }

          accumulator -= FIXED_DT_MS;
        }
      }

      shatterEffectsRef.current = shatterEffectsRef.current.filter((e) => now - e.createdAtMs < SHATTER_ANIM_MS);

      // Bond en avant à la cueillette : aller-retour (pic à mi-parcours),
      // pas un déplacement qui persiste au-delà de l'animation.
      const dashElapsed = boostDashAtRef.current === null ? Infinity : now - boostDashAtRef.current;
      const dashProgress = Math.min(1, dashElapsed / BOOST_DASH_MS);
      const dashOffset = dashProgress >= 1 ? 0 : Math.sin(dashProgress * Math.PI) * BOOST_DASH_DISTANCE;

      // Battement continu en boucle (se voit même sans taper, dès l'écran de
      // repos) : oscille doucement entre le repos et un pic modéré.
      const idlePhase = (now % WING_IDLE_PERIOD_MS) / WING_IDLE_PERIOD_MS;
      const idleWave = 0.5 - 0.5 * Math.cos(idlePhase * 2 * Math.PI);
      const idleWingAngle = WING_REST_RADIANS + (WING_IDLE_PEAK_RADIANS - WING_REST_RADIANS) * idleWave;

      // Battement plus ample déclenché par un tap, qui prend le dessus sur
      // le cycle continu le temps de sa décrue.
      const sinceFlapMs = now - (lastFlapAtRef.current ?? -Infinity);
      const flapProgress = Math.max(0, 1 - sinceFlapMs / WING_FLAP_ANIM_MS);
      const eased = flapProgress * flapProgress;
      const tapWingAngle = WING_REST_RADIANS + (WING_FLAP_MAX_RADIANS - WING_REST_RADIANS) * eased;

      const wingAngle = statusRef.current === "dead" ? WING_REST_RADIANS : Math.max(idleWingAngle, tapWingAngle);

      render(
        ctx!,
        canvas!,
        icarusRef.current,
        columnsRef.current,
        boostsRef.current,
        shatterEffectsRef.current,
        dashOffset,
        boostColumnsRemainingRef.current > 0,
        statusRef.current,
        scoreRef.current,
        wingAngle,
        now,
      );
      rafId = requestAnimationFrame(frame);
    }

    rafId = requestAnimationFrame(frame);

    return () => {
      observer.disconnect();
      cancelAnimationFrame(rafId);
    };
  }, []);

  const prefersReducedMotion =
    typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;

  return (
    <div
      ref={containerRef}
      onPointerDown={requestFlap}
      className="relative flex flex-1 touch-none items-center justify-center overflow-hidden bg-ink select-none"
    >
      <canvas ref={canvasRef} className="block" />

      {status === "dead" && !prefersReducedMotion && <Confetti />}

      {status === "idle" && (
        <div className="pointer-events-none absolute inset-x-0 bottom-10 flex justify-center px-6">
          <p className="marble-surface animate-reveal rounded-lg border border-gold/40 px-4 py-2 text-center text-sm text-text-marble shadow-lg">
            Touche l&apos;écran pour battre des ailes et t&apos;envoler entre les colonnes.
          </p>
        </div>
      )}

      {status === "dead" && (
        <div className="absolute inset-0 flex items-center justify-center bg-ink/50">
          <div className="marble-surface animate-reveal flex flex-col items-center gap-2 rounded-xl border border-gold/40 px-6 py-4 text-center shadow-lg">
            <p className="text-xs tracking-wide text-text-mut uppercase">Score</p>
            <p className="font-heading text-3xl text-gold-bright">{score}</p>
            <button
              type="button"
              onPointerDown={(event) => event.stopPropagation()}
              onClick={resetGame}
              className="mt-1 rounded-md bg-ink-2 px-4 py-2 font-heading text-sm tracking-wide text-gold-bright uppercase transition hover:bg-ink"
            >
              Rejouer
            </button>
          </div>
        </div>
      )}
    </div>
  );
}

function render(
  ctx: CanvasRenderingContext2D,
  canvas: HTMLCanvasElement,
  icarus: IcarusState,
  columns: Column[],
  boosts: Boost[],
  shatterEffects: ShatterEffect[],
  dashOffset: number,
  boosted: boolean,
  status: Status,
  score: number,
  wingAngle: number,
  now: number,
) {
  const dpr = window.devicePixelRatio || 1;
  const cssWidth = canvas.width / dpr;
  const worldScale = cssWidth / VIEWPORT_WIDTH;

  ctx.save();
  ctx.scale(dpr, dpr);
  ctx.save();
  ctx.scale(worldScale, worldScale);

  ctx.clearRect(0, 0, VIEWPORT_WIDTH, VIEWPORT_HEIGHT);
  ctx.fillStyle = "#0A1B33";
  ctx.fillRect(0, 0, VIEWPORT_WIDTH, VIEWPORT_HEIGHT);

  for (const column of columns) {
    drawColumn(ctx, column);
  }

  for (const boost of boosts) {
    drawBoost(ctx, boost, now);
  }

  for (const effect of shatterEffects) {
    drawShatterEffect(ctx, effect, now);
  }

  ctx.fillStyle = "#2A2116";
  ctx.fillRect(0, GROUND_Y, VIEWPORT_WIDTH, VIEWPORT_HEIGHT - GROUND_Y);

  const icarusX = ICARUS_X + dashOffset;

  if (boosted) {
    drawSpeedLines(ctx, icarusX, icarus.y, now);
  }

  // Traînée de vitesse derrière Icare pendant le bond en avant.
  if (dashOffset > 1) {
    ctx.save();
    ctx.translate(icarusX, icarus.y);
    for (let i = 1; i <= 3; i++) {
      ctx.globalAlpha = (dashOffset / BOOST_DASH_DISTANCE) * (0.3 / i);
      ctx.fillStyle = "#E7C560";
      ctx.beginPath();
      ctx.ellipse(-i * 10, 0, ICARUS_RADIUS * 0.7, ICARUS_RADIUS * 0.5, 0, 0, Math.PI * 2);
      ctx.fill();
    }
    ctx.restore();
  }

  const tilt = Math.max(-1, Math.min(1, icarus.velocityY / 300)) * TILT_MAX_RADIANS;
  drawIcarus(ctx, icarusX, icarus.y, tilt, wingAngle, boosted);

  ctx.restore();

  if (status === "flying") {
    ctx.save();
    ctx.fillStyle = "#F4ECD8";
    ctx.font = "700 28px Cinzel, serif";
    ctx.textAlign = "center";
    ctx.fillText(String(score), cssWidth / 2, 44);
    ctx.restore();
  }

  ctx.restore();
}
