/* ========================================================================
   LUMINA — Generative AI workspace (local Stable Diffusion / FLUX bridge)
   Connects to local SD/FLUX endpoint OR uses procedural placeholder.
   ALL bundled outputs are procedural — fully commercial-safe.
   ======================================================================== */
const { useState: useStateG, useEffect: useEffectG, useRef: useRefG } = React;

const STYLE_PRESETS = [
  { id: "vapor",   label: "Vaporwave",  prompt: "vaporwave aesthetic, neon pink and cyan, palm trees, sunset, retro 80s grid",
    palette: ["#ff5ad6", "#4be6ff", "#ffae3a"] },
  { id: "cyber",   label: "Cyberpunk",  prompt: "cyberpunk city skyline at night, neon signs, rain reflections, blade runner",
    palette: ["#ff3b3b", "#4be6ff", "#b785ff"] },
  { id: "abstract",label: "Abstract",   prompt: "abstract flowing liquid metal, iridescent, fluid simulation, octane render",
    palette: ["#ffae3a", "#b785ff", "#4be6ff"] },
  { id: "nature",  label: "Nature",     prompt: "lush rainforest canopy, sunbeams, cinematic, vibrant green and gold",
    palette: ["#7cff4f", "#ffae3a", "#2dd400"] },
  { id: "space",   label: "Cosmos",     prompt: "nebula in deep space, swirling galaxy, stardust, vivid colors",
    palette: ["#b785ff", "#4be6ff", "#ff5ad6"] },
  { id: "geo",     label: "Geometric",  prompt: "isometric geometric shapes, pastel gradients, swiss design",
    palette: ["#ff5ad6", "#7cff4f", "#ffae3a"] },
  { id: "noir",    label: "Film Noir",  prompt: "black and white film noir, dramatic shadows, rain, mystery",
    palette: ["#ffffff", "#9b9a8c", "#1a1a1e"] },
  { id: "anime",   label: "Anime",      prompt: "anime illustration, soft lighting, studio ghibli style, vibrant",
    palette: ["#ff5ad6", "#4be6ff", "#ffae3a"] },
];

const MODELS = [
  { id: "flux-schnell",  label: "FLUX.1-schnell", licence: "Apache 2.0 · commercial ✓", steps: 4 },
  { id: "sdxl-turbo",    label: "SDXL Turbo",     licence: "OpenRAIL · commercial ✓ (restrictions)", steps: 4 },
  { id: "sd-15",         label: "SD 1.5",         licence: "CreativeML Open RAIL-M · commercial ✓", steps: 25 },
  { id: "sd-turbo",      label: "SD Turbo",       licence: "SAI · research only", steps: 1 },
];

const ASPECT_PRESETS = [
  { id: "16x9",  label: "16:9",   w: 1024, h: 576 },
  { id: "9x16",  label: "9:16",   w: 576, h: 1024 },
  { id: "1x1",   label: "1:1",    w: 768, h: 768 },
  { id: "21x9",  label: "21:9",   w: 1280, h: 540 },
  { id: "4x3",   label: "4:3",    w: 1024, h: 768 },
  { id: "3x4",   label: "3:4",    w: 768, h: 1024 },
];

let _gid = 0;

