"use client";

import { useEffect, useRef } from "react";

/**
 * Bloom — a noise-displaced dot sphere with a spectral gradient (deep-indigo
 * core -> magenta -> warm rim). Ported from the internal design-systems gallery
 * ("Depth / Bloom") as a self-contained animated hero background. Pure Canvas 2D;
 * pauses when off-screen or the tab is hidden, and renders a single static frame
 * when the user prefers reduced motion.
 */

type RGB = { r: number; g: number; b: number };
type V3 = { x: number; y: number; z: number };

const TAU = Math.PI * 2;

// Spectral gradient stops: [radial position, r, g, b].
const BLOOM_STOPS: Array<[number, number, number, number]> = [
  [0.0, 20, 16, 54],
  [0.3, 62, 40, 120],
  [0.52, 150, 52, 132],
  [0.68, 214, 72, 118],
  [0.84, 244, 120, 70],
];

function hexToRgb(hex: string): RGB {
  let h = hex.replace("#", "");
  if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
  const n = parseInt(h, 16);
  return { r: (n >> 16) & 255, g: (n >> 8) & 255, b: n & 255 };
}

function rotateY(p: V3, a: number): V3 {
  const c = Math.cos(a);
  const s = Math.sin(a);
  return { x: p.x * c + p.z * s, y: p.y, z: -p.x * s + p.z * c };
}

function rotateX(p: V3, a: number): V3 {
  const c = Math.cos(a);
  const s = Math.sin(a);
  return { x: p.x, y: p.y * c - p.z * s, z: p.y * s + p.z * c };
}

/** Displacement field — "the way it morphs". Integer phase multipliers loop seamlessly. */
function bloomDisp(mode: number, theta: number, phi: number, phase: number, detail: number, sym: number): number {
  const hi = 0.4 + detail;
  switch (mode) {
    case 1: // Ripples
      return (
        Math.sin(theta * sym * 2 - phase * 2) * 0.62 +
        Math.sin(theta * sym * 4 - phase * 3 + 1) * 0.3 * hi +
        Math.sin(phi * 2 + phase) * 0.14
      );
    case 2: // Twist
      return (
        Math.sin(phi * sym + theta * 5 + phase) * 0.55 +
        Math.sin(phi * sym - theta * 3 - phase * 2) * 0.34 * hi
      );
    case 3: // Lobes
      return (
        Math.cos(phi * sym + phase) * Math.sin(theta * 2) * 0.7 +
        Math.sin(theta * 3 + phase * 2) * 0.2 +
        Math.sin(phi * sym * 2 - phase) * 0.2 * hi
      );
    default: // 0 — Waves: smooth rolling swell; sym sets the fold count
      return (
        Math.sin(phi * sym + theta * 3 + phase) * 0.5 +
        Math.sin(phi * (sym + 1) - theta * 2 + phase * 2 + 1.7) * 0.32 +
        (Math.sin(theta * 4 - phase) * 0.3 + Math.sin(phi * (sym + 2) + theta + phase * 3) * 0.22) * hi
      );
  }
}

type BloomOpts = {
  amp: number;
  detail: number;
  spin: number;
  mode: number;
  sym: number;
  density: number;
  /* Dot radius multiplier, independent of `density`. Needed because the base
     radius is 1.5 / density, so lowering density for phone performance made
     each dot LARGER (1.43 vs 1.0 on desktop) rather than smaller. This lets
     the point count stay low while the dots shrink. */
  dotScale: number;
  /* Multiplier applied to the desaturated luma. 1 keeps the original
     brightness relationship; higher values push the dots toward flat white,
     which is what the old CSS brightness() filter was reaching for. */
  lift: number;
  accent: string;
  centerX: number;
  zoom: number;
};

type BloomPt = { sx: number; sy: number; z: number };

