/* ========================================================================
   LUMINA — Lighting / DMX workspace (Art-Net / sACN style)
   Audio-reactive stage lighting: moving heads, PARs, strobes, lasers, bars.
   Live light-reactive preview driven by window.AudioEngine, BPM chases,
   per-fixture audio mapping, DMX universe patch + live channel monitor.
   The "Lumiverse" — what makes this a true Resolume Arena competitor for
   DJ parties: VJ visuals AND the room lights move with the music.
   ======================================================================== */
const { useState: useStateL, useEffect: useEffectL, useRef: useRefL } = React;

/* fixture archetypes — DMX footprint + look */
const FIXTURE_TYPES = {
  par:     { label: "RGB PAR",        ch: 4,  icon: "◉", glow: 90,  beam: false },
  wash:    { label: "RGBW Wash",      ch: 7,  icon: "⬭", glow: 150, beam: false },
  moving:  { label: "Moving Head",    ch: 14, icon: "✦", glow: 70,  beam: true  },
  strobe:  { label: "Strobe",         ch: 2,  icon: "▣", glow: 220, beam: false },
  blinder: { label: "Audience Blinder",ch: 3, icon: "▦", glow: 200, beam: false },
  bar:     { label: "LED Pixel Bar",  ch: 24, icon: "▬", glow: 60,  beam: false },
  laser:   { label: "Laser Fan",      ch: 6,  icon: "✸", glow: 40,  beam: true  },
};

const COLOR_PALETTES = [
  { id: "rave",   label: "Rave",    colors: ["#ff1a8c", "#00e6ff", "#7cff4f", "#b14bff"] },
  { id: "warm",   label: "Warm",    colors: ["#ff7a1a", "#ffd84a", "#ff3b3b", "#ffae3a"] },
  { id: "cool",   label: "Cool",    colors: ["#1a6bff", "#00e6ff", "#7cffea", "#b14bff"] },
  { id: "uv",     label: "UV/Pink", colors: ["#b14bff", "#ff1a8c", "#6a00ff", "#ff5ad6"] },
  { id: "mono",   label: "Strobe W",colors: ["#ffffff", "#eaf2ff", "#ffffff", "#dfe8ff"] },
];

const CHASES = [
  { id: "off",     label: "Static" },
  { id: "chase",   label: "Chase" },
  { id: "pingpong",label: "Ping-Pong" },
  { id: "alt",     label: "Odd/Even" },
  { id: "random",  label: "Sparkle" },
  { id: "wave",    label: "Wave" },
];

let _fid = 0;
function makeFixture(type, x, y) {
  const def = FIXTURE_TYPES[type];
  return {
    id: ++_fid, type, x, y,
    name: def.label + " " + (_fid),
    universe: 1, startCh: ((_fid - 1) * def.ch) % 512 + 1,
    // audio mapping
    dimBand: "energy",      // which band drives intensity
    colorMode: "palette",   // palette | static | audio
    staticColor: "#ff1a8c",
    paletteIdx: 0,
    strobeOnBeat: type === "strobe" || type === "blinder",
    movement: type === "moving" ? "sweep" : (type === "laser" ? "fan" : "none"),
    moveBand: "mid",
    rangeDeg: 60,
    react: 1,               // reactivity amount
    chaseGroup: 0,
  };
}

