/* ========================================================================
   LUMINA — AI Stream (realtime frame-by-frame diffusion, audio-reactive)
   StreamDiffusion-style loop: every frame the LIVE composite is sent as
   img2img to a local diffusion server and the result comes back as the
   "AI Stream" visualizer source — usable in Layers, Live Loops, Output
   Mapping and the node graph like any other source.
   Works with any local server exposing the /sdapi/v1/img2img API
   (incl. realtime LCM/Turbo builds). Fast & lean: low-res frames,
   1–4 steps, JPEG transport, self-paced loop (no queue pile-up).
   Audio-reactive: bass pushes denoising — the wall "breathes" with the kick.
   ======================================================================== */
(function () {
  const { useState, useEffect, useRef } = React;
  const STORE = "lumina_ai_stream_v1";

  const S = {
    running: false,
    endpoint: "http://127.0.0.1:7860",
    prompt: "baroque palace facade dissolving into nebula, volumetric light, high contrast",
    negPrompt: "text, watermark, frame, border",
    denoiseBase: 0.38,
    denoiseAudio: 0.3,     // how much BASS pushes denoise
    cfg: 1.8,
    steps: 2,
    size: 384,             // square-ish working res (w computed by aspect)
    seed: 1234,
    lockSeed: true,
    fps: 0,
    lastErr: null,
    _frame: null,          // latest decoded frame (Image)
    _prevFrame: null,
    _frameT: 0,
    listeners: new Set(),
    on(fn) { this.listeners.add(fn); return () => this.listeners.delete(fn); },
    emit() { this.listeners.forEach(fn => { try { fn(this); } catch (e) {} }); },

    save() {
      try {
        const { endpoint, prompt, negPrompt, denoiseBase, denoiseAudio, cfg, steps, size, seed, lockSeed } = this;
        localStorage.setItem(STORE, JSON.stringify({ endpoint, prompt, negPrompt, denoiseBase, denoiseAudio, cfg, steps, size, seed, lockSeed }));
      } catch (e) {}
    },
    restore() {
      try { Object.assign(this, JSON.parse(localStorage.getItem(STORE) || "{}")); } catch (e) {}
    },

    start() {
      if (this.running) return;
      this.running = true;
      this.lastErr = null;
      this.emit();
      this._loop();
    },
    stop() { this.running = false; this.emit(); },

    async _loop() {
      const srcCnv = document.createElement("canvas");
      let lastDone = performance.now();
      while (this.running) {
        try {
          const t = performance.now() / 1000;
          // grab the LIVE composite at working resolution (16:9)
          const H = this.size, W = Math.round(this.size * 16 / 9 / 8) * 8;
          srcCnv.width = W; srcCnv.height = H;
          const sx = srcCnv.getContext("2d");
          if (window.LuminaComposite) window.LuminaComposite(sx, W, H, t);
          const b64 = srcCnv.toDataURL("image/jpeg", 0.62).split(",")[1];

          // audio-reactive denoise: the kick reshapes the image
          const A = window.AudioEngine || {};
          const denoise = Math.max(0.05, Math.min(0.92,
            this.denoiseBase + (A.bass || 0) * this.denoiseAudio));

          const res = await fetch(this.endpoint.replace(/\/$/, "") + "/sdapi/v1/img2img", {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({
              init_images: [b64],
              prompt: this.prompt,
              negative_prompt: this.negPrompt,
              denoising_strength: denoise,
              steps: this.steps,
              cfg_scale: this.cfg,
              width: W, height: H,
              seed: this.lockSeed ? this.seed : -1,
              sampler_name: "Euler a",
            }),
          });
          if (!res.ok) throw new Error("HTTP " + res.status);
          const data = await res.json();
          const img64 = data.images && data.images[0];
          if (img64) {
            const im = new Image();
            await new Promise((ok, ko) => { im.onload = ok; im.onerror = ko; im.src = "data:image/png;base64," + img64; });
            this._prevFrame = this._frame;
            this._frame = im;
            this._frameT = performance.now();
            const now = performance.now();
            this.fps = this.fps * 0.7 + (1000 / Math.max(1, now - lastDone)) * 0.3;
            lastDone = now;
            this.lastErr = null;
          }
        } catch (e) {
          this.lastErr = String(e.message || e);
          this.fps = 0;
          await new Promise(r => setTimeout(r, 1200));   // back off, retry
        }
        this.emit();
      }
    },
  };
  S.restore();
  window.LuminaAIStream = S;

  /* ── register as a first-class visualizer source ── */
  function drawCover(ctx, img, w, h, alpha) {
    const sr = img.width / img.height, tr = w / h;
    let dw, dh;
    if (sr > tr) { dh = h; dw = h * sr; } else { dw = w; dh = w / sr; }
    ctx.globalAlpha = alpha;
    ctx.drawImage(img, (w - dw) / 2, (h - dh) / 2, dw, dh);
    ctx.globalAlpha = 1;
  }
  window.VISUALIZERS = window.VISUALIZERS || [];
  window.VISUALIZERS.push({
    id: "aistream", name: "AI Stream", tag: "DIFFUSION · LIVE",
    draw(ctx, w, h, A, P, t) {
      ctx.fillStyle = "#000"; ctx.fillRect(0, 0, w, h);
      if (S._frame) {
        // crossfade between the two latest AI frames for fluid motion
        const age = Math.min(1, (performance.now() - S._frameT) / 220);
        if (S._prevFrame) drawCover(ctx, S._prevFrame, w, h, 1);
        drawCover(ctx, S._frame, w, h, S._prevFrame ? age : 1);
      } else {
        ctx.fillStyle = "rgba(255,174,58,0.7)";
        ctx.font = "bold 13px monospace";
        ctx.textAlign = "center"; ctx.textBaseline = "middle";
        ctx.fillText(S.running ? "AI STREAM · waiting for first frame…" : "AI STREAM idle — open the ◉ AI Stream tool", w / 2, h / 2);
      }
    },
  });

  /* ── floating tool panel ── */
  function Row({ label, children }) {
    return (
      <div style={{ display: "grid", gridTemplateColumns: "64px 1fr 40px", gap: 6, alignItems: "center", marginTop: 5 }}>
        <span className="cap" style={{ fontSize: 8 }}>{label}</span>
        {children}
      </div>
    );
  }
  function Slider({ label, value, min, max, step, onChange, fmt }) {
    return (
      <Row label={label}>
        <input type="range" min={min} max={max} step={step} value={value}
          onChange={e => onChange(Number(e.target.value))} style={{ width: "100%" }} />
        <span style={{ fontFamily: "var(--font-mono)", fontSize: 9, color: "var(--led-cyan)", textAlign: "right" }}>
          {fmt ? fmt(value) : value}
        </span>
      </Row>
    );
  }

  function AIStreamTool() {
    const [open, setOpen] = useState(false);
    const [, setTick] = useState(0);
    const cnvRef = useRef(null);
    useEffect(() => S.on(() => setTick(t => (t + 1) % 1e6)), []);
    useEffect(() => { S.save(); });
    // mini live preview of the AI frame
    useEffect(() => {
      if (!open) return;
      let raf;
      const draw = () => {
        const c = cnvRef.current;
        if (c && S._frame) {
          c.width = 232; c.height = 130;
          const x = c.getContext("2d");
          x.fillStyle = "#000"; x.fillRect(0, 0, c.width, c.height);
          drawCover(x, S._frame, c.width, c.height, 1);
        }
        raf = requestAnimationFrame(draw);
      };
      raf = requestAnimationFrame(draw);
      return () => cancelAnimationFrame(raf);
    }, [open]);

    if (!open) {
      return (
        <button onClick={() => setOpen(true)}
          style={{
            padding: "8px 14px", border: "none", borderRadius: 8,
            background: "linear-gradient(180deg, var(--panel-hi), var(--panel))",
            color: S.running ? "var(--led-green)" : "var(--led-violet)",
            fontFamily: "var(--font-display)", fontWeight: 800, fontSize: 10,
            letterSpacing: "0.16em", textTransform: "uppercase",
            cursor: "pointer", boxShadow: "var(--nshadow-out-sm)",
            display: "flex", alignItems: "center", gap: 6, minWidth: 150, textAlign: "left",
          }}>
          <span style={{
            width: 8, height: 8, borderRadius: "50%",
            background: S.running ? "var(--led-green)" : "var(--ink-faint)",
            boxShadow: S.running ? "0 0 8px var(--led-green)" : "none",
            animation: S.running ? "pulse 1.2s infinite" : "none",
          }} />
          ◉ AI Stream{S.running ? ` · ${S.fps.toFixed(1)}fps` : ""}
        </button>
      );
    }

    return (
      <div style={{
        width: 260,
        background: "linear-gradient(180deg, var(--panel-hi), var(--panel-lo))",
        borderRadius: 10, padding: 10,
        boxShadow: "0 20px 50px rgba(0,0,0,0.7), 0 0 0 1px rgba(183,133,255,0.35)",
      }}>
        <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 8 }}>
          <span style={{
            fontFamily: "var(--font-display)", fontWeight: 800, fontSize: 11,
            letterSpacing: "0.16em", textTransform: "uppercase",
            color: "var(--led-violet)", flex: 1,
          }}>◉ AI Stream · realtime</span>
          <button data-lum-close="1" className="nbtn compact" onClick={() => setOpen(false)} style={{ padding: "2px 7px" }}>×</button>
        </div>

        <canvas ref={cnvRef} style={{
          width: "100%", aspectRatio: "16/9", borderRadius: 6, display: "block",
          background: "#000", boxShadow: "var(--nshadow-in-sm)", marginBottom: 6,
        }} />

        <div style={{ display: "flex", gap: 6, marginBottom: 6 }}>
          <button className={`nbtn compact ${S.running ? "" : "glow pressed"}`}
            style={{ flex: 1, padding: "7px", fontWeight: 800,
              color: S.running ? "var(--led-red)" : "var(--led-green)" }}
            onClick={() => { S.running ? S.stop() : S.start(); }}>
            {S.running ? "■ STOP" : "▶ START STREAM"}
          </button>
          <div style={{
            padding: "4px 8px", borderRadius: 5, alignSelf: "center",
            fontFamily: "var(--font-mono)", fontSize: 9,
            color: S.running ? "var(--led-green)" : "var(--ink-faint)",
            background: "rgba(0,0,0,0.4)",
          }}>{S.fps.toFixed(1)} fps</div>
        </div>

        <input value={S.endpoint} onChange={e => { S.endpoint = e.target.value; S.emit(); }}
          placeholder="http://127.0.0.1:7860"
          style={{
            width: "100%", padding: "5px 7px", borderRadius: 4, border: "none", outline: "none",
            background: "linear-gradient(180deg,#0a0a0c,#14141a)", color: "var(--led-cyan)",
            fontFamily: "var(--font-mono)", fontSize: 9, boxShadow: "var(--nshadow-in-sm)",
            boxSizing: "border-box",
          }} />
        <textarea value={S.prompt} onChange={e => { S.prompt = e.target.value; S.emit(); }}
          rows={2} placeholder="prompt"
          style={{
            width: "100%", marginTop: 5, padding: "5px 7px", borderRadius: 4, border: "none",
            outline: "none", resize: "vertical", boxSizing: "border-box",
            background: "linear-gradient(180deg,#0a0a0c,#14141a)", color: "var(--ink)",
            fontFamily: "var(--font-mono)", fontSize: 9, boxShadow: "var(--nshadow-in-sm)",
          }} />

        <Slider label="Denoise" value={S.denoiseBase} min={0.05} max={0.9} step={0.01}
          onChange={v => { S.denoiseBase = v; S.emit(); }} fmt={v => v.toFixed(2)} />
        <Slider label="Bass→Dnz" value={S.denoiseAudio} min={0} max={0.6} step={0.01}
          onChange={v => { S.denoiseAudio = v; S.emit(); }} fmt={v => v.toFixed(2)} />
        <Slider label="Steps" value={S.steps} min={1} max={8} step={1}
          onChange={v => { S.steps = v; S.emit(); }} />
        <Slider label="CFG" value={S.cfg} min={1} max={7} step={0.1}
          onChange={v => { S.cfg = v; S.emit(); }} fmt={v => v.toFixed(1)} />
        <Row label="Res">
          <div className="chips" style={{ padding: 2 }}>
            {[256, 384, 512].map(r => (
              <button key={r} className={`chip ${S.size === r ? "active" : ""}`}
                onClick={() => { S.size = r; S.emit(); }}
                style={{ padding: "3px 7px", fontSize: 9 }}>{r}</button>
            ))}
          </div>
          <span />
        </Row>
        <Row label="Seed lock">
          <Flip value={S.lockSeed} onChange={v => { S.lockSeed = v; S.emit(); }} />
          <span />
        </Row>

        {S.lastErr && (
          <div style={{ marginTop: 6, fontSize: 9, color: "var(--led-red)",
            fontFamily: "var(--font-mono)", wordBreak: "break-all" }}>
            ⚠ {S.lastErr} — server running?
          </div>
        )}
        <div style={{ fontSize: 8.5, color: "var(--ink-faint)", marginTop: 6, lineHeight: 1.5 }}>
          Feeds the LIVE composite frame-by-frame to a local diffusion server
          (img2img API, port 7860) and returns it as the
          <b style={{ color: "var(--led-violet)" }}> AI Stream</b> source —
          pick it in Channels / Layers / Mapping. Use an LCM/Turbo model
          with 1–2 steps for realtime.
        </div>
      </div>
    );
  }

  function mount() {
    if (!window.React || !window.ReactDOM) { setTimeout(mount, 100); return; }
    let host = document.getElementById("__aistream_panel");
    if (!host) {
      host = document.createElement("div");
      host.id = "__aistream_panel";
      host.style.cssText = "position: fixed; top: 214px; right: 18px; z-index: 595;";
      document.body.appendChild(host);
    }
    ReactDOM.createRoot(host).render(<AIStreamTool />);
  }
  if (document.readyState === "complete" || document.readyState === "interactive") setTimeout(mount, 1500);
  else document.addEventListener("DOMContentLoaded", () => setTimeout(mount, 1500));
})();