function renderBloom(ctx: CanvasRenderingContext2D, w: number, h: number, t: number, o: BloomOpts): void {
  ctx.clearRect(0, 0, w, h);

  const cols = Math.round(92 * o.density);
  const rows = Math.round(54 * o.density);
  const cx = w * o.centerX;
  const cy = h / 2;
  const reach = 1 + o.amp * 1.6;
  const fit = Math.min(w, h) * 0.43 * o.zoom;
  const scale = fit / reach;
  const maxR = fit;
  const camZ = 6;
  const phase = t * 0.5;
  const yaw = o.spin * t;
  const pitch = 0.18;
  const hot = hexToRgb(o.accent);

  const pts: BloomPt[] = [];
  for (let j = 1; j < rows; j++) {
    const theta = (j / rows) * Math.PI;
    const sinT = Math.sin(theta);
    const cosT = Math.cos(theta);
    for (let i = 0; i < cols; i++) {
      const phi = (i / cols) * TAU;
      const raw = bloomDisp(o.mode, theta, phi, phase, o.detail, o.sym);
      const disp = raw < -1.3 ? -1.3 : raw > 1.3 ? 1.3 : raw;
      const r = 1 + o.amp * disp;
      let p: V3 = {
        x: sinT * Math.cos(phi) * r,
        y: cosT * r,
        z: sinT * Math.sin(phi) * r,
      };
      p = rotateY(p, yaw);
      p = rotateX(p, pitch);
      if (p.z < -0.12) continue;
      const d = camZ / (camZ - p.z);
      pts.push({ sx: cx + p.x * scale * d, sy: cy - p.y * scale * d, z: p.z });
    }
  }
  pts.sort((a, b) => a.z - b.z);

  const base = (1.5 / o.density) * o.dotScale;
  const lastStop = BLOOM_STOPS[BLOOM_STOPS.length - 1];
  for (const pt of pts) {
    const rr = Math.min(1, Math.hypot(pt.sx - cx, pt.sy - cy) / maxR);
    let cr: number;
    let cg: number;
    let cb: number;
    if (rr >= lastStop[0]) {
      const f = (rr - lastStop[0]) / (1 - lastStop[0]);
      cr = lastStop[1] + (hot.r - lastStop[1]) * f;
      cg = lastStop[2] + (hot.g - lastStop[2]) * f;
      cb = lastStop[3] + (hot.b - lastStop[3]) * f;
    } else {
      let s = 0;
      while (s < BLOOM_STOPS.length - 2 && rr >= BLOOM_STOPS[s + 1][0]) s++;
      const a = BLOOM_STOPS[s];
      const b = BLOOM_STOPS[s + 1];
      const f = (rr - a[0]) / (b[0] - a[0]);
      cr = a[1] + (b[1] - a[1]) * f;
      cg = a[2] + (b[2] - a[2]) * f;
      cb = a[3] + (b[3] - a[3]) * f;
    }
    const size = base * (0.72 + rr * 0.6);
    /* Desaturated here rather than via a CSS filter. `filter: grayscale(1)
       brightness(100)` on the element fought the element's own `opacity`:
       a 100x multiplier re-saturates whatever opacity dimmed, and Chrome and
       Safari disagree on whether filter or opacity is applied first — so the
       canvas ignored its opacity on iOS while honouring it on desktop.
       Rec. 709 luma, then lifted toward white, so `opacity` is now the single
       control over how strongly it reads. */
    const luma = 0.2126 * cr + 0.7152 * cg + 0.0722 * cb;
    const g = Math.min(255, luma * o.lift) | 0;
    ctx.fillStyle = `rgba(${g}, ${g}, ${g}, 0.95)`;
    if (size < 1.2) {
      const sq = size * 1.8;
      ctx.fillRect(pt.sx - sq / 2, pt.sy - sq / 2, sq, sq);
    } else {
      ctx.beginPath();
      ctx.arc(pt.sx, pt.sy, size, 0, TAU);
      ctx.fill();
    }
  }
}

export default function BloomBackground({ className }: { className?: string }) {
  const canvasRef = useRef<HTMLCanvasElement>(null);

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

    const opts: BloomOpts = {
      amp: 0.3,
      detail: 0.5,
      spin: 0.03,
      mode: 0, // Waves
      sym: 5,
      density: 1,
      dotScale: 1,
      lift: 2.4,
      accent: "#ffb43a",
      centerX: 0.68,
      zoom: 1.4,
    };

    let raf = 0;
    let start = 0;
    let running = false;
    let cw = 0;
    let ch = 0;

    const resize = () => {
      const parent = canvas.parentElement;
      const rect = parent
        ? parent.getBoundingClientRect()
        : { width: window.innerWidth, height: window.innerHeight };
      cw = Math.max(1, Math.round(rect.width));
      ch = Math.max(1, Math.round(rect.height));
      const dpr = Math.min(window.devicePixelRatio || 1, 1.75);
      const phone = cw < 700;
      opts.density = phone ? 1.05 : 1.5;
      /* 0.40 lands the phone dot radius at ~0.57 against desktop's 1.0 — the
         low density stays (it is there for performance) but the dots no longer
         inflate with it. Below ~1.2 the renderer switches from arc() to a
         small fillRect, so these draw as fine specks rather than circles. */
      opts.dotScale = phone ? 0.4 : 1;
      opts.centerX = phone ? 0.5 : 0.68;
      opts.zoom = phone ? 1.5 : 1.4; // larger sphere on phones; it is faint enough now not to crowd the headline
      canvas.width = Math.round(cw * dpr);
      canvas.height = Math.round(ch * dpr);
      canvas.style.width = `${cw}px`;
      canvas.style.height = `${ch}px`;
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    };

    const frame = (now: number) => {
      if (!running) return;
      if (!start) start = now;
      const t = ((now - start) / 1000) * 0.35; // Speed
      renderBloom(ctx, cw, ch, t, opts);
      raf = requestAnimationFrame(frame);
    };

    const play = () => {
      // Ambient hero background: animate regardless of the reduced-motion /
      // battery-saver setting, which was freezing it to a static frame on
      // laptops and phones running in low-power mode.
      if (running || document.hidden) return;
      running = true;
      raf = requestAnimationFrame(frame);
    };
    const stop = () => {
      running = false;
      cancelAnimationFrame(raf);
    };

    resize();
    play();

    const onResize = () => {
      resize();
    };
    window.addEventListener("resize", onResize);

    const onVisibility = () => {
      if (document.hidden) stop();
      else play();
    };
    document.addEventListener("visibilitychange", onVisibility);

    // Pause when the hero scrolls out of view.
    const io = new IntersectionObserver(
      (entries) => {
        if (entries[0]?.isIntersecting) play();
        else stop();
      },
      { threshold: 0 },
    );
    io.observe(canvas);

    // Re-measure whenever the hero (parent) actually resizes. A one-shot mount
    // measurement can fire before layout has settled (parent width still 0).
    const ro = new ResizeObserver(() => {
      resize();
    });
    if (canvas.parentElement) ro.observe(canvas.parentElement);

    return () => {
      stop();
      window.removeEventListener("resize", onResize);
      document.removeEventListener("visibilitychange", onVisibility);
      io.disconnect();
      ro.disconnect();
    };
  }, []);

  return <canvas ref={canvasRef} className={className} aria-hidden="true" />;
}
