/* ========================================================================
   LUMINA — p5.js-inspired visualizers, batch 2
   ======================================================================== */
(function () {
  const TAU = Math.PI * 2;
  const hsla = (h, s = 80, l = 60, a = 1) => `hsla(${h}, ${s}%, ${l}%, ${a})`;
  const bg = (ctx, w, h, P) => {
    ctx.fillStyle = `rgba(8,8,12,${P.bgAlpha ?? 0.18})`;
    ctx.fillRect(0, 0, w, h);
  };

  /* ============ PHYLLOTAXIS (sunflower spiral) ============ */
  const Phyllotaxis = {
    id: "phyllotaxis", name: "Phyllotaxis", tag: "GOLDEN",
    icon: <svg viewBox="0 0 24 24" fill="currentColor">
      <circle cx="12" cy="6" r="1.5"/><circle cx="16" cy="10" r="1.5"/>
      <circle cx="14" cy="15" r="1.5"/><circle cx="9" cy="15" r="1.5"/>
      <circle cx="7" cy="10" r="1.5"/><circle cx="12" cy="12" r="1.5"/>
    </svg>,
    draw(ctx, w, h, A, P, t) {
      bg(ctx, w, h, P);
      const cx = w/2, cy = h/2;
      const N = Math.max(200, Math.floor(P.density * 900));
      const c = (Math.min(w, h) / 28) * (1 + A.bass * 0.3 * P.intensity);
      const goldAngle = 137.5 * Math.PI / 180;
      ctx.shadowBlur = P.glow * 8;
      for (let i = 0; i < N; i++) {
        const a = i * goldAngle + t * 0.3 * P.motion;
        const r = c * Math.sqrt(i);
        const x = cx + r * Math.cos(a);
        const y = cy + r * Math.sin(a);
        const fi = Math.floor((i / N) * A.freq.length * 0.5);
        const v = A.freq[fi] / 255;
        const hue = (P.hue + i * 1.2 + t * 30) % 360;
        const sz = 2 + (i / N) * 5 + v * 6 * P.intensity;
        ctx.fillStyle = hsla(hue, 100, 55 + v * 30, 0.85);
        ctx.shadowColor = hsla(hue, 100, 60);
        ctx.beginPath(); ctx.arc(x, y, sz, 0, TAU); ctx.fill();
      }
      ctx.shadowBlur = 0;
    },
  };

  /* ============ METABALLS ============ */
  const Metaballs = {
    id: "metaballs", name: "Metaballs", tag: "BLOB",
    icon: <svg viewBox="0 0 24 24" fill="currentColor">
      <circle cx="9" cy="10" r="5"/><circle cx="16" cy="14" r="4"/>
    </svg>,
    draw(ctx, w, h, A, P, t) {
      bg(ctx, w, h, P);
      const N = 7;
      if (!Metaballs._b) {
        Metaballs._b = Array.from({ length: N }, (_, i) => ({
          x: Math.random(), y: Math.random(),
          vx: (Math.random() - 0.5) * 0.004,
          vy: (Math.random() - 0.5) * 0.004,
          r: 0.06 + Math.random() * 0.08,
        }));
      }
      const balls = Metaballs._b;
      for (const b of balls) {
        b.x += b.vx * (1 + A.energy * 3); b.y += b.vy * (1 + A.energy * 3);
        if (b.x < 0 || b.x > 1) b.vx *= -1;
        if (b.y < 0 || b.y > 1) b.vy *= -1;
      }
      // low-res field
      const step = 8;
      const rad = (1 + A.bass * 0.6 * P.intensity);
      for (let y = 0; y < h; y += step) {
        for (let x = 0; x < w; x += step) {
          let sum = 0;
          for (const b of balls) {
            const dx = b.x * w - x, dy = b.y * h - y;
            sum += (b.r * rad * Math.min(w, h)) ** 2 / (dx * dx + dy * dy + 1);
          }
          if (sum > 0.8) {
            const hue = (P.hue + sum * 30 + t * 30) % 360;
            ctx.fillStyle = hsla(hue, 100, Math.min(70, 30 + sum * 20), 0.9);
            ctx.fillRect(x, y, step + 1, step + 1);
          }
        }
      }
    },
  };

  /* ============ TRUCHET TILES ============ */
  const Truchet = {
    id: "truchet", name: "Truchet Tiles", tag: "MAZE",
    icon: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6">
      <path d="M2 12a10 10 0 0 1 10-10M22 12a10 10 0 0 1-10 10"/>
    </svg>,
    draw(ctx, w, h, A, P, t) {
      bg(ctx, w, h, P);
      const sz = 48;
      ctx.lineWidth = P.lineWidth + 1 + A.bass * 3 * P.intensity;
      ctx.shadowBlur = P.glow * 8;
      for (let y = 0, ry = 0; ry < h + sz; y++, ry += sz) {
        for (let x = 0, rx = 0; rx < w + sz; x++, rx += sz) {
          const flip = (Math.sin(x * 12.9 + y * 78.2 + Math.floor(t * 1.5)) > 0);
          const hue = (P.hue + (x + y) * 12 + t * 40) % 360;
          ctx.strokeStyle = hsla(hue, 100, 60, 0.9);
          ctx.shadowColor = hsla(hue, 100, 60);
          ctx.beginPath();
          if (flip) {
            ctx.arc(rx, ry, sz/2, 0, Math.PI/2);
            ctx.arc(rx + sz, ry + sz, sz/2, Math.PI, Math.PI*1.5);
          } else {
            ctx.arc(rx + sz, ry, sz/2, Math.PI/2, Math.PI);
            ctx.arc(rx, ry + sz, sz/2, Math.PI*1.5, TAU);
          }
          ctx.stroke();
        }
      }
      ctx.shadowBlur = 0;
    },
  };

  /* ============ WAVE INTERFERENCE ============ */
  const Interference = {
    id: "interference", name: "Wave Interference", tag: "RIPPLE",
    icon: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.4">
      <circle cx="8" cy="12" r="6"/><circle cx="16" cy="12" r="6"/>
    </svg>,
    draw(ctx, w, h, A, P, t) {
      bg(ctx, w, h, P);
      const sources = [
        { x: w * 0.35, y: h * 0.5 },
        { x: w * 0.65, y: h * 0.5 },
        { x: w * 0.5, y: h * 0.3 + Math.sin(t) * 50 },
      ];
      const step = 8;
      const wl = 30 - A.high * 15;
      for (let y = 0; y < h; y += step) {
        for (let x = 0; x < w; x += step) {
          let sum = 0;
          for (const s of sources) {
            const d = Math.hypot(s.x - x, s.y - y);
            sum += Math.sin(d / wl - t * 4 * P.motion);
          }
          const v = (sum / sources.length + 1) / 2;
          if (v > 0.55) {
            const hue = (P.hue + v * 80 + t * 30) % 360;
            ctx.fillStyle = hsla(hue, 100, 30 + v * 50, 0.85);
            ctx.fillRect(x, y, step + 1, step + 1);
          }
        }
      }
    },
  };

  /* ============ GAME OF LIFE (audio reseeds) ============ */
  const GameOfLife = {
    id: "gameOfLife", name: "Game of Life", tag: "CELLULAR",
    icon: <svg viewBox="0 0 24 24" fill="currentColor">
      <rect x="4" y="4" width="4" height="4"/><rect x="10" y="4" width="4" height="4"/>
      <rect x="10" y="10" width="4" height="4"/><rect x="16" y="10" width="4" height="4"/>
      <rect x="4" y="16" width="4" height="4"/>
    </svg>,
    draw(ctx, w, h, A, P, t) {
      const cell = 10;
      const cols = Math.floor(w / cell), rows = Math.floor(h / cell);
      if (!GameOfLife._g || GameOfLife._cols !== cols || GameOfLife._rows !== rows) {
        GameOfLife._cols = cols; GameOfLife._rows = rows;
        GameOfLife._g = Array.from({ length: rows }, () =>
          Array.from({ length: cols }, () => Math.random() > 0.7 ? 1 : 0));
        GameOfLife._t = 0;
      }
      let g = GameOfLife._g;
      // step rate tied to time
      const now = performance.now();
      if (now - (GameOfLife._last || 0) > 90) {
        GameOfLife._last = now;
        const ng = g.map(r => r.slice());
        for (let y = 0; y < rows; y++) {
          for (let x = 0; x < cols; x++) {
            let n = 0;
            for (let dy = -1; dy <= 1; dy++)
              for (let dx = -1; dx <= 1; dx++) {
                if (dx === 0 && dy === 0) continue;
                const yy = (y + dy + rows) % rows, xx = (x + dx + cols) % cols;
                n += g[yy][xx];
              }
            ng[y][x] = g[y][x] ? (n === 2 || n === 3 ? 1 : 0) : (n === 3 ? 1 : 0);
          }
        }
        // reseed on beat
        if (A.beat > 0.5) {
          for (let i = 0; i < 40; i++) {
            ng[Math.floor(Math.random() * rows)][Math.floor(Math.random() * cols)] = 1;
          }
        }
        GameOfLife._g = ng; g = ng;
      }
      bg(ctx, w, h, P);
      ctx.shadowBlur = P.glow * 6;
      for (let y = 0; y < rows; y++) {
        for (let x = 0; x < cols; x++) {
          if (g[y][x]) {
            const hue = (P.hue + (x + y) * 3 + t * 20) % 360;
            ctx.fillStyle = hsla(hue, 100, 60, 0.9);
            ctx.shadowColor = hsla(hue, 100, 60);
            ctx.fillRect(x * cell + 1, y * cell + 1, cell - 2, cell - 2);
          }
        }
      }
      ctx.shadowBlur = 0;
    },
  };

  /* ============ SUPERSHAPE (parametric morphing) ============ */
  const Supershape = {
    id: "supershape", name: "Supershape", tag: "MORPH",
    icon: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.4">
      <path d="M12 2 C18 2 22 8 22 12 C22 18 16 22 12 22 C6 22 2 16 2 12 C2 6 6 2 12 2"/>
    </svg>,
    draw(ctx, w, h, A, P, t) {
      bg(ctx, w, h, P);
      const cx = w/2, cy = h/2;
      const R = Math.min(w, h) * 0.36;
      const m = 4 + Math.floor((Math.sin(t * 0.3) * 0.5 + 0.5) * 12 + A.bass * 6);
      const n1 = 0.3 + A.mid * 2;
      const n2 = 0.3 + Math.sin(t * 0.5) * 0.3 + 1;
      const n3 = n2;
      function ss(angle) {
        const t1 = Math.abs(Math.cos(m * angle / 4)) ** n2;
        const t2 = Math.abs(Math.sin(m * angle / 4)) ** n3;
        return (t1 + t2) ** (-1 / n1);
      }
      ctx.shadowBlur = P.glow * 20;
      for (let layer = 0; layer < 3; layer++) {
        ctx.beginPath();
        for (let i = 0; i <= 360; i++) {
          const a = (i / 360) * TAU;
          const r = ss(a) * R * (1 - layer * 0.2) * (1 + A.energy * 0.2);
          const x = cx + Math.cos(a + t * 0.2 * P.motion) * r;
          const y = cy + Math.sin(a + t * 0.2 * P.motion) * r;
          if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
        }
        const hue = (P.hue + layer * 40 + t * 40) % 360;
        ctx.strokeStyle = hsla(hue, 100, 60, 1 - layer * 0.25);
        ctx.shadowColor = hsla(hue, 100, 60);
        ctx.lineWidth = P.lineWidth + 1;
        ctx.stroke();
      }
      ctx.shadowBlur = 0;
    },
  };

  /* ============ TYPOGRAPHY WAVE ============ */
  const TypeWave = {
    id: "typeWave", name: "Type Wave", tag: "KINETIC",
    icon: <svg viewBox="0 0 24 24" fill="currentColor"><text x="2" y="17" fontSize="15" fontWeight="800" fontFamily="sans-serif">W~</text></svg>,
    draw(ctx, w, h, A, P, t) {
      bg(ctx, w, h, P);
      const word = (P.text || "LUMINA").toUpperCase();
      const rows = 7;
      ctx.textAlign = "center"; ctx.textBaseline = "middle";
      for (let r = 0; r < rows; r++) {
        const y = (r + 0.5) / rows * h;
        const size = h / rows * 0.85;
        ctx.font = `900 ${size}px "Space Grotesk", sans-serif`;
        const off = Math.sin(t * 2 + r * 0.6) * 40 * (1 + A.bass);
        const hue = (P.hue + r * 30 + t * 40) % 360;
        ctx.fillStyle = hsla(hue, 100, 60, 0.85);
        ctx.shadowColor = hsla(hue, 100, 60);
        ctx.shadowBlur = P.glow * 12;
        // repeat word across width
        const reps = Math.ceil(w / (size * word.length * 0.65)) + 2;
        for (let i = -1; i < reps; i++) {
          const x = (i * size * word.length * 0.65 + (t * 30 * (r % 2 ? 1 : -1))) % (w + size * 4);
          ctx.fillText(word, x + off, y);
        }
      }
      ctx.shadowBlur = 0;
    },
  };

  /* ============ PARTICLE GALAXY (spiral arms) ============ */
  const Galaxy = {
    id: "galaxy", name: "Galaxy Spiral", tag: "COSMOS",
    icon: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.4">
      <path d="M12 12 Q18 6 20 12 Q16 18 12 12 Q6 18 4 12 Q8 6 12 12"/>
    </svg>,
    draw(ctx, w, h, A, P, t) {
      ctx.fillStyle = `rgba(4,4,10,${0.1 + (1 - P.bgAlpha) * 0.05})`;
      ctx.fillRect(0, 0, w, h);
      const cx = w/2, cy = h/2;
      const arms = 4;
      const N = Math.max(300, Math.floor(P.density * 1400));
      ctx.globalCompositeOperation = "lighter";
      for (let i = 0; i < N; i++) {
        const arm = i % arms;
        const dist = (i / N);
        const baseAngle = (arm / arms) * TAU + dist * 6 + t * 0.2 * P.motion;
        const r = dist * Math.min(w, h) * 0.5 * (1 + A.bass * 0.2);
        const jitter = (Math.sin(i * 12.9) * 0.5) * (1 - dist) * 40;
        const x = cx + Math.cos(baseAngle) * r + jitter;
        const y = cy + Math.sin(baseAngle) * r + jitter;
        const hue = (P.hue + dist * 120 + arm * 30 + t * 20) % 360;
        const sz = (1 - dist) * 2.5 + 0.5;
        ctx.fillStyle = hsla(hue, 100, 65, (1 - dist) * 0.9);
        ctx.fillRect(x, y, sz, sz);
      }
      // core
      const grd = ctx.createRadialGradient(cx, cy, 0, cx, cy, 80 * (1 + A.bass));
      grd.addColorStop(0, hsla(P.hue, 100, 90, 0.9));
      grd.addColorStop(1, hsla(P.hue, 100, 60, 0));
      ctx.fillStyle = grd;
      ctx.beginPath(); ctx.arc(cx, cy, 80 * (1 + A.bass), 0, TAU); ctx.fill();
      ctx.globalCompositeOperation = "source-over";
    },
  };

  /* ============ MOIRE PATTERN ============ */
  const Moire = {
    id: "moire", name: "Moiré", tag: "OPTICAL",
    icon: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1">
      <circle cx="10" cy="12" r="8"/><circle cx="14" cy="12" r="8"/>
    </svg>,
    draw(ctx, w, h, A, P, t) {
      bg(ctx, w, h, P);
      const draw1 = (ox, oy, hue) => {
        ctx.strokeStyle = hsla(hue, 100, 60, 0.5);
        ctx.lineWidth = 1.5;
        for (let r = 10; r < Math.max(w, h); r += 14) {
          ctx.beginPath(); ctx.arc(ox, oy, r, 0, TAU); ctx.stroke();
        }
      };
      const sep = 40 + Math.sin(t * P.motion) * 30 + A.bass * 60 * P.intensity;
      ctx.shadowBlur = P.glow * 6;
      draw1(w/2 - sep, h/2, (P.hue) % 360);
      draw1(w/2 + sep, h/2, (P.hue + 120) % 360);
      ctx.shadowBlur = 0;
    },
  };

  /* ============ DELAUNAY-style low-poly ============ */
  const LowPoly = {
    id: "lowPoly", name: "Low Poly", tag: "FACETS",
    icon: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.2">
      <path d="M2 6L10 2L14 9L22 5M2 6L6 14L10 2M6 14L14 9M6 14L12 22L14 9M14 9L22 5L20 16L12 22"/>
    </svg>,
    draw(ctx, w, h, A, P, t) {
      bg(ctx, w, h, P);
      const cols = 10, rows = 7;
      if (!LowPoly._pts) {
        LowPoly._pts = [];
        for (let y = 0; y <= rows; y++)
          for (let x = 0; x <= cols; x++)
            LowPoly._pts.push({ bx: x / cols, by: y / rows, ph: Math.random() * TAU });
      }
      const pts = LowPoly._pts;
      const pos = pts.map(p => ({
        x: (p.bx + Math.sin(t + p.ph) * 0.02 * (1 + A.energy)) * w,
        y: (p.by + Math.cos(t + p.ph) * 0.02 * (1 + A.energy)) * h,
      }));
      const idx = (x, y) => y * (cols + 1) + x;
      for (let y = 0; y < rows; y++) {
        for (let x = 0; x < cols; x++) {
          const a = pos[idx(x, y)], b = pos[idx(x+1, y)];
          const c = pos[idx(x, y+1)], d = pos[idx(x+1, y+1)];
          const baseHue = (P.hue + (x + y) * 15 + t * 30);
          [[a, b, c], [b, d, c]].forEach((tri, k) => {
            ctx.beginPath();
            ctx.moveTo(tri[0].x, tri[0].y);
            ctx.lineTo(tri[1].x, tri[1].y);
            ctx.lineTo(tri[2].x, tri[2].y);
            ctx.closePath();
            const hue = (baseHue + k * 10) % 360;
            const lum = 25 + ((x * 7 + y * 13 + k * 5) % 30) + A.bass * 20;
            ctx.fillStyle = hsla(hue, 80, lum, 0.95);
            ctx.fill();
          });
        }
      }
    },
  };

  window.VISUALIZERS.push(Phyllotaxis, Metaballs, Truchet, Interference,
                          GameOfLife, Supershape, TypeWave, Galaxy, Moire, LowPoly);
})();
