/* ========================================================================
   LUMINA — Landscape visualizers (Vaporwave, Living Terrain, Mountain Drive)
   ======================================================================== */
(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);
  };

  /* ============ VAPORWAVE GRID DRIVE ============ */
  const Vaporwave = {
    id: "vaporwave",
    name: "Vaporwave Drive",
    tag: "GRID · 1986",
    icon: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.4">
      <path d="M2 22 L9 12 L15 12 L22 22 M2 18 L22 18 M2 14 L22 14"/>
      <circle cx="12" cy="6" r="3" fill="currentColor" />
      <path d="M2 11 L22 11" opacity="0.5"/>
    </svg>,
    draw(ctx, w, h, A, P, t) {
      // Sky gradient
      const sky = ctx.createLinearGradient(0, 0, 0, h * 0.55);
      const hue = P.hue;
      sky.addColorStop(0, hsla(hue + 280, 80, 18));
      sky.addColorStop(0.5, hsla(hue + 320, 90, 30));
      sky.addColorStop(1, hsla(hue + 350, 95, 45));
      ctx.fillStyle = sky;
      ctx.fillRect(0, 0, w, h * 0.55);
      // Ground gradient
      const ground = ctx.createLinearGradient(0, h * 0.55, 0, h);
      ground.addColorStop(0, hsla(hue + 270, 80, 14));
      ground.addColorStop(1, hsla(hue + 280, 80, 5));
      ctx.fillStyle = ground;
      ctx.fillRect(0, h * 0.55, w, h * 0.45);

      // Sun
      const cx = w / 2, sy = h * 0.45;
      const sr = Math.min(w, h) * 0.18 * (1 + A.high * 0.2 * P.intensity);
      const sg = ctx.createRadialGradient(cx, sy, 0, cx, sy, sr);
      sg.addColorStop(0, hsla(hue, 100, 80, 1));
      sg.addColorStop(0.4, hsla(hue + 20, 100, 60, 0.9));
      sg.addColorStop(1, hsla(hue + 60, 100, 30, 0));
      ctx.fillStyle = sg;
      ctx.beginPath(); ctx.arc(cx, sy, sr, 0, TAU); ctx.fill();
      // Horizontal sun stripes
      ctx.save();
      ctx.globalCompositeOperation = "destination-out";
      for (let i = 0; i < 8; i++) {
        const sy2 = sy - sr * 0.5 + (i * sr * 0.15);
        ctx.fillStyle = "rgba(0,0,0,1)";
        ctx.fillRect(cx - sr, sy2, sr * 2, Math.max(1, sr * 0.03));
      }
      ctx.restore();
      // sun glow
      ctx.save();
      ctx.shadowColor = hsla(hue, 100, 70);
      ctx.shadowBlur = 60 * P.glow * (1 + A.bass * 0.5);
      ctx.fillStyle = "rgba(0,0,0,0)";
      ctx.beginPath(); ctx.arc(cx, sy, sr * 0.6, 0, TAU); ctx.fill();
      ctx.restore();

      // Mountains (procedural silhouettes)
      const mtnSeed = (k) => Math.sin(k * 12.9898) * 43758.5453 - Math.floor(Math.sin(k * 12.9898) * 43758.5453);
      const horizon = h * 0.55;
      const mtnLayers = [
        { z: 0.4, hue: hue + 320, count: 7 },
        { z: 0.7, hue: hue + 340, count: 5 },
      ];
      for (const L of mtnLayers) {
        ctx.beginPath();
        ctx.moveTo(0, horizon);
        const N = 64;
        for (let i = 0; i <= N; i++) {
          const x = (i / N) * w;
          let y = horizon;
          for (let k = 0; k < L.count; k++) {
            const peakX = w * mtnSeed(k * 7 + L.z * 13);
            const peakH = (40 + mtnSeed(k * 13 + L.z * 7) * 80) * (1 + A.bass * 0.5);
            const peakW = 80 + mtnSeed(k * 5 + L.z) * 200;
            const d = Math.abs(x - peakX);
            if (d < peakW) {
              y -= peakH * Math.cos(d / peakW * Math.PI * 0.5);
            }
          }
          ctx.lineTo(x, y);
        }
        ctx.lineTo(w, horizon); ctx.closePath();
        const mg = ctx.createLinearGradient(0, horizon - 100, 0, horizon);
        mg.addColorStop(0, hsla(L.hue, 80, 20));
        mg.addColorStop(1, hsla(L.hue, 80, 8));
        ctx.fillStyle = mg; ctx.fill();
        // outline (neon glow)
        ctx.strokeStyle = hsla(L.hue + 30, 100, 60, 0.7);
        ctx.lineWidth = 1.2;
        ctx.shadowColor = hsla(L.hue + 30, 100, 60);
        ctx.shadowBlur = 6 * P.glow;
        ctx.stroke();
      }
      ctx.shadowBlur = 0;

      // Palm trees (silhouettes left/right)
      const drawPalm = (px, py, scale, flip) => {
        ctx.save();
        ctx.translate(px, py); ctx.scale(flip ? -scale : scale, scale);
        ctx.fillStyle = "#000";
        // trunk
        ctx.beginPath();
        ctx.moveTo(-2, 0); ctx.bezierCurveTo(2, -60, -4, -80, 0, -120);
        ctx.lineTo(2, -120); ctx.bezierCurveTo(-2, -80, 4, -60, 4, 0);
        ctx.closePath(); ctx.fill();
        // fronds
        for (let f = 0; f < 7; f++) {
          const a = -Math.PI / 2 + (f - 3) * 0.45;
          ctx.save();
          ctx.translate(0, -120); ctx.rotate(a);
          ctx.beginPath();
          ctx.moveTo(0, 0);
          ctx.quadraticCurveTo(30, -10, 60, 5);
          ctx.quadraticCurveTo(40, -2, 0, 4);
          ctx.closePath(); ctx.fill();
          ctx.restore();
        }
        ctx.restore();
      };
      drawPalm(w * 0.08, horizon + 10, 0.9, false);
      drawPalm(w * 0.92, horizon + 14, 0.85, true);
      drawPalm(w * 0.18, horizon + 6, 0.55, false);
      drawPalm(w * 0.85, horizon + 6, 0.6, true);

      // Perspective grid (drives toward us)
      const speed = (1 + A.bass * 4 * P.intensity);
      const scroll = (t * speed * 0.5) % 1;
      ctx.strokeStyle = hsla(hue + 20, 100, 65, 0.9);
      ctx.shadowColor = hsla(hue + 20, 100, 60);
      ctx.shadowBlur = 8 * P.glow;
      ctx.lineWidth = 1.5;
      // Vertical lines (toward vanishing point)
      const vpX = w / 2, vpY = horizon;
      for (let i = -10; i <= 10; i++) {
        const x = vpX + i * (w * 0.5);
        ctx.beginPath();
        ctx.moveTo(x, h);
        ctx.lineTo(vpX, vpY);
        ctx.stroke();
      }
      // Horizontal lines (rows perspective)
      const N = 14;
      for (let i = 0; i < N; i++) {
        const u = (i + scroll) / N;
        const z = u * u; // perspective
        const y = horizon + (h - horizon) * z;
        if (y > h || y < horizon) continue;
        const lineAlpha = (1 - u * 0.5);
        ctx.strokeStyle = hsla(hue + 20, 100, 65, lineAlpha);
        ctx.beginPath();
        ctx.moveTo(0, y); ctx.lineTo(w, y);
        ctx.stroke();
      }
      ctx.shadowBlur = 0;

      // CRT scan lines
      ctx.fillStyle = "rgba(0,0,0,0.18)";
      for (let y = 0; y < h; y += 3) ctx.fillRect(0, y, w, 1);
    },
  };

  /* ============ LIVING TERRAIN (heightmap displacement) ============ */
  const terrainHistory = [];
  const Terrain3D = {
    id: "terrainLive",
    name: "Living Terrain",
    tag: "HEIGHTMAP",
    icon: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.4">
      <path d="M2 18 L7 10 L12 15 L17 8 L22 18" />
      <path d="M2 22 L22 22"/>
    </svg>,
    draw(ctx, w, h, A, P, t) {
      bg(ctx, w, h, P);
      // Sky
      const sky = ctx.createLinearGradient(0, 0, 0, h * 0.5);
      sky.addColorStop(0, hsla(P.hue + 230, 60, 8, 1));
      sky.addColorStop(1, hsla(P.hue + 220, 70, 18, 1));
      ctx.fillStyle = sky; ctx.fillRect(0, 0, w, h * 0.55);
      // Capture current freq into terrain history
      const N = 96;
      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;
      }
      terrainHistory.unshift(row);
      const ROWS = 28;
      while (terrainHistory.length > ROWS) terrainHistory.pop();

      // Draw terrain from back to front
      const horizon = h * 0.5;
      for (let r = ROWS - 1; r >= 0; r--) {
        const rr = terrainHistory[r];
        if (!rr) continue;
        const z = r / ROWS;
        const persp = 1 - z * 0.65;
        const y0 = horizon + (h - horizon) * (1 - z) * 0.95;
        const amp = h * 0.45 * persp * P.intensity;
        const xPad = w * 0.5 * (1 - persp);
        const ww = w - xPad * 2;

        // Build the path
        ctx.beginPath();
        for (let i = 0; i <= N; i++) {
          const x = xPad + (i / N) * ww;
          const v = rr[i] || 0;
          // add wavy noise
          const noise = Math.sin(i * 0.4 + t * 0.6 + r * 0.3) * 0.06 * P.motion;
          const y = y0 - (v + noise) * amp;
          if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
        }
        ctx.lineTo(w - xPad, y0);
        ctx.lineTo(xPad, y0);
        ctx.closePath();
        const hue = (P.hue + r * 4) % 360;
        const grd = ctx.createLinearGradient(0, y0 - amp, 0, y0);
        grd.addColorStop(0, hsla(hue, 90, 60, 0.85 - z * 0.55));
        grd.addColorStop(0.6, hsla(hue + 30, 80, 35, 0.7 - z * 0.55));
        grd.addColorStop(1, hsla(hue + 60, 80, 15, 0));
        ctx.fillStyle = grd; ctx.fill();
        // ridge stroke
        ctx.strokeStyle = hsla(hue, 100, 75, 0.95 - z * 0.6);
        ctx.lineWidth = P.lineWidth * (1.4 - z * 0.7);
        ctx.shadowColor = hsla(hue, 100, 65);
        ctx.shadowBlur = P.glow * 14 * (1 - z);
        ctx.stroke();
      }
      ctx.shadowBlur = 0;
      // ground reflection grid
      ctx.strokeStyle = hsla(P.hue, 80, 50, 0.2);
      for (let i = 0; i < 18; i++) {
        const z = i / 18;
        const y = horizon + (h - horizon) * z * z;
        if (y > h) continue;
        ctx.beginPath();
        ctx.moveTo(0, y); ctx.lineTo(w, y); ctx.stroke();
      }
    },
  };

  /* ============ MOUNTAIN DRIVE (first-person racing through peaks) ============ */
  const Mountains = {
    id: "mountainsDrive",
    name: "Mountain Drive",
    tag: "FIRST · PERSON",
    icon: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.4">
      <path d="M2 20 L8 6 L12 14 L16 4 L22 20"/>
      <circle cx="18" cy="6" r="2" fill="currentColor"/>
    </svg>,
    draw(ctx, w, h, A, P, t) {
      // Sky
      const sky = ctx.createLinearGradient(0, 0, 0, h * 0.6);
      sky.addColorStop(0, hsla(P.hue + 200, 50, 5));
      sky.addColorStop(0.7, hsla(P.hue + 280, 70, 22));
      sky.addColorStop(1, hsla(P.hue + 320, 85, 40));
      ctx.fillStyle = sky; ctx.fillRect(0, 0, w, h * 0.65);
      // Stars
      for (let i = 0; i < 80; i++) {
        const sx = (Math.sin(i * 13.37) * 0.5 + 0.5) * w;
        const sy = (Math.sin(i * 7.13) * 0.5 + 0.5) * h * 0.4;
        const sa = 0.4 + Math.sin(t * 2 + i) * 0.3;
        ctx.fillStyle = `rgba(255,255,255,${sa})`;
        ctx.fillRect(sx, sy, 1.5, 1.5);
      }
      // Moon
      const mx = w * 0.78, my = h * 0.18, mr = Math.min(w, h) * 0.07;
      const mg = ctx.createRadialGradient(mx, my, 0, mx, my, mr * 1.5);
      mg.addColorStop(0, "rgba(255,255,240,0.9)");
      mg.addColorStop(0.4, "rgba(255,255,240,0.6)");
      mg.addColorStop(1, "rgba(255,255,240,0)");
      ctx.fillStyle = mg;
      ctx.beginPath(); ctx.arc(mx, my, mr * 1.5, 0, TAU); ctx.fill();
      ctx.fillStyle = "rgba(255,255,240,1)";
      ctx.beginPath(); ctx.arc(mx, my, mr, 0, TAU); ctx.fill();

      // Mountains: 4 layers parallax scrolling
      const horizon = h * 0.65;
      const layers = [
        { z: 0.85, hue: P.hue + 240, amp: 60, freq: 0.004, speed: 0.02 },
        { z: 0.65, hue: P.hue + 270, amp: 100, freq: 0.006, speed: 0.04 },
        { z: 0.4,  hue: P.hue + 300, amp: 160, freq: 0.008, speed: 0.08 },
        { z: 0.15, hue: P.hue + 330, amp: 220, freq: 0.011, speed: 0.16 },
      ];
      for (const L of layers) {
        ctx.beginPath();
        ctx.moveTo(0, h);
        const N = 200;
        const off = t * L.speed * (1 + A.energy * 2 * P.motion) * 100;
        for (let i = 0; i <= N; i++) {
          const x = (i / N) * w;
          const u = (x + off) * L.freq;
          let y = horizon;
          y -= (Math.sin(u) * 0.5 + 0.5) * L.amp;
          y -= Math.sin(u * 3.1) * L.amp * 0.3;
          y -= Math.sin(u * 7.7 + 1.3) * L.amp * 0.15;
          y -= A.bass * 20 * P.intensity * Math.sin(u * 0.7);
          ctx.lineTo(x, y);
        }
        ctx.lineTo(w, h); ctx.closePath();
        const lg = ctx.createLinearGradient(0, horizon - L.amp, 0, h);
        lg.addColorStop(0, hsla(L.hue, 80, 50 * (1 - L.z * 0.6), 1));
        lg.addColorStop(1, hsla(L.hue + 30, 70, 8, 1));
        ctx.fillStyle = lg; ctx.fill();
        // neon top ridge
        ctx.strokeStyle = hsla(L.hue + 40, 100, 70, 0.95 - L.z * 0.5);
        ctx.lineWidth = 1.5 - L.z;
        ctx.shadowColor = hsla(L.hue + 40, 100, 60);
        ctx.shadowBlur = P.glow * 10 * (1 - L.z);
        ctx.stroke();
      }
      ctx.shadowBlur = 0;

      // Road (perspective)
      const cx = w / 2;
      const roadGrd = ctx.createLinearGradient(0, horizon, 0, h);
      roadGrd.addColorStop(0, "rgba(20,10,30,0.95)");
      roadGrd.addColorStop(1, "rgba(40,20,60,1)");
      ctx.fillStyle = roadGrd;
      ctx.beginPath();
      ctx.moveTo(cx, horizon);
      ctx.lineTo(cx - w * 0.5, h);
      ctx.lineTo(cx + w * 0.5, h);
      ctx.closePath(); ctx.fill();
      // Road lines (dashed)
      const speed = (1 + A.bass * 4 * P.intensity);
      const scroll = (t * speed * 0.7) % 1;
      ctx.strokeStyle = hsla(P.hue + 30, 100, 70, 0.9);
      ctx.shadowColor = hsla(P.hue + 30, 100, 70);
      ctx.shadowBlur = P.glow * 8;
      const segs = 18;
      for (let i = 0; i < segs; i++) {
        const u = (i + scroll) / segs;
        const z = u * u;
        const y0 = horizon + (h - horizon) * z;
        const y1 = horizon + (h - horizon) * Math.min(1, (z + 1 / segs * 0.6));
        const ww = (w * 0.5) * (z);
        ctx.lineWidth = Math.max(1, 5 * z);
        ctx.beginPath();
        ctx.moveTo(cx, y0); ctx.lineTo(cx, y1);
        ctx.stroke();
        // side lines
        ctx.lineWidth = Math.max(1, 3 * z);
        ctx.beginPath();
        ctx.moveTo(cx - ww, y0); ctx.lineTo(cx - ww * 1.05, y1);
        ctx.moveTo(cx + ww, y0); ctx.lineTo(cx + ww * 1.05, y1);
        ctx.stroke();
      }
      ctx.shadowBlur = 0;
      // car silhouette at bottom
      ctx.fillStyle = "rgba(0,0,0,0.85)";
      const cy = h - 30;
      ctx.beginPath();
      ctx.moveTo(cx - 50, h);
      ctx.bezierCurveTo(cx - 50, cy - 10, cx - 30, cy - 24, cx, cy - 24);
      ctx.bezierCurveTo(cx + 30, cy - 24, cx + 50, cy - 10, cx + 50, h);
      ctx.closePath(); ctx.fill();
      // car tail lights (audio reactive)
      ctx.fillStyle = `hsla(0, 100%, 55%, ${0.6 + A.bass * 0.4})`;
      ctx.shadowColor = "rgba(255,50,50,1)";
      ctx.shadowBlur = 20 * P.glow * (1 + A.bass);
      ctx.fillRect(cx - 36, cy - 18, 8, 4);
      ctx.fillRect(cx + 28, cy - 18, 8, 4);
      ctx.shadowBlur = 0;
    },
  };

  /* ============ CITY SKYLINE (audio-reactive buildings) ============ */
  const City = {
    id: "citySkyline",
    name: "Cyber Skyline",
    tag: "BUILDINGS",
    icon: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.4">
      <rect x="3" y="10" width="3" height="12"/>
      <rect x="7" y="6" width="4" height="16"/>
      <rect x="12" y="13" width="3" height="9"/>
      <rect x="16" y="8" width="4" height="14"/>
    </svg>,
    draw(ctx, w, h, A, P, t) {
      // Background gradient
      const bgGrd = ctx.createLinearGradient(0, 0, 0, h);
      bgGrd.addColorStop(0, hsla(P.hue + 250, 60, 6));
      bgGrd.addColorStop(0.7, hsla(P.hue + 310, 75, 22));
      bgGrd.addColorStop(1, hsla(P.hue + 350, 80, 32));
      ctx.fillStyle = bgGrd; ctx.fillRect(0, 0, w, h);

      // sun behind
      const cx = w / 2, sy = h * 0.6;
      const sr = Math.min(w, h) * 0.22;
      const sg = ctx.createRadialGradient(cx, sy, 0, cx, sy, sr);
      sg.addColorStop(0, hsla(P.hue + 15, 100, 70, 0.9));
      sg.addColorStop(1, hsla(P.hue + 50, 100, 50, 0));
      ctx.fillStyle = sg;
      ctx.beginPath(); ctx.arc(cx, sy, sr, 0, TAU); ctx.fill();

      // Buildings — use freq bins as heights
      const N = Math.max(32, Math.floor(P.density * 96));
      const baseY = h * 0.78;
      ctx.shadowBlur = P.glow * 14;
      for (let i = 0; i < N; i++) {
        const fi = Math.floor((i / N) * (A.freq.length * 0.55));
        const v = A.freq[fi] / 255;
        const bw = w / N;
        const x = i * bw;
        const baseH = 60 + (Math.sin(i * 0.7) * 0.5 + 0.5) * 200;
        const bh = baseH + v * h * 0.35 * P.intensity;
        const y = baseY - bh;
        const hue = (P.hue + i * 4) % 360;
        // building
        const bg = ctx.createLinearGradient(0, y, 0, baseY);
        bg.addColorStop(0, hsla(hue, 60, 8));
        bg.addColorStop(1, hsla(hue, 80, 4));
        ctx.fillStyle = bg;
        ctx.fillRect(x, y, bw - 1, bh);
        // top neon stripe
        ctx.fillStyle = hsla(hue + 30, 100, 65, 0.9);
        ctx.shadowColor = hsla(hue + 30, 100, 55);
        ctx.fillRect(x, y, bw - 1, 2);
        // windows (random lit)
        for (let wy = y + 8; wy < baseY - 4; wy += 7) {
          for (let wx = x + 2; wx < x + bw - 4; wx += 4) {
            const lit = ((wx * wy + Math.floor(t * (1 + v * 2))) % 17) < 4;
            if (lit) {
              ctx.fillStyle = hsla(hue + 60, 100, 80, 0.5 + v * 0.4);
              ctx.fillRect(wx, wy, 2, 3);
            }
          }
        }
      }
      ctx.shadowBlur = 0;

      // Ground / reflection
      const ground = ctx.createLinearGradient(0, baseY, 0, h);
      ground.addColorStop(0, hsla(P.hue + 320, 60, 15));
      ground.addColorStop(1, hsla(P.hue + 280, 60, 3));
      ctx.fillStyle = ground;
      ctx.fillRect(0, baseY, w, h - baseY);
      // grid lines on ground
      ctx.strokeStyle = hsla(P.hue + 30, 100, 60, 0.6);
      ctx.shadowColor = hsla(P.hue + 30, 100, 60);
      ctx.shadowBlur = 6 * P.glow;
      for (let i = -10; i <= 10; i++) {
        const lx = w / 2 + i * (w * 0.08);
        ctx.beginPath();
        ctx.moveTo(lx, baseY); ctx.lineTo(lx + i * 60, h);
        ctx.stroke();
      }
      const scroll = (t * 0.5 * (1 + A.bass * 2)) % 1;
      for (let i = 0; i < 10; i++) {
        const u = (i + scroll) / 10;
        const y = baseY + (h - baseY) * u * u;
        if (y > h) continue;
        ctx.globalAlpha = 1 - u * 0.4;
        ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(w, y); ctx.stroke();
      }
      ctx.globalAlpha = 1;
      ctx.shadowBlur = 0;
    },
  };

  window.VISUALIZERS.push(Vaporwave, Terrain3D, Mountains, City);
})();
