/* ========================================================================
   LUMINA — Visualizer engines (canvas2d).
   Each visualizer is { id, name, tag, icon(svg), draw(ctx, w, h, A, P, t) }
   A = audio state (bass, mid, high, energy, beat, freq, wave)
   P = params { intensity, smoothness, color, hue, density, motion, glow,
                bgAlpha, mirror, kaleidoSegs, particleCount, lineWidth }
   ======================================================================== */

(function () {
  const TAU = Math.PI * 2;

  // ---- color helpers ----
  function hsla(h, s = 80, l = 60, a = 1) {
    return `hsla(${h}, ${s}%, ${l}%, ${a})`;
  }
  function bgFill(ctx, w, h, A, P) {
    const a = P.bgAlpha ?? 0.18;
    ctx.fillStyle = `rgba(8, 8, 12, ${a})`;
    ctx.fillRect(0, 0, w, h);
  }
  function reset(ctx) {
    ctx.globalCompositeOperation = "source-over";
    ctx.shadowBlur = 0;
    ctx.lineCap = "round";
    ctx.lineJoin = "round";
  }

  // ============ 1. Spectrum Bars (classic) ============
  const Bars = {
    id: "bars", name: "Spectrum Bars", tag: "FFT · 64",
    icon: <svg viewBox="0 0 24 24"><g fill="currentColor">
      <rect x="2"  y="14" width="3" height="8" rx="1"/>
      <rect x="7"  y="9"  width="3" height="13" rx="1"/>
      <rect x="12" y="4"  width="3" height="18" rx="1"/>
      <rect x="17" y="11" width="3" height="11" rx="1"/>
    </g></svg>,
    draw(ctx, w, h, A, P, t) {
      reset(ctx); bgFill(ctx, w, h, A, P);
      const N = Math.max(16, Math.floor(P.density * 96));
      const gap = w / N * 0.18;
      const bw = w / N - gap;
      const baseH = h * 0.05;
      ctx.shadowBlur = P.glow * 30;
      for (let i = 0; i < N; i++) {
        const fi = Math.floor((i / N) * (A.freq.length * 0.6));
        const v = A.freq[fi] / 255;
        const bh = baseH + v * h * 0.85 * P.intensity;
        const x = i * (bw + gap);
        const y = h - bh;
        const hue = (P.hue + i * 2 + t * 20) % 360;
        const grd = ctx.createLinearGradient(0, y, 0, h);
        grd.addColorStop(0, hsla(hue, 90, 65));
        grd.addColorStop(1, hsla((hue + 60) % 360, 90, 35));
        ctx.fillStyle = grd;
        ctx.shadowColor = hsla(hue, 90, 60, 0.8);
        ctx.fillRect(x, y, bw, bh);
        // cap line
        ctx.fillStyle = hsla(hue, 90, 80, 0.9);
        ctx.fillRect(x, y - 2, bw, 2);
      }
      // mirror
      if (P.mirror) {
        ctx.save();
        ctx.globalAlpha = 0.25;
        ctx.scale(1, -1);
        ctx.translate(0, -h);
        ctx.drawImage(ctx.canvas, 0, 0);
        ctx.restore();
      }
    }
  };

  // ============ 2. Radial / Polar Spectrum ============
  const Radial = {
    id: "radial", name: "Radial Halo", tag: "POLAR",
    icon: <svg viewBox="0 0 24 24"><g fill="none" stroke="currentColor" strokeWidth="1.6">
      <circle cx="12" cy="12" r="3"/><circle cx="12" cy="12" r="7" strokeDasharray="2 2"/>
      <path d="M12 2v3M12 19v3M2 12h3M19 12h3M5 5l2 2M17 17l2 2M19 5l-2 2M5 19l2-2"/>
    </g></svg>,
    draw(ctx, w, h, A, P, t) {
      reset(ctx); bgFill(ctx, w, h, A, P);
      const cx = w/2, cy = h/2;
      const radius = Math.min(w, h) * 0.18 + A.bass * 40 * P.intensity;
      const N = Math.max(32, Math.floor(P.density * 180));
      ctx.shadowBlur = P.glow * 22;
      for (let i = 0; i < N; i++) {
        const a = (i / N) * TAU + t * 0.3 * P.motion;
        const fi = Math.floor((i / N) * (A.freq.length * 0.5));
        const v = A.freq[fi] / 255;
        const len = 8 + v * Math.min(w, h) * 0.35 * P.intensity;
        const hue = (P.hue + i * (360 / N) + t * 30) % 360;
        const x1 = cx + Math.cos(a) * radius;
        const y1 = cy + Math.sin(a) * radius;
        const x2 = cx + Math.cos(a) * (radius + len);
        const y2 = cy + Math.sin(a) * (radius + len);
        ctx.strokeStyle = hsla(hue, 90, 60);
        ctx.shadowColor = hsla(hue, 90, 60, 0.8);
        ctx.lineWidth = P.lineWidth;
        ctx.beginPath();
        ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke();
      }
      // inner pulsing disc
      const g = ctx.createRadialGradient(cx, cy, 0, cx, cy, radius);
      g.addColorStop(0, hsla(P.hue, 90, 70, 0.4 + A.bass * 0.4));
      g.addColorStop(1, hsla(P.hue, 90, 30, 0));
      ctx.fillStyle = g;
      ctx.beginPath(); ctx.arc(cx, cy, radius * 1.3, 0, TAU); ctx.fill();
    }
  };

  // ============ 3. Waveform (oscilloscope) ============
  const Wave = {
    id: "wave", name: "Oscilloscope", tag: "TIME",
    icon: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6">
      <path d="M2 12 Q5 4 8 12 T14 12 T20 12 T22 12"/></svg>,
    draw(ctx, w, h, A, P, t) {
      reset(ctx); bgFill(ctx, w, h, A, P);
      const wave = A.wave;
      const n = wave.length;
      const cy = h / 2;
      const amp = h * 0.38 * P.intensity;
      // glow pass
      for (let pass = 0; pass < 3; pass++) {
        ctx.beginPath();
        for (let i = 0; i < n; i++) {
          const x = (i / (n - 1)) * w;
          const y = cy + ((wave[i] - 128) / 128) * amp;
          if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
        }
        const hue = (P.hue + t * 30) % 360;
        ctx.strokeStyle = hsla(hue, 90, 65, [0.15, 0.35, 1][pass]);
        ctx.lineWidth = [P.lineWidth * 6, P.lineWidth * 3, P.lineWidth][pass];
        ctx.shadowBlur = [22 * P.glow, 10 * P.glow, 0][pass];
        ctx.shadowColor = hsla(hue, 90, 60);
        ctx.stroke();
      }
      // mirror below
      if (P.mirror) {
        ctx.save(); ctx.globalAlpha = 0.4; ctx.scale(1, -1); ctx.translate(0, -h);
        ctx.beginPath();
        for (let i = 0; i < n; i++) {
          const x = (i / (n - 1)) * w;
          const y = cy + ((wave[i] - 128) / 128) * amp;
          if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
        }
        ctx.strokeStyle = hsla((P.hue + 180) % 360, 90, 60); ctx.stroke();
        ctx.restore();
      }
    }
  };

  // ============ 4. Particle Field ============
  const particles = [];
  function ensureParticles(n, w, h) {
    while (particles.length < n) {
      particles.push({
        x: Math.random() * w, y: Math.random() * h,
        vx: (Math.random() - 0.5) * 0.6,
        vy: (Math.random() - 0.5) * 0.6,
        r: 0.5 + Math.random() * 2.5,
        s: Math.random(),
      });
    }
    while (particles.length > n) particles.pop();
  }
  const Particles = {
    id: "particles", name: "Particle Storm", tag: "WGL · 800",
    icon: <svg viewBox="0 0 24 24" fill="currentColor">
      <circle cx="5" cy="6" r="1.2"/><circle cx="10" cy="14" r="1.6"/>
      <circle cx="16" cy="6" r="1"/><circle cx="19" cy="16" r="1.4"/>
      <circle cx="13" cy="20" r="1.1"/><circle cx="7" cy="18" r="0.9"/>
      <circle cx="14" cy="10" r="1"/><circle cx="20" cy="9" r="0.9"/>
    </svg>,
    draw(ctx, w, h, A, P, t) {
      reset(ctx); bgFill(ctx, w, h, A, P);
      const N = Math.max(40, Math.floor(P.density * 800));
      ensureParticles(N, w, h);
      const cx = w/2, cy = h/2;
      ctx.globalCompositeOperation = "lighter";
      for (let i = 0; i < N; i++) {
        const p = particles[i];
        // pull toward center, push on beat
        const dx = cx - p.x, dy = cy - p.y;
        const d = Math.hypot(dx, dy) || 1;
        const pull = 0.0015 * P.motion;
        p.vx += (dx / d) * pull - (dx / d) * A.bass * 0.4 * P.intensity;
        p.vy += (dy / d) * pull - (dy / d) * A.bass * 0.4 * P.intensity;
        p.vx *= 0.96; p.vy *= 0.96;
        p.x += p.vx * (1 + A.energy); p.y += p.vy * (1 + A.energy);
        if (p.x < 0) p.x += w; else if (p.x > w) p.x -= w;
        if (p.y < 0) p.y += h; else if (p.y > h) p.y -= h;
        const hue = (P.hue + p.s * 80 + t * 30) % 360;
        const r = p.r * (1 + A.high * 2 * P.intensity);
        ctx.fillStyle = hsla(hue, 90, 60, 0.7);
        ctx.shadowBlur = P.glow * 12;
        ctx.shadowColor = hsla(hue, 90, 60);
        ctx.beginPath(); ctx.arc(p.x, p.y, r, 0, TAU); ctx.fill();
      }
    }
  };

  // ============ 5. Kaleidoscope ============
  const Kaleido = {
    id: "kaleido", name: "Kaleidoscope", tag: "MIRROR ×8",
    icon: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.4">
      <path d="M12 2L22 12L12 22L2 12Z"/><path d="M12 2v20M2 12h20M5 5l14 14M19 5L5 19"/>
    </svg>,
    draw(ctx, w, h, A, P, t) {
      reset(ctx); bgFill(ctx, w, h, A, P);
      const cx = w/2, cy = h/2;
      const segs = P.kaleidoSegs || 8;
      const r = Math.min(w, h) * 0.5;
      ctx.save();
      ctx.translate(cx, cy);
      for (let s = 0; s < segs; s++) {
        ctx.save();
        ctx.rotate((s / segs) * TAU);
        if (s % 2) ctx.scale(-1, 1);
        // clip wedge
        ctx.beginPath();
        ctx.moveTo(0, 0);
        ctx.arc(0, 0, r, -Math.PI / segs, Math.PI / segs);
        ctx.closePath();
        ctx.clip();
        // draw petals
        const N = Math.floor(P.density * 30) + 8;
        ctx.shadowBlur = P.glow * 18;
        for (let i = 0; i < N; i++) {
          const fi = Math.floor((i / N) * (A.freq.length * 0.5));
          const v = A.freq[fi] / 255;
          const ang = (i / N) * Math.PI / segs + t * 0.3 * P.motion;
          const dist = (i / N) * r * (0.6 + v * 0.6 * P.intensity);
          const sz = 4 + v * 60 * P.intensity;
          const hue = (P.hue + i * 8 + t * 40) % 360;
          ctx.fillStyle = hsla(hue, 90, 60, 0.7);
          ctx.shadowColor = hsla(hue, 90, 60);
          ctx.beginPath();
          ctx.arc(Math.cos(ang) * dist, Math.sin(ang) * dist, sz * 0.5, 0, TAU);
          ctx.fill();
        }
        ctx.restore();
      }
      ctx.restore();
    }
  };

  // ============ 6. Liquid Terrain (3D-feel ridge) ============
  const ridgeBuffer = [];
  const Terrain = {
    id: "terrain", name: "Liquid Ridge", tag: "FALLOFF",
    icon: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.4">
      <path d="M2 18 Q6 10 10 14 T18 12 T22 16"/>
      <path d="M2 14 Q6 8 10 11 T18 9 T22 13" opacity="0.6"/>
      <path d="M2 10 Q6 5 10 8 T18 6 T22 9" opacity="0.3"/>
    </svg>,
    draw(ctx, w, h, A, P, t) {
      reset(ctx); bgFill(ctx, w, h, A, P);
      const SLICES = 14;
      const N = 64;
      const row = new Array(N);
      for (let i = 0; i < N; i++) {
        const fi = Math.floor((i / N) * (A.freq.length * 0.55));
        row[i] = A.freq[fi] / 255;
      }
      ridgeBuffer.unshift(row);
      while (ridgeBuffer.length > SLICES) ridgeBuffer.pop();

      for (let s = ridgeBuffer.length - 1; s >= 0; s--) {
        const r = ridgeBuffer[s];
        const z = s / SLICES;
        const persp = 1 - z * 0.55;
        const yBase = h * (0.42 + z * 0.45);
        const xPad = w * 0.06 * (1 - persp);
        const ww = w - xPad * 2;
        const hue = (P.hue + s * 6) % 360;
        ctx.beginPath();
        for (let i = 0; i < N; i++) {
          const x = xPad + (i / (N - 1)) * ww;
          const amp = h * 0.32 * persp * P.intensity;
          const y = yBase - r[i] * amp;
          if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
        }
        ctx.lineTo(w - xPad, yBase); ctx.lineTo(xPad, yBase); ctx.closePath();
        const grd = ctx.createLinearGradient(0, yBase - 100, 0, yBase);
        grd.addColorStop(0, hsla(hue, 90, 60, 0.9 - z * 0.5));
        grd.addColorStop(1, hsla((hue + 80) % 360, 80, 25, 0));
        ctx.fillStyle = grd; ctx.fill();
        ctx.strokeStyle = hsla(hue, 90, 70, 0.9 - z * 0.5);
        ctx.lineWidth = P.lineWidth * 0.9;
        ctx.shadowBlur = P.glow * 12;
        ctx.shadowColor = hsla(hue, 90, 60);
        ctx.stroke();
      }
    }
  };

  // ============ 7. Tunnel / Vortex ============
  const Tunnel = {
    id: "tunnel", name: "Vortex Tunnel", tag: "DEPTH",
    icon: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.4">
      <rect x="2" y="2" width="20" height="20" rx="2"/>
      <rect x="5" y="5" width="14" height="14" rx="1.5"/>
      <rect x="8" y="8" width="8" height="8" rx="1"/>
      <rect x="10.5" y="10.5" width="3" height="3"/>
    </svg>,
    draw(ctx, w, h, A, P, t) {
      reset(ctx); bgFill(ctx, w, h, A, P);
      const cx = w/2, cy = h/2;
      const N = 28;
      const rot = t * 0.3 * P.motion;
      ctx.save(); ctx.translate(cx, cy);
      ctx.shadowBlur = P.glow * 14;
      for (let i = N - 1; i >= 0; i--) {
        const k = i / N;
        const scale = Math.pow(1.18, i) * 8;
        const beatPulse = 1 + A.bass * 0.25 * P.intensity;
        const size = scale * beatPulse;
        const rotI = rot * (1 - k) * 4 + k * Math.PI;
        const hue = (P.hue + i * 14 + t * 60) % 360;
        ctx.save();
        ctx.rotate(rotI);
        ctx.strokeStyle = hsla(hue, 90, 60, 0.9 - k * 0.4);
        ctx.shadowColor = hsla(hue, 90, 60);
        ctx.lineWidth = P.lineWidth * (1.5 + (1 - k) * 2);
        ctx.beginPath();
        ctx.rect(-size, -size, size * 2, size * 2);
        ctx.stroke();
        ctx.restore();
      }
      ctx.restore();
    }
  };

  // ============ 8. Frequency Liquid (flow field) ============
  const Liquid = {
    id: "liquid", name: "Plasma Flow", tag: "FIELD",
    icon: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.4">
      <path d="M2 8 Q8 2 14 8 T22 8 M2 16 Q8 22 14 16 T22 16"/>
      <path d="M2 12 Q8 6 14 12 T22 12" opacity="0.6"/>
    </svg>,
    draw(ctx, w, h, A, P, t) {
      reset(ctx); bgFill(ctx, w, h, A, P);
      ctx.globalCompositeOperation = "lighter";
      const N = Math.max(20, Math.floor(P.density * 70));
      for (let i = 0; i < N; i++) {
        const phase = (i / N) * TAU;
        const freq = 1 + (i % 4);
        const hue = (P.hue + i * 12 + t * 40) % 360;
        const amp = h * 0.18 * P.intensity * (0.5 + A.energy);
        ctx.beginPath();
        for (let x = 0; x <= w; x += 4) {
          const u = x / w;
          const y =
            h * 0.5 +
            Math.sin(u * TAU * freq + t * P.motion + phase) * amp +
            Math.sin(u * TAU * (freq + 2) - t * P.motion * 1.4 + phase * 1.3) * amp * 0.4 +
            (A.wave[(x * 4) % A.wave.length] - 128) * 0.3 * P.intensity;
          if (x === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
        }
        ctx.strokeStyle = hsla(hue, 90, 60, 0.4);
        ctx.shadowBlur = P.glow * 10;
        ctx.shadowColor = hsla(hue, 90, 60);
        ctx.lineWidth = P.lineWidth;
        ctx.stroke();
      }
    }
  };

  // ============ 9. Lissajous ============
  const Lissajous = {
    id: "lissajous", name: "Lissajous", tag: "X/Y",
    icon: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.4">
      <path d="M4 12 Q8 2 12 12 T20 12 Q16 22 12 12"/>
    </svg>,
    draw(ctx, w, h, A, P, t) {
      reset(ctx); bgFill(ctx, w, h, A, P);
      const cx = w/2, cy = h/2;
      const R = Math.min(w, h) * 0.4 * P.intensity;
      const a = 3 + Math.floor(A.bass * 4);
      const b = 2 + Math.floor(A.high * 4);
      const dlt = t * P.motion;
      ctx.shadowBlur = P.glow * 16;
      for (let pass = 0; pass < 2; pass++) {
        ctx.beginPath();
        for (let i = 0; i <= 600; i++) {
          const u = (i / 600) * TAU;
          const x = cx + Math.sin(a * u + dlt) * R;
          const y = cy + Math.sin(b * u) * R;
          if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
        }
        const hue = (P.hue + t * 30) % 360;
        ctx.strokeStyle = hsla(hue, 90, 65, pass ? 1 : 0.3);
        ctx.shadowColor = hsla(hue, 90, 60);
        ctx.lineWidth = pass ? P.lineWidth : P.lineWidth * 5;
        ctx.stroke();
      }
    }
  };

  // ============ 10. CRT Text / Spectrum Type ============
  const TypeViz = {
    id: "type", name: "CRT Type", tag: "TEXT",
    icon: <svg viewBox="0 0 24 24" fill="currentColor">
      <text x="3" y="17" fontSize="14" fontWeight="700" fontFamily="monospace">AV</text>
    </svg>,
    draw(ctx, w, h, A, P, t) {
      reset(ctx); bgFill(ctx, w, h, A, P);
      const word = (P.text || "LUMINA").toUpperCase();
      const cx = w/2, cy = h/2;
      const size = Math.min(w, h) * 0.22 * (1 + A.bass * 0.25 * P.intensity);
      ctx.font = `900 ${size}px "Space Grotesk", system-ui, sans-serif`;
      ctx.textAlign = "center";
      ctx.textBaseline = "middle";
      // RGB split offset by bass
      const off = 2 + A.bass * 18 * P.intensity;
      ctx.globalCompositeOperation = "screen";
      ctx.fillStyle = hsla((P.hue + 0) % 360, 100, 55, 0.9);
      ctx.fillText(word, cx - off, cy);
      ctx.fillStyle = hsla((P.hue + 120) % 360, 100, 55, 0.9);
      ctx.fillText(word, cx + off, cy);
      ctx.fillStyle = hsla((P.hue + 240) % 360, 100, 65, 0.9);
      ctx.fillText(word, cx, cy + off * 0.4);
      // bars under
      ctx.globalCompositeOperation = "source-over";
      const N = 32;
      for (let i = 0; i < N; i++) {
        const fi = Math.floor((i / N) * (A.freq.length * 0.4));
        const v = A.freq[fi] / 255;
        const bw = w / N;
        ctx.fillStyle = hsla(P.hue, 90, 60, 0.5);
        ctx.fillRect(i * bw + 1, h - v * h * 0.18, bw - 2, v * h * 0.18);
      }
      // scanlines overlay
      ctx.fillStyle = "rgba(0,0,0,0.18)";
      for (let y = 0; y < h; y += 3) ctx.fillRect(0, y, w, 1);
    }
  };

  window.VISUALIZERS = [Bars, Radial, Wave, Particles, Kaleido, Terrain, Tunnel, Liquid, Lissajous, TypeViz];
})();