function GenerativePanel() {
  const [prompt, setPrompt] = useStateG(STYLE_PRESETS[0].prompt);
  const [neg, setNeg] = useStateG("blurry, lowres, watermark, text");
  const [model, setModel] = useStateG("flux-schnell");
  const [aspect, setAspect] = useStateG("16x9");
  const [steps, setSteps] = useStateG(4);
  const [cfg, setCfg] = useStateG(7);
  const [seed, setSeed] = useStateG(Math.floor(Math.random() * 99999));
  const [palette, setPalette] = useStateG(STYLE_PRESETS[0].palette);
  const [endpoint, setEndpoint] = useStateG("http://127.0.0.1:7860");
  const [connected, setConnected] = useStateG(false);
  const [busy, setBusy] = useStateG(false);
  const [progress, setProgress] = useStateG(0);
  const [results, setResults] = useStateG([]);
  const [selected, setSelected] = useStateG(null);
  const [sampler, setSampler] = useStateG("Euler a");
  const [batch, setBatch] = useStateG(1);
  const [realtime, setRealtime] = useStateG(false);
  const [rtStyle, setRtStyle] = useStateG("flow");
  const rtCanvasRef = useRefG(null);

  const aspectDef = ASPECT_PRESETS.find(a => a.id === aspect);
  const modelDef = MODELS.find(m => m.id === model);

  const tryConnect = async () => {
    setBusy(true);
    setProgress(0);
    // try to ping local SD API (Automatic1111 / ComfyUI)
    try {
      const ctl = new AbortController();
      const tm = setTimeout(() => ctl.abort(), 1500);
      const r = await fetch(endpoint + "/sdapi/v1/sd-models", { signal: ctl.signal });
      clearTimeout(tm);
      setConnected(r.ok);
    } catch (e) { setConnected(false); }
    setBusy(false);
  };

  const pickPreset = (p) => {
    setPrompt(p.prompt);
    setPalette(p.palette);
  };

  const generate = async () => {
    setBusy(true);
    setProgress(0);

    // If connected to a local SD endpoint, do a REAL txt2img call
    if (connected) {
      try {
        const [w, h] = [aspectDef.w, aspectDef.h];
        // progress poller
        let polling = true;
        const poll = async () => {
          while (polling) {
            try {
              const pr = await fetch(endpoint + "/sdapi/v1/progress");
              const pj = await pr.json();
              if (pj && typeof pj.progress === "number") setProgress(pj.progress);
            } catch (e) {}
            await new Promise(r => setTimeout(r, 400));
          }
        };
        poll();
        const res = await fetch(endpoint + "/sdapi/v1/txt2img", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            prompt, negative_prompt: neg,
            steps, cfg_scale: cfg, seed,
            width: w, height: h,
            sampler_name: sampler,
            batch_size: batch,
            n_iter: 1,
          }),
        });
        polling = false;
        const data = await res.json();
        if (data.images && data.images.length) {
          data.images.forEach((b64, i) => {
            const url = "data:image/png;base64," + b64;
            const im = new Image();
            im.onload = () => {
              const out = {
                id: ++_gid, url, prompt, palette, model, aspect,
                seed: seed + i, w, h, thumb: url,
                elapsed: "live", img: im, real: true,
              };
              setResults(r => [out, ...r].slice(0, 24));
              if (i === 0) setSelected(out);
            };
            im.src = url;
          });
          setBusy(false); setProgress(0);
          setSeed(Math.floor(Math.random() * 99999));
          return;
        }
      } catch (e) {
        console.warn("Local SD failed, using procedural:", e);
      }
    }

    // Fallback: procedural preview
    const totalMs = 600 + Math.random() * 1500;
    const start = performance.now();
    const tick = () => {
      const elapsed = performance.now() - start;
      const p = Math.min(1, elapsed / totalMs);
      setProgress(p);
      if (p < 1) requestAnimationFrame(tick);
      else finishGen();
    };
    requestAnimationFrame(tick);
  };

  const finishGen = async () => {
    // Make procedural placeholder image based on prompt hash + palette
    const cnv = document.createElement("canvas");
    cnv.width = aspectDef.w; cnv.height = aspectDef.h;
    const ctx = cnv.getContext("2d");
    proceduralImage(ctx, aspectDef.w, aspectDef.h, prompt, palette, seed);

    cnv.toBlob(async (blob) => {
      const url = URL.createObjectURL(blob);
      const im = new Image();
      await new Promise(r => { im.onload = r; im.src = url; });
      const out = {
        id: ++_gid, url, prompt, palette, model, aspect,
        seed, w: aspectDef.w, h: aspectDef.h,
        thumb: cnv.toDataURL("image/jpeg", 0.6),
        elapsed: (Math.random() * 1.4 + 0.5).toFixed(2),
        img: im,
      };
      setResults(r => [out, ...r].slice(0, 24));
      setSelected(out);
      setBusy(false);
      setProgress(0);
      setSeed(Math.floor(Math.random() * 99999));
    });
  };

  const sendToLayers = (g) => {
    // create a fake "image layer" in MediaStore
    fetch(g.url).then(r => r.blob()).then(b => {
      const f = new File([b], `gen-${g.id}.png`, { type: "image/png" });
      window.MediaStore?.add(f);
    });
  };

  return (
    <div className="panel stage-bezel" style={{ padding: 0, display: "flex", flexDirection: "column" }}>
      <Screws />
      <div className="stage-toolbar">
        <div className="stage-tabs"><div className="stage-tab active">Generative AI</div></div>
        <div style={{ flex: 1 }} />
        <Lcd tone={connected ? "green" : "amber"} style={{ fontSize: 10, padding: "4px 8px" }}>
          <Led on={connected} color={connected ? "green" : "amber"} size={6} style={{marginRight: 6}} />
          {connected ? "LOCAL MODEL · ONLINE" : "OFFLINE · PROCEDURAL"}
        </Lcd>
        <button className={`nbtn compact ${realtime ? "glow pressed" : ""}`} onClick={() => setRealtime(r => !r)}
          style={realtime ? { color: "var(--led-green)", borderLeft: "2px solid var(--led-green)" } : {}}>
          {realtime ? "● REALTIME" : "○ Realtime"}
        </button>
        <input value={endpoint} onChange={e => setEndpoint(e.target.value)}
          style={{
            background: "linear-gradient(180deg,#0a0a0c,#14141a)",
            border: "none", borderRadius: 6, padding: "5px 8px",
            color: "var(--led-cyan)", fontFamily: "var(--font-mono)", fontSize: 10,
            boxShadow: "var(--nshadow-in-sm)", outline: "none", width: 200,
            textShadow: "0 0 3px var(--led-cyan-glow)",
          }} placeholder="http://127.0.0.1:7860" />
        <button className="nbtn compact" onClick={tryConnect} disabled={busy}>
          {busy ? "…" : "Connect"}
        </button>
      </div>

      <div className="lum-wsgrid" style={{ display: "grid", gridTemplateColumns: "240px 1fr 260px", gap: 0, flex: 1, minHeight: 0 }}>
        {/* LEFT: presets */}
        <div style={{
          padding: 12, background: "linear-gradient(180deg, #14141a, #0a0a0c)",
          borderRight: "1px solid rgba(255,255,255,0.04)",
          display: "flex", flexDirection: "column", gap: 10, overflowY: "auto",
        }}>
          <div className="cap">Style Presets</div>
          <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 5 }}>
            {STYLE_PRESETS.map(p => (
              <button key={p.id} className="nbtn compact"
                onClick={() => pickPreset(p)}
                style={{
                  padding: "8px 6px", flexDirection: "column", gap: 4,
                  background: `linear-gradient(135deg, ${p.palette[0]}30, ${p.palette[1]}20)`,
                  borderLeft: `2px solid ${p.palette[0]}`,
                  fontSize: 9,
                }}>
                <div style={{ display: "flex", gap: 2 }}>
                  {p.palette.map((c, i) => (
                    <span key={i} style={{
                      width: 12, height: 12, borderRadius: 3,
                      background: c, boxShadow: `0 0 4px ${c}`,
                    }} />
                  ))}
                </div>
                <span style={{ fontSize: 9 }}>{p.label}</span>
              </button>
            ))}
          </div>

          <div className="cap" style={{ marginTop: 6 }}>Model</div>
          <div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
            {MODELS.map(m => (
              <button key={m.id}
                onClick={() => { setModel(m.id); setSteps(m.steps); }}
                className={`preset-card ${m.id === model ? "active" : ""}`}
                style={{ padding: 7 }}>
                <div className="preset-icon" style={{ width: 30, height: 30, fontSize: 14, color: "var(--led-violet)" }}>
                  ✦
                </div>
                <div style={{ flex: 1, minWidth: 0, textAlign: "left" }}>
                  <div className="preset-name" style={{ fontSize: 10 }}>{m.label}</div>
                  <div className="preset-tag" style={{ fontSize: 7.5 }}>{m.licence}</div>
                </div>
              </button>
            ))}
          </div>

          <div className="cap" style={{ marginTop: 6 }}>Aspect</div>
          <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 4 }}>
            {ASPECT_PRESETS.map(a => (
              <button key={a.id}
                onClick={() => setAspect(a.id)}
                className={`chip ${aspect === a.id ? "active" : ""}`}
                style={{ padding: "6px 4px", fontSize: 9 }}>
                {a.label}
                <br/>
                <span style={{ fontSize: 7, opacity: 0.6 }}>{a.w}×{a.h}</span>
              </button>
            ))}
          </div>

          <div style={{
            padding: "8px 10px", marginTop: 10,
            background: "rgba(75,230,255,0.06)",
            border: "1px solid rgba(75,230,255,0.2)",
            borderRadius: 6, fontSize: 9, lineHeight: 1.5,
            color: "var(--ink-dim)",
          }}>
            <b style={{ color: "var(--led-cyan)" }}>Commercial OK.</b><br/>
            FLUX.1-schnell (Apache 2.0) is your safest bet for commercial output. Run locally via
            ComfyUI / Diffusers / Stable Horde. App connects to <code>localhost:7860</code> by default.
          </div>
        </div>

        {/* CENTER: preview + prompt */}
        <div style={{ display: "flex", flexDirection: "column", background: "#000" }}>
          <div style={{
            flex: 1, position: "relative", padding: 18,
            display: "grid", placeItems: "center",
          }}>
            {realtime ? (
              <RealtimeGen palette={palette} style={rtStyle} aspectDef={aspectDef} canvasRef={rtCanvasRef} />
            ) : selected ? (
              <img src={selected.url}
                style={{
                  maxWidth: "100%", maxHeight: "100%",
                  borderRadius: 8,
                  boxShadow: "0 0 0 1px rgba(255,255,255,0.1), 0 20px 60px rgba(0,0,0,0.8), 0 0 40px rgba(255,174,58,0.2)"
                }} alt="" />
            ) : (
              <div style={{
                aspectRatio: `${aspectDef.w} / ${aspectDef.h}`, width: "70%",
                background: `linear-gradient(135deg, ${palette[0]}40, ${palette[1]}40, ${palette[2] || palette[0]}40)`,
                border: "1px dashed rgba(255,174,58,0.4)",
                borderRadius: 8,
                display: "grid", placeItems: "center",
                color: "var(--ink-dim)", fontFamily: "var(--font-mono)", fontSize: 11,
                letterSpacing: "0.2em", textTransform: "uppercase",
              }}>
                ✦ READY · {aspectDef.w}×{aspectDef.h}
              </div>
            )}
            {busy && (
              <div style={{
                position: "absolute", inset: 18, display: "grid", placeItems: "center",
                background: "rgba(0,0,0,0.65)", backdropFilter: "blur(4px)",
                borderRadius: 8,
              }}>
                <div style={{ textAlign: "center" }}>
                  <div style={{
                    width: 280, height: 6, borderRadius: 3,
                    background: "rgba(255,255,255,0.1)",
                    overflow: "hidden", margin: "0 auto",
                  }}>
                    <div style={{
                      width: `${progress * 100}%`, height: "100%",
                      background: "linear-gradient(90deg, var(--accent-glow), var(--accent))",
                      boxShadow: "0 0 12px var(--accent-glow)",
                      transition: "width 60ms linear",
                    }} />
                  </div>
                  <div style={{
                    marginTop: 14, fontFamily: "var(--font-mono)", fontSize: 11,
                    color: "var(--accent)", textShadow: "0 0 4px var(--accent-glow)",
                    letterSpacing: "0.18em", textTransform: "uppercase",
                  }}>
                    {modelDef.label} · {Math.round(progress * steps)}/{steps} steps · seed {seed}
                  </div>
                </div>
              </div>
            )}
          </div>

          {/* Prompt area */}
          <div style={{
            padding: 12,
            borderTop: "1px solid rgba(255,255,255,0.05)",
            background: "linear-gradient(180deg, #14141a, #0a0a0c)",
            display: "flex", flexDirection: "column", gap: 8,
          }}>
            <textarea value={prompt} onChange={e => setPrompt(e.target.value)}
              placeholder="Describe what you want…"
              style={{
                width: "100%", padding: "10px 12px",
                background: "linear-gradient(180deg,#0a0a0c,#14141a)",
                border: "1px solid rgba(255,255,255,0.06)", borderRadius: 6,
                color: "var(--ink)", fontFamily: "var(--font-ui)", fontSize: 12,
                outline: "none", resize: "none", minHeight: 56, lineHeight: 1.5,
              }} />
            <textarea value={neg} onChange={e => setNeg(e.target.value)}
              placeholder="Negative prompt (what to avoid)"
              style={{
                width: "100%", padding: "6px 10px",
                background: "linear-gradient(180deg,#0a0a0c,#14141a)",
                border: "1px solid rgba(255,59,59,0.1)", borderRadius: 6,
                color: "rgba(255,150,150,0.85)", fontFamily: "var(--font-mono)", fontSize: 10,
                outline: "none", resize: "none", minHeight: 28,
              }} />
            <div style={{ display: "flex", gap: 6, alignItems: "center" }}>
              {realtime ? (
                <>
                  <div style={{ display: "flex", gap: 3, flex: 1 }}>
                    {[["flow","Flow Field"],["plasma","Plasma"],["nebula","Nebula"],["voronoi","Cells"]].map(([id, lbl]) => (
                      <button key={id} className={`chip ${rtStyle === id ? "active" : ""}`}
                        onClick={() => setRtStyle(id)} style={{ flex: 1, padding: "8px 4px", fontSize: 9 }}>
                        {lbl}
                      </button>
                    ))}
                  </div>
                  <button className="nbtn glow pressed" style={{ padding: "12px 14px", fontSize: 11, fontWeight: 700 }}
                    onClick={() => {
                      const cnv = rtCanvasRef.current;
                      if (!cnv) return;
                      cnv.toBlob(b => {
                        if (!b) return;
                        const f = new File([b], `rt-gen-${Date.now()}.png`, { type: "image/png" });
                        window.MediaStore?.add(f);
                      });
                    }}>
                    ↗ Grab Frame
                  </button>
                </>
              ) : (
                <>
                  <button className="nbtn glow pressed" onClick={generate}
                    disabled={busy}
                    style={{ flex: 1, padding: "12px 14px", fontSize: 12, fontWeight: 700 }}>
                    {busy ? "⏵ GENERATING…" : "✦ GENERATE"}
                  </button>
                  <button className="nbtn compact"
                    onClick={() => setSeed(Math.floor(Math.random() * 99999))}>
                    🎲
                  </button>
                </>
              )}
            </div>
          </div>
        </div>

        {/* RIGHT: settings + recent */}
        <div style={{
          padding: 12, background: "linear-gradient(180deg, #14141a, #0a0a0c)",
          borderLeft: "1px solid rgba(255,255,255,0.04)",
          display: "flex", flexDirection: "column", gap: 10, overflowY: "auto",
        }}>
          <div className="dock-section" style={{ padding: 8 }}>
            <div className="dock-label">Sampling</div>
            <SliderInline label="Steps" value={steps} onChange={setSteps} min={1} max={50} step={1} />
            <SliderInline label="CFG" value={cfg} onChange={setCfg} min={1} max={20} step={0.5} />
            <SliderInline label="Batch" value={batch} onChange={v => setBatch(Math.round(v))} min={1} max={9} step={1} />
            <div style={{ marginTop: 6 }}>
              <span className="cap" style={{ fontSize: 8 }}>Sampler</span>
              <select value={sampler} onChange={e => setSampler(e.target.value)}
                style={{
                  width: "100%", marginTop: 3,
                  background: "linear-gradient(180deg,#0a0a0c,#14141a)",
                  border: "none", borderRadius: 4, padding: "4px 6px",
                  color: "var(--led-cyan)", fontFamily: "var(--font-mono)", fontSize: 10,
                  boxShadow: "var(--nshadow-in-sm)", outline: "none",
                }}>
                {["Euler a", "Euler", "DPM++ 2M Karras", "DPM++ SDE Karras", "DDIM", "LMS", "Heun", "UniPC"].map(s =>
                  <option key={s}>{s}</option>)}
              </select>
            </div>
            <div style={{ display: "grid", gridTemplateColumns: "40px 1fr", gap: 6, alignItems: "center", marginTop: 6 }}>
              <span className="cap" style={{ fontSize: 8 }}>Seed</span>
              <input type="number" value={seed} onChange={e => setSeed(Number(e.target.value))}
                style={{
                  background: "linear-gradient(180deg,#0a0a0c,#14141a)",
                  border: "none", borderRadius: 4, padding: "4px 6px",
                  color: "var(--led-cyan)", fontFamily: "var(--font-mono)", fontSize: 10,
                  boxShadow: "var(--nshadow-in-sm)", outline: "none",
                }} />
            </div>
          </div>

          <div className="cap">Recent ({results.length})</div>
          <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 4, overflowY: "auto", flex: 1, paddingRight: 4 }}>
            {results.map(g => (
              <button key={g.id}
                onClick={() => setSelected(g)}
                style={{
                  position: "relative",
                  aspectRatio: `${g.w} / ${g.h}`,
                  background: `url(${g.thumb}) center/cover`,
                  border: selected?.id === g.id ? "2px solid var(--accent)" : "1px solid rgba(255,255,255,0.06)",
                  borderRadius: 4, padding: 0, cursor: "pointer",
                  boxShadow: selected?.id === g.id ? "0 0 8px var(--accent-glow)" : "none",
                }}>
                <div style={{
                  position: "absolute", bottom: 0, left: 0, right: 0,
                  padding: "2px 4px",
                  background: "linear-gradient(0deg, rgba(0,0,0,0.85), transparent)",
                  fontSize: 7, fontFamily: "var(--font-mono)", color: "#fff",
                  textAlign: "left",
                }}>#{g.seed}</div>
              </button>
            ))}
          </div>

          {selected && (
            <div style={{ display: "flex", gap: 4, marginTop: "auto" }}>
              <button className="nbtn compact glow pressed" style={{ flex: 1 }}
                onClick={() => sendToLayers(selected)}>
                ↗ Add to Layers
              </button>
              <a className="nbtn compact" href={selected.url} download={`lumina-gen-${selected.id}.png`}
                style={{ textDecoration: "none", display: "inline-flex" }}>
                ⤓
              </a>
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

/* === Procedural placeholder generator (used when no local SD endpoint) ===
   Produces commercially-safe, royalty-free abstract images. */
function proceduralImage(ctx, W, H, prompt, palette, seed) {
  // hash prompt -> stable seed
  let s = seed | 0;
  for (let i = 0; i < prompt.length; i++) s = (s * 31 + prompt.charCodeAt(i)) | 0;
  const rng = mulberry32(s);

  // base gradient
  const g = ctx.createLinearGradient(0, 0, W, H);
  g.addColorStop(0, palette[0]);
  g.addColorStop(0.5, palette[1]);
  g.addColorStop(1, palette[2] || palette[0]);
  ctx.fillStyle = g;
  ctx.fillRect(0, 0, W, H);

  // soft color blobs (Gaussian-ish)
  ctx.globalCompositeOperation = "screen";
  for (let i = 0; i < 30; i++) {
    const x = rng() * W, y = rng() * H;
    const r = (0.05 + rng() * 0.25) * Math.max(W, H);
    const c = palette[Math.floor(rng() * palette.length)];
    const rg = ctx.createRadialGradient(x, y, 0, x, y, r);
    rg.addColorStop(0, c + "AA");
    rg.addColorStop(0.5, c + "44");
    rg.addColorStop(1, c + "00");
    ctx.fillStyle = rg;
    ctx.fillRect(0, 0, W, H);
  }

  // procedural shapes
  ctx.globalCompositeOperation = "source-over";
  const styleHint = prompt.toLowerCase();
  if (styleHint.includes("grid") || styleHint.includes("vapor") || styleHint.includes("retro")) {
    // perspective grid
    const horizon = H * 0.55;
    ctx.strokeStyle = "rgba(255,255,255,0.6)";
    ctx.lineWidth = 1.5;
    for (let i = -10; i <= 10; i++) {
      ctx.beginPath();
      ctx.moveTo(W/2 + i * W * 0.5, H);
      ctx.lineTo(W/2, horizon);
      ctx.stroke();
    }
    for (let i = 1; i < 12; i++) {
      const u = i / 12;
      const y = horizon + (H - horizon) * u * u;
      ctx.beginPath();
      ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke();
    }
    // sun
    const sr = Math.min(W, H) * 0.18;
    const sg = ctx.createRadialGradient(W/2, horizon, 0, W/2, horizon, sr);
    sg.addColorStop(0, palette[0]); sg.addColorStop(1, palette[0] + "00");
    ctx.fillStyle = sg;
    ctx.beginPath(); ctx.arc(W/2, horizon, sr, 0, Math.PI * 2); ctx.fill();
  } else if (styleHint.includes("geo")) {
    // isometric shapes
    for (let i = 0; i < 20; i++) {
      ctx.fillStyle = palette[i % palette.length] + "BB";
      ctx.beginPath();
      const x = rng() * W, y = rng() * H, r = 30 + rng() * 100;
      const sides = 3 + Math.floor(rng() * 5);
      for (let j = 0; j < sides; j++) {
        const a = (j / sides) * Math.PI * 2;
        const px = x + Math.cos(a) * r, py = y + Math.sin(a) * r;
        if (j === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
      }
      ctx.closePath(); ctx.fill();
    }
  } else {
    // organic curves
    ctx.lineWidth = 2;
    for (let i = 0; i < 40; i++) {
      ctx.strokeStyle = palette[i % palette.length] + "88";
      ctx.beginPath();
      let x = rng() * W, y = rng() * H;
      ctx.moveTo(x, y);
      for (let s = 0; s < 30; s++) {
        x += (rng() - 0.5) * 40;
        y += (rng() - 0.5) * 40;
        ctx.lineTo(x, y);
      }
      ctx.stroke();
    }
  }

  // grain
  ctx.globalCompositeOperation = "overlay";
  const im = ctx.getImageData(0, 0, Math.min(W, 800), Math.min(H, 800));
  const d = im.data;
  for (let i = 0; i < d.length; i += 4) {
    const n = (rng() - 0.5) * 30;
    d[i]   = Math.max(0, Math.min(255, d[i] + n));
    d[i+1] = Math.max(0, Math.min(255, d[i+1] + n));
    d[i+2] = Math.max(0, Math.min(255, d[i+2] + n));
  }
  ctx.putImageData(im, 0, 0);
  ctx.globalCompositeOperation = "source-over";

  // signature watermark
  ctx.fillStyle = "rgba(255,255,255,0.15)";
  ctx.font = `600 ${Math.floor(H * 0.012)}px "JetBrains Mono", monospace`;
  ctx.textAlign = "right";
  ctx.fillText(`LUMINA · gen #${seed}`, W - 14, H - 14);
}

function mulberry32(a) {
  return function () {
    let t = a += 0x6D2B79F5;
    t = Math.imul(t ^ (t >>> 15), t | 1);
    t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

function SliderInline({ label, value, onChange, min, max, step, unit = "" }) {
  return (
    <div style={{ display: "grid", gridTemplateColumns: "40px 1fr 42px", gap: 6, alignItems: "center", marginTop: 4 }}>
      <span className="cap" style={{ fontSize: 8 }}>{label}</span>
      <HSlider value={value} onChange={onChange} min={min} max={max} />
      <span style={{ fontFamily: "var(--font-mono)", fontSize: 9, color: "var(--accent)",
        textShadow: "0 0 3px var(--accent-glow)", textAlign: "right" }}>
        {step >= 1 ? value.toFixed(0) : value.toFixed(2)}{unit}
      </span>
    </div>
  );
}

/* === Realtime generative canvas — continuously-evolving, audio-reactive ===
   Live procedural art driven by AudioEngine. Royalty-free, runs offline. */
function RealtimeGen({ palette, style, aspectDef, canvasRef }) {
  const innerRef = useRefG(null);
  const rafRef = useRefG(0);
  const stateRef = useRefG({ particles: null, cells: null, t0: performance.now() });
  const styleRef = useRefG(style); styleRef.current = style;
  const palRef = useRefG(palette); palRef.current = palette;

  useEffectG(() => {
    const cnv = innerRef.current;
    if (!cnv) return;
    if (canvasRef) canvasRef.current = cnv;
    const ctx = cnv.getContext("2d");
    const W = cnv.width = aspectDef.w;
    const H = cnv.height = aspectDef.h;

    const hexToRgb = (h) => {
      const n = parseInt(h.replace("#", ""), 16);
      return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
    };

    const initParticles = () => {
      stateRef.current.particles = Array.from({ length: 900 }, () => ({
        x: Math.random() * W, y: Math.random() * H,
        px: 0, py: 0, c: Math.floor(Math.random() * palRef.current.length),
      }));
    };
    const initCells = () => {
      stateRef.current.cells = Array.from({ length: 18 }, (_, i) => ({
        x: Math.random(), y: Math.random(),
        a: Math.random() * Math.PI * 2, sp: 0.1 + Math.random() * 0.3, c: i % palRef.current.length,
      }));
    };
    initParticles(); initCells();
    ctx.fillStyle = "#06060a"; ctx.fillRect(0, 0, W, H);

    const TAU = Math.PI * 2;
    const draw = () => {
      const A = window.AudioEngine || { bass: 0, mid: 0, high: 0, energy: 0, beat: 0 };
      const t = (performance.now() - stateRef.current.t0) / 1000;
      const pal = palRef.current;
      const st = styleRef.current;

      if (st === "flow") {
        ctx.fillStyle = `rgba(6,6,10,${0.06 + A.energy * 0.05})`;
        ctx.fillRect(0, 0, W, H);
        const ps = stateRef.current.particles;
        const sp = 1 + A.energy * 5;
        for (const p of ps) {
          p.px = p.x; p.py = p.y;
          const ang = (Math.sin(p.x * 0.004 + t * 0.3) + Math.cos(p.y * 0.004 - t * 0.2)) * Math.PI
                      + A.bass * 4;
          p.x += Math.cos(ang) * sp; p.y += Math.sin(ang) * sp;
          if (p.x < 0 || p.x > W || p.y < 0 || p.y > H) {
            p.x = Math.random() * W; p.y = Math.random() * H; p.px = p.x; p.py = p.y;
          }
          const [r, g, b] = hexToRgb(pal[p.c]);
          ctx.strokeStyle = `rgba(${r},${g},${b},${0.5 + A.high * 0.5})`;
          ctx.lineWidth = 1 + A.bass * 2;
          ctx.beginPath(); ctx.moveTo(p.px, p.py); ctx.lineTo(p.x, p.y); ctx.stroke();
        }
      } else if (st === "plasma") {
        const step = 8;
        const sc = 0.01;
        const amp = 1 + A.bass * 3;
        for (let y = 0; y < H; y += step) {
          for (let x = 0; x < W; x += step) {
            const v = Math.sin(x * sc + t) + Math.sin(y * sc * 1.3 - t * 0.7) +
                      Math.sin((x + y) * sc * 0.7 + t * 0.5) +
                      Math.sin(Math.hypot(x - W/2, y - H/2) * sc * amp - t);
            const idx = Math.floor(((v + 4) / 8) * pal.length) % pal.length;
            const [r, g, b] = hexToRgb(pal[(idx + pal.length) % pal.length]);
            const li = 0.5 + v * 0.12 + A.energy * 0.3;
            ctx.fillStyle = `rgb(${Math.max(0,r*li)|0},${Math.max(0,g*li)|0},${Math.max(0,b*li)|0})`;
            ctx.fillRect(x, y, step, step);
          }
        }
      } else if (st === "nebula") {
        ctx.fillStyle = `rgba(6,6,10,0.08)`;
        ctx.fillRect(0, 0, W, H);
        ctx.globalCompositeOperation = "lighter";
        const N = 6;
        for (let i = 0; i < N; i++) {
          const a = t * (0.2 + i * 0.05) + i;
          const x = W/2 + Math.cos(a) * W * 0.28 * (1 + A.bass);
          const y = H/2 + Math.sin(a * 1.3) * H * 0.28 * (1 + A.mid);
          const r = (0.12 + A.energy * 0.2) * Math.max(W, H);
          const [cr, cg, cb] = hexToRgb(pal[i % pal.length]);
          const rg = ctx.createRadialGradient(x, y, 0, x, y, r);
          rg.addColorStop(0, `rgba(${cr},${cg},${cb},0.4)`);
          rg.addColorStop(1, `rgba(${cr},${cg},${cb},0)`);
          ctx.fillStyle = rg; ctx.beginPath(); ctx.arc(x, y, r, 0, TAU); ctx.fill();
        }
        ctx.globalCompositeOperation = "source-over";
      } else if (st === "voronoi") {
        const cells = stateRef.current.cells;
        for (const c of cells) {
          c.x += Math.cos(c.a) * 0.001 * c.sp * (1 + A.energy * 3);
          c.y += Math.sin(c.a) * 0.001 * c.sp * (1 + A.energy * 3);
          if (c.x < 0 || c.x > 1) c.a = Math.PI - c.a;
          if (c.y < 0 || c.y > 1) c.a = -c.a;
          c.x = Math.max(0, Math.min(1, c.x)); c.y = Math.max(0, Math.min(1, c.y));
        }
        const step = 10;
        for (let y = 0; y < H; y += step) {
          for (let x = 0; x < W; x += step) {
            let best = 1e9, second = 1e9, bi = 0;
            for (let i = 0; i < cells.length; i++) {
              const dx = cells[i].x * W - x, dy = cells[i].y * H - y;
              const d = dx*dx + dy*dy;
              if (d < best) { second = best; best = d; bi = i; }
              else if (d < second) second = d;
            }
            const edge = Math.sqrt(second) - Math.sqrt(best);
            const [r, g, b] = hexToRgb(pal[cells[bi].c]);
            if (edge < 3) { ctx.fillStyle = `rgba(255,255,255,${0.6 + A.high * 0.4})`; }
            else { const li = 0.25 + A.energy * 0.5; ctx.fillStyle = `rgb(${r*li|0},${g*li|0},${b*li|0})`; }
            ctx.fillRect(x, y, step, step);
          }
        }
      }
      rafRef.current = requestAnimationFrame(draw);
    };
    rafRef.current = requestAnimationFrame(draw);
    return () => cancelAnimationFrame(rafRef.current);
  }, [aspectDef.w, aspectDef.h]);

  return (
    <div style={{ position: "relative", width: "100%", height: "100%", display: "grid", placeItems: "center" }}>
      <canvas ref={innerRef} style={{
        maxWidth: "100%", maxHeight: "100%", borderRadius: 8,
        boxShadow: "0 0 0 1px rgba(124,255,79,0.25), 0 20px 60px rgba(0,0,0,0.8), 0 0 40px rgba(124,255,79,0.15)",
      }} />
      <div style={{
        position: "absolute", top: 12, left: 12,
        fontFamily: "var(--font-mono)", fontSize: 10, color: "var(--led-green)",
        background: "rgba(0,0,0,0.5)", padding: "3px 8px", borderRadius: 4,
        letterSpacing: "0.14em", textTransform: "uppercase",
        boxShadow: "0 0 8px rgba(124,255,79,0.3)",
      }}>
        ● LIVE · {style} · audio-reactive
      </div>
    </div>
  );
}

window.GenerativePanel = GenerativePanel;