function LightingWorkspace({ audio }) {
  const [fixtures, setFixtures] = useStateL(() => {
    // default rig: a back line of moving heads + PAR wash + 2 strobes + blinders
    const arr = [];
    for (let i = 0; i < 6; i++) arr.push(makeFixture("moving", 0.12 + i * 0.152, 0.2));
    for (let i = 0; i < 8; i++) arr.push(makeFixture("par", 0.08 + i * 0.12, 0.46));
    arr.push(makeFixture("strobe", 0.2, 0.72));
    arr.push(makeFixture("strobe", 0.8, 0.72));
    arr.push(makeFixture("blinder", 0.4, 0.72));
    arr.push(makeFixture("blinder", 0.6, 0.72));
    arr.push(makeFixture("bar", 0.5, 0.9));
    return arr;
  });
  const [selId, setSelId] = useStateL(null);
  const [palette, setPalette] = useStateL("rave");
  const [chase, setChase] = useStateL("chase");
  const [bpm, setBpm] = useStateL(128);
  const [master, setMaster] = useStateL(1);
  const [blackout, setBlackout] = useStateL(false);
  const [scenes, setScenes] = useStateL(() => {
    try { return JSON.parse(localStorage.getItem("lumina_light_scenes_v1") || "[]"); } catch (e) { return []; }
  });
  const [activeScene, setActiveScene] = useStateL(null);
  const [flashOn, setFlashOn] = useStateL(false);
  const fxRef = useRefL({ flash: false, strobe: false });
  const tapRef = useRefL([]);
  const [artnetIP, setArtnetIP] = useStateL("2.0.0.1");
  const [showBeams, setShowBeams] = useStateL(true);
  const [monitorUniverse, setMonitorUniverse] = useStateL(1);

  const sel = fixtures.find(f => f.id === selId);
  const canvasRef = useRefL(null);
  const dragRef = useRefL(null);
  const dmxRef = useRefL(new Uint8Array(512)); // live channel values for monitor

  const upd = (id, patch) => setFixtures(arr => arr.map(f => f.id === id ? { ...f, ...patch } : f));
  const updSel = (patch) => sel && upd(sel.id, patch);

  const addFixture = (type) => {
    const f = makeFixture(type, 0.5, 0.5);
    setFixtures(arr => [...arr, f]);
    setSelId(f.id);
  };
  const removeFixture = (id) => { setFixtures(arr => arr.filter(f => f.id !== id)); if (selId === id) setSelId(null); };
  const dupFixture = () => { if (!sel) return; const f = { ...sel, id: ++_fid, x: Math.min(0.95, sel.x + 0.04), name: sel.name + " copy" }; setFixtures(arr => [...arr, f]); setSelId(f.id); };

  /* === Live performance: scenes, tap, flash === */
  useEffectL(() => { try { localStorage.setItem("lumina_light_scenes_v1", JSON.stringify(scenes)); } catch (e) {} }, [scenes]);

  const captureScene = () => ({
    palette, chase, bpm, master,
    fx: fixtures.map(f => ({ id: f.id, dimBand: f.dimBand, colorMode: f.colorMode,
      staticColor: f.staticColor, strobeOnBeat: f.strobeOnBeat, movement: f.movement,
      moveBand: f.moveBand, rangeDeg: f.rangeDeg, react: f.react })),
  });
  const saveScene = (idx) => {
    const snap = captureScene();
    setScenes(arr => {
      const next = [...arr];
      next[idx] = { ...snap, name: arr[idx]?.name || `Cue ${idx + 1}` };
      return next;
    });
    setActiveScene(idx);
  };
  const recallScene = (idx) => {
    const s = scenes[idx];
    if (!s) return;
    setPalette(s.palette); setChase(s.chase); setBpm(s.bpm); setMaster(s.master);
    setFixtures(arr => arr.map(f => {
      const o = s.fx?.find(x => x.id === f.id);
      return o ? { ...f, ...o } : f;
    }));
    setActiveScene(idx);
  };
  const clearScene = (idx) => setScenes(arr => arr.map((s, i) => i === idx ? null : s));

  const tap = () => {
    const now = performance.now();
    const arr = tapRef.current.filter(t => now - t < 2500);
    arr.push(now);
    tapRef.current = arr;
    if (arr.length >= 2) {
      const intervals = [];
      for (let i = 1; i < arr.length; i++) intervals.push(arr[i] - arr[i - 1]);
      const avg = intervals.reduce((a, b) => a + b, 0) / intervals.length;
      const newBpm = Math.round(60000 / avg);
      if (newBpm >= 60 && newBpm <= 220) setBpm(newBpm);
    }
  };
  const setFlash = (on) => { fxRef.current.flash = on; setFlashOn(on); };
  const setStrobe = (on) => { fxRef.current.strobe = on; };

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

  /* === Live audio-reactive render === */
  useEffectL(() => {
    const cnv = canvasRef.current;
    if (!cnv) return;
    const ctx = cnv.getContext("2d");
    let raf;
    const t0 = performance.now();

    const tick = () => {
      const r = cnv.parentElement.getBoundingClientRect();
      const dpr = Math.min(2, window.devicePixelRatio || 1);
      const W = Math.floor(r.width), H = Math.floor(r.height);
      if (cnv.width !== W * dpr) { cnv.width = W * dpr; cnv.height = H * dpr; }
      cnv.style.width = W + "px"; cnv.style.height = H + "px";
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);

      const A = window.AudioEngine || { bass: 0, mid: 0, high: 0, energy: 0, beat: 0 };
      const t = (performance.now() - t0) / 1000;
      const beatPhase = (t * bpm / 60) % 1;           // 0..1 within a beat
      const beatIdx = Math.floor(t * bpm / 60);
      const pal = COLOR_PALETTES.find(p => p.id === palette).colors;

      const band = (b) => (A[b] !== undefined ? A[b] : A.energy) || 0;
      const dmx = dmxRef.current; dmx.fill(0);

      // backdrop — dark club + haze
      ctx.fillStyle = "#050507"; ctx.fillRect(0, 0, W, H);
      const haze = ctx.createLinearGradient(0, 0, 0, H);
      haze.addColorStop(0, "rgba(20,20,40,0.25)");
      haze.addColorStop(1, "rgba(5,5,8,0)");
      ctx.fillStyle = haze; ctx.fillRect(0, 0, W, H);
      // truss line
      ctx.strokeStyle = "rgba(255,255,255,0.06)"; ctx.lineWidth = 6;
      ctx.beginPath(); ctx.moveTo(0, H * 0.16); ctx.lineTo(W, H * 0.16); ctx.stroke();

      const chaseActive = (i) => {
        if (chase === "off") return 1;
        const n = Math.max(1, fixtures.length);
        if (chase === "chase") return ((beatIdx % n) === (i % n)) ? 1 : 0.12;
        if (chase === "pingpong") { const p = beatIdx % (2 * n - 2 || 1); const pos = p < n ? p : 2 * n - 2 - p; return pos === (i % n) ? 1 : 0.12; }
        if (chase === "alt") return ((beatIdx % 2) === (i % 2)) ? 1 : 0.12;
        if (chase === "random") return (((beatIdx * 2654435761 ^ i * 40503) >>> 0) % 3 === 0) ? 1 : 0.1;
        if (chase === "wave") return 0.3 + 0.7 * (0.5 + 0.5 * Math.sin(t * 3 + i * 0.6));
        return 1;
      };

      ctx.globalCompositeOperation = "lighter";
      fixtures.forEach((f, i) => {
        const def = FIXTURE_TYPES[f.type];
        const px = f.x * W, py = f.y * H;
        // intensity
        let dim = band(f.dimBand) * f.react;
        dim = Math.min(1, 0.12 + dim * 1.4);
        if (f.strobeOnBeat) {
          const strobe = (A.beat > 0.35 && beatPhase < 0.12) ? 1 : (Math.random() > 0.86 && A.high > 0.4 ? 1 : 0.0);
          dim = strobe;
        }
        dim *= chaseActive(i) * master;
        if (blackout) dim = 0;
        // momentary live overrides
        const fx = fxRef.current;
        let forceWhite = false;
        if (fx.strobe) { dim = (beatPhase * 8 % 1) < 0.5 ? 1 : 0; forceWhite = true; }
        if (fx.flash) { dim = 1; forceWhite = true; }

        // color
        let col;
        if (forceWhite) col = "#ffffff";
        else if (f.colorMode === "static") col = f.staticColor;
        else if (f.colorMode === "audio") col = `hsl(${(band("bass") * 120 + band("high") * 240 + t * 40) % 360},100%,55%)`;
        else col = pal[(i + beatIdx) % pal.length];
        const [cr, cg, cb] = hexToRgb(col.startsWith("hsl") ? hslToHex(col) : col);

        // write DMX (RGB dimmer)
        if (f.universe === monitorUniverse) {
          const base = f.startCh - 1;
          dmx[(base) % 512] = Math.round(dim * 255);            // dimmer / R
          dmx[(base + 1) % 512] = Math.round(cr * dim);
          dmx[(base + 2) % 512] = Math.round(cg * dim);
          dmx[(base + 3) % 512] = Math.round(cb * dim);
        }

        if (dim <= 0.01) {
          // draw dim fixture body
          ctx.globalCompositeOperation = "source-over";
          drawBody(ctx, px, py, def, "#222", f.id === selId);
          ctx.globalCompositeOperation = "lighter";
          return;
        }

        // beams
        if (showBeams && def.beam) {
          let ang = Math.PI / 2; // down
          if (f.movement === "sweep") ang += Math.sin(t * (1 + band(f.moveBand) * 3)) * (f.rangeDeg * Math.PI / 180);
          if (f.type === "laser" || f.movement === "fan") {
            const fans = 9;
            for (let k = 0; k < fans; k++) {
              const a = Math.PI / 2 + (k - (fans - 1) / 2) * (f.rangeDeg / fans * Math.PI / 180) + Math.sin(t * 2) * 0.2;
              const len = H * (0.5 + band(f.moveBand) * 0.5);
              const g = ctx.createLinearGradient(px, py, px + Math.cos(a) * len, py + Math.sin(a) * len);
              g.addColorStop(0, `rgba(${cr},${cg},${cb},${0.7 * dim})`);
              g.addColorStop(1, `rgba(${cr},${cg},${cb},0)`);
              ctx.strokeStyle = g; ctx.lineWidth = 2;
              ctx.beginPath(); ctx.moveTo(px, py); ctx.lineTo(px + Math.cos(a) * len, py + Math.sin(a) * len); ctx.stroke();
            }
          } else {
            // moving-head cone
            const len = H * (0.55 + band(f.moveBand) * 0.4);
            const spread = 0.10 + band("bass") * 0.05;
            const ex = px + Math.cos(ang) * len, ey = py + Math.sin(ang) * len;
            const perpx = Math.cos(ang + Math.PI / 2), perpy = Math.sin(ang + Math.PI / 2);
            const ww = len * spread;
            const g = ctx.createLinearGradient(px, py, ex, ey);
            g.addColorStop(0, `rgba(${cr},${cg},${cb},${0.5 * dim})`);
            g.addColorStop(1, `rgba(${cr},${cg},${cb},0)`);
            ctx.fillStyle = g;
            ctx.beginPath();
            ctx.moveTo(px - perpx * 6, py - perpy * 6);
            ctx.lineTo(ex - perpx * ww, ey - perpy * ww);
            ctx.lineTo(ex + perpx * ww, ey + perpy * ww);
            ctx.lineTo(px + perpx * 6, py + perpy * 6);
            ctx.closePath(); ctx.fill();
          }
        }

        // glow pool
        const gr = def.glow * (0.5 + dim) * (1 + band("bass") * 0.5);
        const rg = ctx.createRadialGradient(px, py, 0, px, py, gr);
        rg.addColorStop(0, `rgba(${cr},${cg},${cb},${0.9 * dim})`);
        rg.addColorStop(0.4, `rgba(${cr},${cg},${cb},${0.35 * dim})`);
        rg.addColorStop(1, `rgba(${cr},${cg},${cb},0)`);
        ctx.fillStyle = rg;
        ctx.beginPath(); ctx.arc(px, py, gr, 0, Math.PI * 2); ctx.fill();

        // LED bar pixels
        if (f.type === "bar") {
          const segs = 12, bw = W * 0.22, sh = 7;
          for (let s = 0; s < segs; s++) {
            const sx = px - bw / 2 + (s / segs) * bw;
            const fi = Math.floor((s / segs) * 1);
            const c2 = pal[(s + beatIdx) % pal.length];
            const [r2, g2, b2] = hexToRgb(c2);
            const v = Math.min(1, dim * (0.4 + band("high") * (0.5 + 0.5 * Math.sin(s + t * 6))));
            ctx.fillStyle = `rgba(${r2},${g2},${b2},${v})`;
            ctx.shadowColor = c2; ctx.shadowBlur = 12 * v;
            ctx.fillRect(sx, py - sh / 2, bw / segs - 2, sh);
          }
          ctx.shadowBlur = 0;
        }

        // fixture body
        ctx.globalCompositeOperation = "source-over";
        drawBody(ctx, px, py, def, col, f.id === selId);
        ctx.globalCompositeOperation = "lighter";
      });
      ctx.globalCompositeOperation = "source-over";

      raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [fixtures, selId, palette, chase, bpm, master, blackout, showBeams, monitorUniverse]);

  const onDown = (e) => {
    const rect = canvasRef.current.getBoundingClientRect();
    const ux = (e.clientX - rect.left) / rect.width, uy = (e.clientY - rect.top) / rect.height;
    let best = null, bd = 0.05;
    fixtures.forEach(f => { const d = Math.hypot(f.x - ux, f.y - uy); if (d < bd) { bd = d; best = f.id; } });
    if (best !== null) { setSelId(best); dragRef.current = best; }
  };
  const onMove = (e) => {
    if (dragRef.current === null) return;
    const rect = canvasRef.current.getBoundingClientRect();
    const ux = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
    const uy = Math.max(0, Math.min(1, (e.clientY - rect.top) / rect.height));
    upd(dragRef.current, { x: ux, y: uy });
  };
  const onUp = () => { dragRef.current = null; };

  const totalCh = fixtures.reduce((s, f) => s + FIXTURE_TYPES[f.type].ch, 0);
  const universes = [...new Set(fixtures.map(f => f.universe))].sort((a, b) => a - b);

  return (
    <div className="panel stage-bezel" style={{ padding: 0, display: "flex", flexDirection: "column", overflow: "hidden" }}>
      <Screws />
      <div className="stage-toolbar" style={{ flexWrap: "wrap" }}>
        <div className="stage-tabs"><div className="stage-tab active">Lighting · DMX</div></div>
        <div style={{ flex: 1 }} />
        <span className="cap">Add:</span>
        <div className="chips" style={{ padding: 3 }}>
          {Object.entries(FIXTURE_TYPES).map(([k, d]) => (
            <button key={k} className="chip" onClick={() => addFixture(k)} style={{ padding: "5px 8px", fontSize: 9 }}>
              <span style={{ marginRight: 3 }}>{d.icon}</span>{d.label}
            </button>
          ))}
        </div>
        <button className={`nbtn compact ${showBeams ? "glow pressed" : ""}`} onClick={() => setShowBeams(b => !b)}>Beams</button>
        <button className={`nbtn compact ${blackout ? "danger glow pressed" : ""}`} onClick={() => setBlackout(b => !b)}>
          {blackout ? "● BLACKOUT" : "Blackout"}
        </button>
      </div>

      <div style={{ display: "grid", gridTemplateColumns: "230px 1fr 250px", flex: 1, minHeight: 0 }}>
        {/* LEFT: rig + look */}
        <div style={{ padding: 10, 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">Look · Palette</div>
          <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 4 }}>
            {COLOR_PALETTES.map(p => (
              <button key={p.id} className={`nbtn compact ${palette === p.id ? "glow pressed" : ""}`}
                onClick={() => setPalette(p.id)} style={{ padding: "6px 4px", flexDirection: "column", gap: 4, fontSize: 9 }}>
                <div style={{ display: "flex", gap: 2 }}>
                  {p.colors.map((c, i) => <span key={i} style={{ width: 10, height: 10, borderRadius: 2, background: c, boxShadow: `0 0 4px ${c}` }} />)}
                </div>
                {p.label}
              </button>
            ))}
          </div>

          <div className="cap" style={{ marginTop: 4 }}>Chase / Pattern</div>
          <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 4 }}>
            {CHASES.map(c => (
              <button key={c.id} className={`chip ${chase === c.id ? "active" : ""}`}
                onClick={() => setChase(c.id)} style={{ padding: "5px 2px", fontSize: 8.5 }}>{c.label}</button>
            ))}
          </div>

          <div style={{ display: "grid", gridTemplateColumns: "auto 1fr 40px", gap: 6, alignItems: "center", marginTop: 4 }}>
            <span className="cap" style={{ fontSize: 8 }}>BPM</span>
            <HSlider value={bpm} onChange={v => setBpm(Math.round(v))} min={60} max={200} />
            <span style={{ fontFamily: "var(--font-mono)", fontSize: 10, color: "var(--led-cyan)", textAlign: "right" }}>{bpm}</span>
          </div>
          <div style={{ display: "grid", gridTemplateColumns: "auto 1fr 40px", gap: 6, alignItems: "center" }}>
            <span className="cap" style={{ fontSize: 8 }}>Master</span>
            <HSlider value={master} onChange={setMaster} min={0} max={1} />
            <span style={{ fontFamily: "var(--font-mono)", fontSize: 10, color: "var(--accent)", textAlign: "right" }}>{(master * 100) | 0}%</span>
          </div>

          <div className="cap" style={{ marginTop: 6, display: "flex", justifyContent: "space-between" }}>
            <span>Rig ({fixtures.length})</span><span style={{ color: "var(--led-cyan)" }}>{totalCh} ch</span>
          </div>
          <div style={{ display: "flex", flexDirection: "column", gap: 3, maxHeight: 200, overflowY: "auto" }}>
            {fixtures.map(f => (
              <div key={f.id} onClick={() => setSelId(f.id)}
                style={{ display: "grid", gridTemplateColumns: "16px 1fr auto auto", gap: 6, alignItems: "center",
                  padding: "4px 6px", borderRadius: 4, cursor: "pointer",
                  background: f.id === selId ? "linear-gradient(90deg,rgba(255,122,26,0.22),transparent)" : "linear-gradient(180deg,var(--panel-hi),var(--panel))",
                  border: f.id === selId ? "1px solid var(--accent)" : "1px solid rgba(255,255,255,0.04)" }}>
                <span style={{ fontSize: 12, textAlign: "center" }}>{FIXTURE_TYPES[f.type].icon}</span>
                <span style={{ fontFamily: "var(--font-mono)", fontSize: 9.5, color: f.id === selId ? "var(--ink)" : "var(--ink-dim)",
                  whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{f.name}</span>
                <span style={{ fontFamily: "var(--font-mono)", fontSize: 8, color: "var(--ink-faint)" }}>U{f.universe}·{f.startCh}</span>
                <button className="nbtn compact danger" style={{ padding: "1px 4px", fontSize: 8 }}
                  onClick={e => { e.stopPropagation(); removeFixture(f.id); }}>×</button>
              </div>
            ))}
          </div>

          <div className="cap" style={{ marginTop: 6 }}>Art-Net Output</div>
          <div style={{ display: "grid", gridTemplateColumns: "auto 1fr", gap: 6, alignItems: "center" }}>
            <span className="cap" style={{ fontSize: 8 }}>Node IP</span>
            <input value={artnetIP} onChange={e => setArtnetIP(e.target.value)}
              style={{ padding: "4px 6px", borderRadius: 4, border: "none", outline: "none",
                background: "linear-gradient(180deg,#0a0a0c,#14141a)", color: "var(--led-green)",
                fontFamily: "var(--font-mono)", fontSize: 10, boxShadow: "var(--nshadow-in-sm)" }} />
          </div>
          <div style={{ display: "flex", alignItems: "center", gap: 6, padding: "5px 7px",
            background: "rgba(124,255,79,0.06)", border: "1px solid rgba(124,255,79,0.2)", borderRadius: 6 }}>
            <Led on color="green" size={6} />
            <span style={{ fontFamily: "var(--font-mono)", fontSize: 9, color: "var(--led-green)" }}>
              {universes.length} universe{universes.length === 1 ? "" : "s"} · broadcasting
            </span>
          </div>
        </div>

        {/* CENTER: live stage */}
        <div style={{ position: "relative", background: "#000" }}>
          <canvas ref={canvasRef} onMouseDown={onDown} onMouseMove={onMove} onMouseUp={onUp} onMouseLeave={onUp}
            style={{ display: "block", width: "100%", height: "100%", cursor: dragRef.current ? "grabbing" : "grab" }} />
          <div className="screen-corner" style={{ top: 10, left: 14 }}>
            ● LIGHTING · {fixtures.length} fixtures · {totalCh} DMX ch · {palette.toUpperCase()}
          </div>
          <div className="screen-corner" style={{ top: 10, right: 14 }}>
            {blackout ? "■ BLACKOUT" : "● LIVE · " + chase.toUpperCase()}
          </div>
          <div className="screen-corner" style={{ bottom: 10, left: 14 }}>
            Drag fixtures to position · audio-reactive in real time
          </div>
          <DmxMonitor dmxRef={dmxRef} universe={monitorUniverse} onUniverse={setMonitorUniverse} universes={universes} />
        </div>

        {/* RIGHT: fixture inspector */}
        <div style={{ padding: 10, background: "linear-gradient(180deg,#14141a,#0a0a0c)",
          borderLeft: "1px solid rgba(255,255,255,0.04)", display: "flex", flexDirection: "column", gap: 8, overflowY: "auto" }}>
          {!sel ? (
            <div style={{ padding: 16, textAlign: "center", color: "var(--ink-faint)", fontSize: 11, lineHeight: 1.6 }}>
              <div className="cap" style={{ marginBottom: 10 }}>Fixture Inspector</div>
              <p>Click a fixture to map it to the music.</p>
              <p style={{ fontSize: 10, marginTop: 8, opacity: 0.8 }}>
                Every light reacts to live audio — pick which frequency band drives intensity, color and movement.
              </p>
            </div>
          ) : (
            <>
              <div className="dock-section" style={{ padding: 8 }}>
                <input value={sel.name} onChange={e => updSel({ name: e.target.value })}
                  style={{ width: "100%", padding: "6px 8px", background: "linear-gradient(180deg,#0a0a0c,#14141a)",
                    border: "none", borderRadius: 4, outline: "none", color: "var(--accent)",
                    fontFamily: "var(--font-display)", fontSize: 12, fontWeight: 700, boxShadow: "var(--nshadow-in-sm)" }} />
                <div style={{ fontFamily: "var(--font-mono)", fontSize: 9, color: "var(--ink-faint)", marginTop: 4 }}>
                  {FIXTURE_TYPES[sel.type].label} · {FIXTURE_TYPES[sel.type].ch} ch
                </div>
              </div>

              <div className="dock-section" style={{ padding: 8 }}>
                <div className="dock-label">DMX Patch</div>
                <div style={{ display: "grid", gridTemplateColumns: "auto 1fr auto 1fr", gap: 6, alignItems: "center", marginTop: 6 }}>
                  <span className="cap" style={{ fontSize: 8 }}>Univ</span>
                  <input type="number" min="1" value={sel.universe} onChange={e => updSel({ universe: Number(e.target.value) })} style={lInp} />
                  <span className="cap" style={{ fontSize: 8 }}>Ch</span>
                  <input type="number" min="1" max="512" value={sel.startCh} onChange={e => updSel({ startCh: Number(e.target.value) })} style={lInp} />
                </div>
              </div>

              <div className="dock-section" style={{ padding: 8 }}>
                <div className="dock-label">Intensity ← Audio</div>
                <BandPick value={sel.dimBand} onChange={v => updSel({ dimBand: v })} />
                <LSI label="React" value={sel.react} onChange={v => updSel({ react: v })} min={0} max={2} step={0.01} />
                <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginTop: 6 }}>
                  <span className="cap" style={{ fontSize: 9 }}>Strobe on beat</span>
                  <Flip value={sel.strobeOnBeat} onChange={v => updSel({ strobeOnBeat: v })} />
                </div>
              </div>

              <div className="dock-section" style={{ padding: 8 }}>
                <div className="dock-label">Color</div>
                <div className="chips" style={{ padding: 2, marginTop: 4 }}>
                  {["palette", "static", "audio"].map(m => (
                    <button key={m} className={`chip ${sel.colorMode === m ? "active" : ""}`}
                      onClick={() => updSel({ colorMode: m })} style={{ padding: "3px 6px", fontSize: 9 }}>{m}</button>
                  ))}
                </div>
                {sel.colorMode === "static" && (
                  <input type="color" value={sel.staticColor} onChange={e => updSel({ staticColor: e.target.value })}
                    style={{ width: "100%", height: 26, marginTop: 6, border: "none", borderRadius: 6, background: "transparent", cursor: "pointer" }} />
                )}
              </div>

              {(FIXTURE_TYPES[sel.type].beam) && (
                <div className="dock-section" style={{ padding: 8 }}>
                  <div className="dock-label">Movement</div>
                  <div className="chips" style={{ padding: 2, marginTop: 4 }}>
                    {(sel.type === "laser" ? ["fan", "none"] : ["sweep", "none"]).map(m => (
                      <button key={m} className={`chip ${sel.movement === m ? "active" : ""}`}
                        onClick={() => updSel({ movement: m })} style={{ padding: "3px 6px", fontSize: 9 }}>{m}</button>
                    ))}
                  </div>
                  {sel.movement !== "none" && (<>
                    <div style={{ marginTop: 6 }}><span className="cap" style={{ fontSize: 8 }}>Speed ← band</span>
                      <BandPick value={sel.moveBand} onChange={v => updSel({ moveBand: v })} /></div>
                    <LSI label="Range" value={sel.rangeDeg} onChange={v => updSel({ rangeDeg: v })} min={10} max={170} step={1} unit="°" />
                  </>)}
                </div>
              )}

              <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 4 }}>
                <button className="nbtn compact" onClick={dupFixture}>Duplicate</button>
                <button className="nbtn compact danger" onClick={() => removeFixture(sel.id)}>Delete</button>
              </div>
            </>
          )}
        </div>
      </div>

      <div className="stage-strip" style={{ gap: 10, flexWrap: "wrap" }}>
        <span className="cap">CUES:</span>
        <div style={{ display: "flex", gap: 4 }}>
          {Array.from({ length: 8 }, (_, i) => {
            const s = scenes[i];
            const isActive = activeScene === i;
            return (
              <button key={i}
                onClick={() => s ? recallScene(i) : saveScene(i)}
                onContextMenu={(e) => { e.preventDefault(); saveScene(i); }}
                title={s ? `Recall ${s.name} · right-click to overwrite` : "Click to store current look"}
                style={{
                  width: 34, height: 30, borderRadius: 5, cursor: "pointer",
                  border: isActive ? "1px solid var(--accent)" : "1px solid rgba(255,255,255,0.08)",
                  background: s
                    ? (isActive ? "linear-gradient(180deg,var(--accent),#c8641a)" : "linear-gradient(180deg,var(--panel-hi),var(--panel))")
                    : "rgba(0,0,0,0.35)",
                  color: s ? (isActive ? "#1a0f04" : "var(--accent)") : "var(--ink-faint)",
                  fontFamily: "var(--font-mono)", fontSize: 11, fontWeight: 700,
                  boxShadow: isActive ? "0 0 10px var(--accent-glow)" : "var(--nshadow-out-sm)",
                }}>{i + 1}</button>
            );
          })}
        </div>
        <span style={{ fontSize: 8, color: "var(--ink-faint)" }}>click empty=store · right-click=overwrite</span>
        <div style={{ width: 1, height: 24, background: "rgba(255,255,255,0.1)" }} />
        <button className="nbtn compact" onClick={tap}
          style={{ minWidth: 70, fontWeight: 700, color: "var(--led-cyan)" }}>
          TAP · {bpm}
        </button>
        <button
          onMouseDown={() => setStrobe(true)} onMouseUp={() => setStrobe(false)} onMouseLeave={() => setStrobe(false)}
          onTouchStart={(e) => { e.preventDefault(); setStrobe(true); }} onTouchEnd={() => setStrobe(false)}
          className="nbtn compact"
          style={{ fontWeight: 700, color: "#eaf2ff" }}>⚡ STROBE</button>
        <button
          onMouseDown={() => setFlash(true)} onMouseUp={() => setFlash(false)} onMouseLeave={() => setFlash(false)}
          onTouchStart={(e) => { e.preventDefault(); setFlash(true); }} onTouchEnd={() => setFlash(false)}
          className={`nbtn compact ${flashOn ? "glow pressed" : ""}`}
          style={{ fontWeight: 700, color: "#fff" }}>☀ FLASH</button>
        <div style={{ flex: 1 }} />
        <span className="cap" style={{ color: "var(--ink-faint)" }}>
          Hold STROBE/FLASH momentary · {chase} · Art-Net → {artnetIP}
        </span>
      </div>
    </div>
  );
}

