/* Background — canvas con doble modo:

   DESKTOP (> 1024px): dot grid con vacío + spring + partículas + ripples.
   El campo de puntitos sigue al cursor con vacío gravitacional; los dots
   springan de vuelta a su anchor (Hooke); partículas drift suaves; click
   = ripple expandido. Esto es el efecto original y se preserva intacto.

   MOBILE/TABLET (≤ 1024px) — V3 ADDENDUM 22 (2026-05-18):
   Adaptación del efecto BeamsBackground de 21st.dev al sistema actual.
   NO se copia el componente TSX literal — solo se reusa la idea visual y
   la lógica canvas (Beam con x/y/angle/length/width/speed/hue/pulse).
   Cero dependencias nuevas: nada de motion, nada de shadcn, nada de
   TypeScript. Mismo canvas#bg-canvas, mismo entrypoint, mismas APIs
   (`__NG_BG`, `__NG_SET_ACCENT`, etc.). Beams son haces de luz diagonales
   con gradient lineal + blur suave + pulso de opacidad. Colores en rango
   cyan→violet (HSL 190–270) coherentes con GalaxIA. Performance: DPR
   capado en 1.5, 6–12 beams según pointer/reduced-motion, pausa con
   document.hidden, cancelAnimationFrame al unmount. */