/* fixture body glyph */
function drawBody(ctx, px, py, def, col, sel) {
  ctx.save();
  ctx.fillStyle = "#16161c";
  ctx.strokeStyle = sel ? "#ffae3a" : "rgba(255,255,255,0.25)";
  ctx.lineWidth = sel ? 2 : 1;
  ctx.beginPath(); ctx.arc(px, py, 7, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
  ctx.fillStyle = col;
  ctx.beginPath(); ctx.arc(px, py, 3, 0, Math.PI * 2); ctx.fill();
  if (sel) { ctx.strokeStyle = "rgba(255,174,58,0.6)"; ctx.beginPath(); ctx.arc(px, py, 12, 0, Math.PI * 2); ctx.stroke(); }
  ctx.restore();
}

function BandPick({ value, onChange }) {
  return (
    <div className="chips" style={{ padding: 2, marginTop: 4 }}>
      {["bass", "mid", "high", "energy"].map(b => (
        <button key={b} className={`chip ${value === b ? "active" : ""}`}
          onClick={() => onChange(b)} style={{ padding: "3px 5px", fontSize: 8.5, flex: 1 }}>{b}</button>
      ))}
    </div>
  );
}

function LSI({ 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)", textAlign: "right" }}>
        {step >= 1 ? value.toFixed(0) : value.toFixed(2)}{unit}
      </span>
    </div>
  );
}

/* live DMX channel monitor (first 48 ch of selected universe) */
function DmxMonitor({ dmxRef, universe, onUniverse, universes }) {
  const ref = useRefL(null);
  useEffectL(() => {
    let raf;
    const draw = () => {
      const cnv = ref.current; if (!cnv) { raf = requestAnimationFrame(draw); return; }
      const ctx = cnv.getContext("2d");
      const W = cnv.width = 232, H = cnv.height = 60;
      ctx.clearRect(0, 0, W, H);
      const dmx = dmxRef.current;
      const N = 48, bw = W / N;
      for (let i = 0; i < N; i++) {
        const v = dmx[i] / 255;
        ctx.fillStyle = `hsl(${140 - v * 140}, 90%, ${20 + v * 35}%)`;
        ctx.fillRect(i * bw, H - v * H, bw - 1, v * H);
      }
      raf = requestAnimationFrame(draw);
    };
    raf = requestAnimationFrame(draw);
    return () => cancelAnimationFrame(raf);
  }, []);
  return (
    <div style={{ position: "absolute", bottom: 34, right: 14, width: 232,
      background: "rgba(0,0,0,0.6)", borderRadius: 6, padding: 6, boxShadow: "var(--nshadow-out-sm)" }}>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 4 }}>
        <span className="cap" style={{ fontSize: 8 }}>DMX · Universe {universe} · ch 1-48</span>
        <select value={universe} onChange={e => onUniverse(Number(e.target.value))}
          style={{ background: "#0a0a0c", color: "var(--led-cyan)", border: "none", borderRadius: 3,
            fontFamily: "var(--font-mono)", fontSize: 9, outline: "none" }}>
          {(universes.length ? universes : [1]).map(u => <option key={u} value={u}>U{u}</option>)}
        </select>
      </div>
      <canvas ref={ref} style={{ width: "100%", height: 60, display: "block", borderRadius: 3 }} />
    </div>
  );
}

function hslToHex(hsl) {
  const m = hsl.match(/hsl\(([\d.]+),\s*([\d.]+)%,\s*([\d.]+)%\)/);
  if (!m) return "#ffffff";
  let [h, s, l] = [parseFloat(m[1]), parseFloat(m[2]) / 100, parseFloat(m[3]) / 100];
  const a = s * Math.min(l, 1 - l);
  const f = n => { const k = (n + h / 30) % 12; const c = l - a * Math.max(-1, Math.min(k - 3, 9 - k, 1)); return Math.round(255 * c).toString(16).padStart(2, "0"); };
  return `#${f(0)}${f(8)}${f(4)}`;
}

window.LightingWorkspace = LightingWorkspace;