(function () {
  const canvas = document.getElementById('bg-canvas');
  if (!canvas) return;
  const ctx = canvas.getContext('2d', { alpha: true });

  const MOBILE_MQ = window.matchMedia('(max-width: 1024px)');

  const state = {
    mode: MOBILE_MQ.matches ? 'beams' : 'dots',
    w: 0, h: 0, dpr: 1,
    mouseX: -9999, mouseY: -9999,
    targetX: -9999, targetY: -9999,
    scrollY: 0,
    spacing: (window.__NG_DEFAULTS && window.__NG_DEFAULTS.dotDensity) || 44,
    spotlight: (window.__NG_DEFAULTS && window.__NG_DEFAULTS.spotlight) || 260,
    accentRGB: [51, 194, 234],
    dots: [],
    particles: [],
    beams: [],
    ripples: [],
    reduced: window.matchMedia('(prefers-reduced-motion: reduce)').matches,
    coarse: window.matchMedia('(pointer: coarse)').matches,
  };
  window.__NG_BG = state;

  function setAccent(name) {
    if (name === 'violet') state.accentRGB = [158, 123, 255];
    else if (name === 'green') state.accentRGB = [80, 210, 160];
    else state.accentRGB = [51, 194, 234];
  }
  setAccent((window.__NG_DEFAULTS && window.__NG_DEFAULTS.accent) || 'cyan');
  window.__NG_SET_ACCENT = setAccent;
  window.__NG_SET_SPACING = (v) => { state.spacing = v; if (state.mode === 'dots') seedDots(); };
  window.__NG_SET_SPOTLIGHT = (v) => { state.spotlight = v; };

  function resize() {
    const isMobile = state.mode === 'beams';
    state.dpr = Math.min(window.devicePixelRatio || 1, isMobile ? 1.5 : 2);
    state.w = window.innerWidth;
    state.h = window.innerHeight;
    canvas.width = state.w * state.dpr;
    canvas.height = state.h * state.dpr;
    canvas.style.width = state.w + 'px';
    canvas.style.height = state.h + 'px';
    /* Resetear transform antes de aplicar DPR para evitar acumulación
       en re-sizes. */
    ctx.setTransform(state.dpr, 0, 0, state.dpr, 0, 0);
    if (isMobile) {
      seedBeams();
    } else {
      seedDots();
      seedParticles();
    }
  }

  /* ═══════════════════ DESKTOP MODE: dots + particles ═══════════════════ */
  function seedDots() {
    const spacing = state.spacing;
    const cols = Math.ceil(state.w / spacing) + 2;
    const rows = Math.ceil(state.h / spacing) + 2;
    state.dots = new Array(cols * rows);
    let k = 0;
    for (let i = 0; i < cols; i++) {
      for (let j = 0; j < rows; j++) {
        const ax = i * spacing;
        const ay = j * spacing;
        state.dots[k++] = { ax, ay, x: ax, y: ay, vx: 0, vy: 0 };
      }
    }
  }

  function seedParticles() {
    const target = state.coarse ? 22 : 40;
    state.particles = [];
    for (let i = 0; i < target; i++) {
      state.particles.push({
        x: Math.random() * state.w,
        y: Math.random() * state.h,
        vx: (Math.random() - 0.5) * 0.12,
        vy: (Math.random() - 0.5) * 0.12,
        r: 0.6 + Math.random() * 1.2,
        a: 0.08 + Math.random() * 0.10,
        layer: Math.random() < 0.4 ? 0.4 : (Math.random() < 0.7 ? 0.7 : 1.0),
      });
    }
  }

  /* ═══════════════════ MOBILE/TABLET MODE: beams ═══════════════════ */
  function createBeam(randomProgress) {
    /* V3 polish ADDENDUM 26 (2026-05-18) — beams más intensos:
       - opacidad base 0.18-0.45 (antes 0.12-0.34) → +50%.
       - speed 0.30-0.85 (antes 0.18-0.50) → +60-70%.
       - width 100-340 (antes 80-260) → +30%.
       - hue ampliado 180→285 (cyan-azul-violet más vivo).
       - lightness 65-72% (antes 65% fijo).
       - pulseSpeed 0.006-0.016 (antes 0.004-0.012) → pulso más visible. */
    const angle = (Math.PI / 5) + (Math.random() - 0.5) * (Math.PI / 6);
    const length = state.h * 1.4 + Math.random() * state.h * 0.6;
    const width = 100 + Math.random() * 240;
    const hue = 180 + Math.random() * 105;
    const lightness = 65 + Math.random() * 7;
    const offX = Math.sin(angle) * length;
    const baseX = randomProgress
      ? -offX * 0.5 + Math.random() * (state.w + offX)
      : -offX * 0.5 + Math.random() * state.w * 0.4;
    const baseY = randomProgress
      ? -length + Math.random() * (state.h + length)
      : -length - Math.random() * state.h * 0.3;
    return {
      x: baseX, y: baseY,
      angle, length, width,
      speed: 0.30 + Math.random() * 0.55,
      hue, lightness,
      opacity: 0.18 + Math.random() * 0.27,
      pulse: Math.random() * Math.PI * 2,
      pulseSpeed: 0.006 + Math.random() * 0.010,
    };
  }

  function seedBeams() {
    /* Cantidad adaptativa: menos en reduced-motion, menos en touch screens
       (típicamente lower-end hardware), más en tablet con mouse.
       Addendum 26: subimos cantidad base (8/13/15) para más presencia. */
    const target = state.reduced ? 8 : (state.coarse ? 13 : 15);
    state.beams = [];
    for (let i = 0; i < target; i++) {
      state.beams.push(createBeam(true));
    }
  }

  function drawBeams() {
    /* Fondo negro semi-transparente para difumar trails y dar sensación
       de profundidad espacial. clearRect simple es suficiente porque
       cada beam dibuja con gradient + alpha. */
    ctx.clearRect(0, 0, state.w, state.h);

    /* Aplicamos blur a TODOS los beams en una sola pasada con ctx.filter.
       Esto da el "atmospheric glow" sin tener que pintar shadow por beam.
       Addendum 26: blur reducido de 28 → 22 px para que los beams se vean
       más definidos sin perder el atmospheric. */
    ctx.save();
    ctx.filter = 'blur(22px)';
    ctx.globalCompositeOperation = 'lighter';

    for (let i = 0; i < state.beams.length; i++) {
      const b = state.beams[i];

      /* Update si no hay reduced-motion. */
      if (!state.reduced) {
        b.x += Math.sin(b.angle) * b.speed;
        b.y += Math.cos(b.angle) * b.speed;
        b.pulse += b.pulseSpeed;
      }

      /* Respawn cuando el beam sale por abajo o muy a la derecha. */
      const exitY = b.y;
      const exitX = b.x + Math.sin(b.angle) * b.length;
      if (exitY > state.h + 100 || exitX > state.w + b.width + 200) {
        state.beams[i] = createBeam(false);
        continue;
      }

      /* Pulso de opacidad ±25% del base. */
      const pulsedOpacity = b.opacity * (0.75 + Math.sin(b.pulse) * 0.25);

      ctx.save();
      ctx.translate(b.x, b.y);
      ctx.rotate(b.angle);
      /* Gradient longitudinal: fade-in 30%, plateau 30-70%, fade-out 100%.
         Addendum 26: saturation 92% (antes 85-88%), lightness adaptativo. */
      const l = b.lightness;
      const grad = ctx.createLinearGradient(0, 0, 0, b.length);
      grad.addColorStop(0,    `hsla(${b.hue}, 92%, ${l}%, 0)`);
      grad.addColorStop(0.16, `hsla(${b.hue}, 92%, ${l}%, ${pulsedOpacity * 0.65})`);
      grad.addColorStop(0.5,  `hsla(${b.hue}, 95%, ${l + 3}%, ${pulsedOpacity})`);
      grad.addColorStop(0.84, `hsla(${b.hue}, 92%, ${l}%, ${pulsedOpacity * 0.65})`);
      grad.addColorStop(1,    `hsla(${b.hue}, 92%, ${l}%, 0)`);
      ctx.fillStyle = grad;
      ctx.fillRect(-b.width / 2, 0, b.width, b.length);
      ctx.restore();
    }

    ctx.restore();

    /* Capa sutil de partículas estáticas (puntitos chicos) si NO reduced-motion.
       Da textura "espacial" sin saturar — máximo 30 puntos. */
    if (!state.reduced) {
      const accentR = state.accentRGB[0], accentG = state.accentRGB[1], accentB = state.accentRGB[2];
      ctx.save();
      ctx.fillStyle = `rgba(${accentR}, ${accentG}, ${accentB}, 0.18)`;
      /* Usar las partículas del modo dots si están disponibles, o crear unas
         simples acá. Para mantener separación, regeneramos en seedBeams si
         queremos. Por simplicidad, dibujamos un pseudo-noise fijo por hash. */
      const STAR_COUNT = 28;
      for (let i = 0; i < STAR_COUNT; i++) {
        /* Determinismo por hash simple: posiciones estables entre frames
           (no flickerean) pero diferentes para cada i. */
        const x = ((i * 73 + 17) % 100) / 100 * state.w;
        const y = ((i * 137 + 53) % 100) / 100 * state.h;
        const r = 0.6 + ((i * 31) % 7) / 10;
        ctx.beginPath();
        ctx.arc(x, y, r, 0, Math.PI * 2);
        ctx.fill();
      }
      ctx.restore();
    }
  }

  /* ═══════════════════ DRAW DOTS (desktop original) ═══════════════════ */
  /* Physics constants for the vacuum + spring */
  const VACUUM_STRENGTH = 1.6;
  const SPRING_K        = 0.045;
  const DAMPING         = 0.80;
  const MIN_DIST_SQ     = 4 * 4;

  function drawDots() {
    const { w, h, spotlight, accentRGB, reduced } = state;
    ctx.clearRect(0, 0, w, h);

    if (state.mouseX < -1000) { state.mouseX = state.targetX; state.mouseY = state.targetY; }
    else {
      state.mouseX += (state.targetX - state.mouseX) * 0.22;
      state.mouseY += (state.targetY - state.mouseY) * 0.22;
    }

    const ar = accentRGB[0], ag = accentRGB[1], ab = accentRGB[2];
    const attractR = spotlight;
    const attractR2 = attractR * attractR;
    const mouseActive = state.targetX > -1000;

    for (let n = 0; n < state.dots.length; n++) {
      const d = state.dots[n];

      /* Anchor estático al viewport (Addendum 18 fix bottom-gap). */
      const ax = d.ax;
      const ay = d.ay;

      if (!reduced) {
        if (mouseActive) {
          const dx = state.mouseX - d.x;
          const dy = state.mouseY - d.y;
          const distSq = dx * dx + dy * dy;
          if (distSq < attractR2 && distSq > MIN_DIST_SQ) {
            const dist = Math.sqrt(distSq);
            const t = 1 - dist / attractR;
            const force = t * t * VACUUM_STRENGTH;
            d.vx += (dx / dist) * force;
            d.vy += (dy / dist) * force;
          }
        }
        d.vx += (ax - d.x) * SPRING_K;
        d.vy += (ay - d.y) * SPRING_K;
        d.vx *= DAMPING;
        d.vy *= DAMPING;
        d.x += d.vx;
        d.y += d.vy;
      } else {
        d.x = ax; d.y = ay;
      }

      const ddx = d.x - state.mouseX;
      const ddy = d.y - state.mouseY;
      const dd2 = ddx * ddx + ddy * ddy;
      let baseA = 0.09;
      let r = 1.1;
      let R = 244, G = 241, B = 234;
      if (!reduced && mouseActive && dd2 < attractR2) {
        const t = 1 - dd2 / attractR2;
        baseA = 0.10 + 0.65 * t;
        r = 1.1 + 2.4 * t;
        R = 244 + (ar - 244) * t;
        G = 241 + (ag - 241) * t;
        B = 234 + (ab - 234) * t;
      }
      ctx.beginPath();
      ctx.fillStyle = `rgba(${R | 0},${G | 0},${B | 0},${baseA})`;
      ctx.arc(d.x, d.y, r, 0, Math.PI * 2);
      ctx.fill();
    }

    for (const p of state.particles) {
      if (!reduced) {
        p.vx += (Math.random() - 0.5) * 0.008;
        p.vy += (Math.random() - 0.5) * 0.008;
        p.vx = Math.max(-0.35, Math.min(0.35, p.vx));
        p.vy = Math.max(-0.35, Math.min(0.35, p.vy));
        const dx = state.mouseX - p.x, dy = state.mouseY - p.y;
        const d2 = dx * dx + dy * dy;
        if (d2 < 200 * 200) {
          const f = (1 - d2 / (200 * 200)) * 0.06;
          p.vx += dx * f * 0.002;
          p.vy += dy * f * 0.002;
        }
        p.x += p.vx * p.layer;
        p.y += p.vy * p.layer;
        if (p.x < -10) p.x = w + 10;
        if (p.x > w + 10) p.x = -10;
        if (p.y < -10) p.y = h + 10;
        if (p.y > h + 10) p.y = -10;
      }
      ctx.beginPath();
      ctx.fillStyle = `rgba(244,241,234,${p.a})`;
      ctx.arc(p.x, p.y - state.scrollY * 0.08 * p.layer, p.r, 0, Math.PI * 2);
      ctx.fill();
    }

    const now = performance.now();
    state.ripples = state.ripples.filter((rp) => now - rp.t < rp.life);
    for (const rp of state.ripples) {
      const k = (now - rp.t) / rp.life;
      const ease = 1 - Math.pow(1 - k, 3);
      const radius = 20 + 320 * ease;
      const alpha = (1 - k) * 0.45;
      ctx.beginPath();
      ctx.strokeStyle = `rgba(${ar},${ag},${ab},${alpha})`;
      ctx.lineWidth = 1.2;
      ctx.arc(rp.x, rp.y, radius, 0, Math.PI * 2);
      ctx.stroke();
      ctx.beginPath();
      ctx.strokeStyle = `rgba(${ar},${ag},${ab},${alpha * 0.4})`;
      ctx.lineWidth = 0.6;
      ctx.arc(rp.x, rp.y, radius * 0.55, 0, Math.PI * 2);
      ctx.stroke();
    }
  }

  /* ═══════════════════ EVENTS ═══════════════════ */
  window.addEventListener('resize', resize, { passive: true });
  window.addEventListener('mousemove', (e) => {
    state.targetX = e.clientX;
    state.targetY = e.clientY;
  }, { passive: true });
  window.addEventListener('mouseleave', () => { state.targetX = -9999; state.targetY = -9999; });
  window.addEventListener('scroll', () => { state.scrollY = window.scrollY; }, { passive: true });
  window.addEventListener('click', (e) => {
    if (state.reduced || state.mode !== 'dots') return;
    state.ripples.push({ x: e.clientX, y: e.clientY, t: performance.now(), life: 800 });
  });

  /* Listener para el media query: switch de modo en vivo si el usuario
     redimensiona la ventana a través del breakpoint 1024 px. */
  function onModeChange() {
    state.mode = MOBILE_MQ.matches ? 'beams' : 'dots';
    resize();
  }
  if (MOBILE_MQ.addEventListener) {
    MOBILE_MQ.addEventListener('change', onModeChange);
  } else if (MOBILE_MQ.addListener) {
    MOBILE_MQ.addListener(onModeChange); // legacy Safari
  }

  /* ═══════════════════ RAF LOOP CON PAUSA EN HIDDEN ═══════════════════ */
  let rafId = 0;
  let isRunning = false;

  function loop() {
    if (state.mode === 'beams') drawBeams();
    else drawDots();
    rafId = requestAnimationFrame(loop);
  }
  function start() {
    if (isRunning) return;
    isRunning = true;
    rafId = requestAnimationFrame(loop);
  }
  function stop() {
    if (!isRunning) return;
    isRunning = false;
    cancelAnimationFrame(rafId);
  }
  document.addEventListener('visibilitychange', () => {
    if (document.hidden) stop();
    else start();
  });

  resize();
  start();
})();
