/* global React */
/*
 * DemixViz — the brand's centerpiece instrument.
 * A synthetic blood-derived cohort rendered in a 2-D projection of the verified
 * spectral-covariance state space. At t=0 it reads as ONE diffuse, high-entropy
 * cohort (what an ITT analysis "sees"); as t→1 the registered de-mixing workflow
 * resolves it into coarse molecular-state classes, each enclosed by its own
 * eigenvalue-derived spectral ellipsoid. A few records abstain.
 *
 * Everything here is illustrative synthetic data — never trial data.
 */
const DV_PALETTE = {
  cyan: "#34e0d8",
  blue: "#5aa9ff",
  purple: "#a78bfa",
  amber: "#ffb454",
  green: "#5ad19b",
  muted: "#6b7e9c",
  mutedDim: "#4a5a76",
  line: "#1e2c42",
  ink: "#c4d0e0",
};
const DV_CLASS_COLORS = [DV_PALETTE.cyan, DV_PALETTE.blue, DV_PALETTE.purple];
const DV_CENTROIDS = [
  [-0.46, 0.16], // Class A — responder-enriched
  [0.5, 0.34],   // Class B
  [0.08, -0.5],  // Class C
];

function dvHexToRgb(h) {
  const n = parseInt(h.slice(1), 16);
  return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
}
function dvMix(a, b, t) {
  const ca = dvHexToRgb(a), cb = dvHexToRgb(b);
  return `rgb(${Math.round(ca[0] + (cb[0] - ca[0]) * t)},${Math.round(ca[1] + (cb[1] - ca[1]) * t)},${Math.round(ca[2] + (cb[2] - ca[2]) * t)})`;
}
function dvRand(seedObj) {
  // mulberry32
  let s = seedObj.s;
  s |= 0; s = (s + 0x6d2b79f5) | 0;
  let r = Math.imul(s ^ (s >>> 15), 1 | s);
  r ^= r + Math.imul(r ^ (r >>> 7), 61 | r);
  seedObj.s = s;
  return ((r ^ (r >>> 14)) >>> 0) / 4294967296;
}
function dvGauss(seedObj, sd) {
  const u = Math.max(1e-6, dvRand(seedObj)), v = dvRand(seedObj);
  return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v) * sd;
}

function dvBuildPoints(seedInt) {
  const seedObj = { s: seedInt };
  const N = 300;
  const pts = [];
  const weights = [0.36, 0.34, 0.3]; // responder class slightly larger
  for (let i = 0; i < N; i++) {
    const r = dvRand(seedObj);
    let cls = 0; let acc = 0;
    for (let k = 0; k < 3; k++) { acc += weights[k]; if (r <= acc) { cls = k; break; } }
    const c = DV_CENTROIDS[cls];
    // de-mixed: tight gaussian around centroid (anisotropic for organic look)
    const ax = dvGauss(seedObj, 0.15), ay = dvGauss(seedObj, 0.12);
    const demixed = [c[0] + ax, c[1] + ay];
    // mixed: collapse toward origin, retaining a faint memory of direction
    const mx = dvGauss(seedObj, 0.4), my = dvGauss(seedObj, 0.4);
    const mixed = [c[0] * 0.14 + mx, c[1] * 0.14 + my];
    pts.push({ cls, mixed, demixed, cur: [0, 0], abstain: false, ph: dvRand(seedObj) * Math.PI * 2 });
  }
  // flag ~6% as abstain — records that sit between classes (no confident routing)
  const nAb = Math.round(N * 0.06);
  for (let j = 0; j < nAb; j++) {
    const p = pts[Math.floor(dvRand(seedObj) * N)];
    const a = Math.floor(dvRand(seedObj) * 3); let b = (a + 1 + Math.floor(dvRand(seedObj) * 2)) % 3;
    p.abstain = true;
    p.demixed = [(DV_CENTROIDS[a][0] + DV_CENTROIDS[b][0]) / 2 + dvGauss(seedObj, 0.07),
                 (DV_CENTROIDS[a][1] + DV_CENTROIDS[b][1]) / 2 + dvGauss(seedObj, 0.07)];
  }
  // kNN local-panel edges computed in feature (de-mixed) space — "local panel" rule
  for (const p of pts) {
    const d = pts.map((q, idx) => [idx, (q.demixed[0] - p.demixed[0]) ** 2 + (q.demixed[1] - p.demixed[1]) ** 2]);
    d.sort((u, v) => u[1] - v[1]);
    p.nbr = d.slice(1, 4).map((x) => x[0]);
  }
  return pts;
}

// 2x2 covariance ellipse from a set of [x,y]
function dvCovEllipse(coords) {
  const n = coords.length; if (n < 3) return null;
  let mx = 0, my = 0;
  for (const c of coords) { mx += c[0]; my += c[1]; }
  mx /= n; my /= n;
  let sxx = 0, syy = 0, sxy = 0;
  for (const c of coords) { const dx = c[0] - mx, dy = c[1] - my; sxx += dx * dx; syy += dy * dy; sxy += dx * dy; }
  sxx /= n; syy /= n; sxy /= n;
  const tr = sxx + syy, det = sxx * syy - sxy * sxy;
  const disc = Math.sqrt(Math.max(0, (tr / 2) * (tr / 2) - det));
  const l1 = tr / 2 + disc, l2 = Math.max(1e-6, tr / 2 - disc);
  const angle = Math.abs(sxy) < 1e-9 ? (sxx >= syy ? 0 : Math.PI / 2) : Math.atan2(l1 - sxx, sxy);
  return { mx, my, rx: Math.sqrt(l1), ry: Math.sqrt(l2), angle, l1, l2 };
}

function DemixViz({ vizStyle = "ellipsoid", target = null, auto = true, showAbstain = true, showSpecificity = false, height = 360, seed = 7, onReadout, accent = 1 }) {
  const canvasRef = React.useRef(null);
  const wrapRef = React.useRef(null);
  const stateRef = React.useRef({ pts: dvBuildPoints(seed), t: 0, target: 0, raf: 0, lastReadout: 0 });
  const propsRef = React.useRef({});
  propsRef.current = { vizStyle, target, auto, showAbstain, showSpecificity, onReadout, accent };

  // rebuild points when seed changes
  React.useEffect(() => { stateRef.current.pts = dvBuildPoints(seed); }, [seed]);

  React.useEffect(() => {
    const canvas = canvasRef.current, wrap = wrapRef.current;
    if (!canvas || !wrap) return;
    const ctx = canvas.getContext("2d");
    let W = 0, H = 0, dpr = Math.min(2, window.devicePixelRatio || 1);
    const resize = () => {
      W = wrap.clientWidth; H = height;
      canvas.width = W * dpr; canvas.height = H * dpr;
      canvas.style.width = W + "px"; canvas.style.height = H + "px";
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    };
    resize();
    const ro = new ResizeObserver(resize); ro.observe(wrap);

    const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
    const st = stateRef.current;
    let startTime = performance.now();
    let autoPhase = reduce ? "hold" : "delay";

    const toPx = (nx, ny) => {
      const pad = 46;
      const cx = W / 2, cy = H / 2;
      const sx = (W - pad * 2) / 2.6, sy = (H - pad * 2) / 2.6;
      const s = Math.min(sx, sy);
      return [cx + nx * s, cy + ny * s, s];
    };

    const draw = (now) => {
     try {
      const P = propsRef.current;
      // ---- advance t ----
      if (P.target != null) {
        st.target = P.target;
      } else if (P.auto) {
        const elapsed = now - startTime;
        if (autoPhase === "delay" && elapsed > 650) { autoPhase = "demix"; startTime = now; }
        else if (autoPhase === "demix") { st.target = 1; }
        else if (autoPhase === "hold") { st.target = 1; }
      }
      // ease current toward target (snappier when a control owns the target)
      const k = reduce ? 1 : (P.target != null ? 0.28 : 0.06);
      st.t += (st.target - st.t) * k;
      if (P.target != null && reduce) st.t = st.target;
      const t = st.t;
      const ease = t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;

      // ---- update positions ----
      const breathe = reduce ? 0 : 0.012;
      for (const p of st.pts) {
        const bx = Math.sin(now / 1400 + p.ph) * breathe;
        const by = Math.cos(now / 1600 + p.ph) * breathe;
        p.cur[0] = p.mixed[0] + (p.demixed[0] - p.mixed[0]) * ease + bx;
        p.cur[1] = p.mixed[1] + (p.demixed[1] - p.mixed[1]) * ease + by;
      }

      ctx.clearRect(0, 0, W, H);

      // ---- background grid (faint instrument field) ----
      ctx.strokeStyle = "rgba(30,44,66,0.5)"; ctx.lineWidth = 1;
      const gstep = 44;
      for (let x = (W / 2) % gstep; x < W; x += gstep) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, H); ctx.stroke(); }
      for (let y = (H / 2) % gstep; y < H; y += gstep) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke(); }

      // ---- healthy-control reference marker ----
      const hc = toPx(0.78, 0.62);
      ctx.strokeStyle = "rgba(90,209,155,0.6)"; ctx.lineWidth = 1.4;
      ctx.beginPath(); ctx.arc(hc[0], hc[1], 7, 0, Math.PI * 2); ctx.stroke();
      ctx.beginPath(); ctx.moveTo(hc[0] - 11, hc[1]); ctx.lineTo(hc[0] + 11, hc[1]); ctx.moveTo(hc[0], hc[1] - 11); ctx.lineTo(hc[0], hc[1] + 11); ctx.stroke();
      ctx.font = "10px 'JetBrains Mono', monospace"; ctx.fillStyle = "rgba(90,209,155,0.85)"; ctx.textAlign = "left";
      ctx.fillText("HC ref", hc[0] + 14, hc[1] + 3);

      // ---- constellation edges (kNN local panels) ----
      if (P.vizStyle === "constellation") {
        for (const p of st.pts) {
          const a = toPx(p.cur[0], p.cur[1]);
          for (const ni of p.nbr) {
            const q = st.pts[ni];
            const b = toPx(q.cur[0], q.cur[1]);
            const same = p.cls === q.cls;
            // cross-class edges fade out as we de-mix; same-class brighten
            const alpha = same ? (0.05 + 0.22 * ease) : 0.16 * (1 - ease);
            if (alpha < 0.012) continue;
            const col = same ? DV_CLASS_COLORS[p.cls] : DV_PALETTE.mutedDim;
            ctx.strokeStyle = col.startsWith("#") ? dvRgba(col, alpha) : col;
            ctx.lineWidth = same ? 1 : 0.8;
            ctx.beginPath(); ctx.moveTo(a[0], a[1]); ctx.lineTo(b[0], b[1]); ctx.stroke();
          }
        }
      }

      // ---- ellipses ----
      const drawEllipse = (e, color, alpha, lw) => {
        if (!e) return;
        const c = toPx(e.mx, e.my); const s = c[2];
        ctx.save();
        ctx.translate(c[0], c[1]); ctx.rotate(-e.angle);
        ctx.beginPath();
        ctx.ellipse(0, 0, Math.max(6, e.rx * s * 2.3), Math.max(6, e.ry * s * 2.3), 0, 0, Math.PI * 2);
        ctx.strokeStyle = dvRgba(color, alpha);
        ctx.lineWidth = lw; ctx.stroke();
        ctx.fillStyle = dvRgba(color, alpha * 0.10); ctx.fill();
        ctx.restore();
        return c;
      };

      if (P.vizStyle === "field") {
        // soft density field per class
        for (let k = 0; k < 3; k++) {
          const coords = st.pts.filter((p) => p.cls === k && !p.abstain).map((p) => p.cur);
          const e = dvCovEllipse(coords); if (!e) continue;
          const c = toPx(e.mx, e.my); const s = c[2];
          const rad = Math.max(e.rx, e.ry) * s * 2.6;
          const g = ctx.createRadialGradient(c[0], c[1], 2, c[0], c[1], rad);
          const al = 0.06 + 0.26 * ease;
          g.addColorStop(0, dvRgba(DV_CLASS_COLORS[k], al));
          g.addColorStop(1, dvRgba(DV_CLASS_COLORS[k], 0));
          ctx.fillStyle = g; ctx.beginPath(); ctx.arc(c[0], c[1], rad, 0, Math.PI * 2); ctx.fill();
        }
      }

      // single "apparent cohort" ellipse fades out; per-class fade in
      const allCoords = st.pts.filter((p) => !p.abstain).map((p) => p.cur);
      drawEllipse(dvCovEllipse(allCoords), DV_PALETTE.muted, 0.5 * (1 - ease), 1.4);

      const labels = [];
      if (P.vizStyle !== "field") {
        for (let k = 0; k < 3; k++) {
          const coords = st.pts.filter((p) => p.cls === k && !p.abstain).map((p) => p.cur);
          const e = dvCovEllipse(coords);
          const c = drawEllipse(e, DV_CLASS_COLORS[k], 0.7 * ease, 1.6);
          if (e && c && ease > 0.4) labels.push([c[0], c[1], "ABC"[k], k]);
        }
      }

      // ---- specificity comparator ghost (inflammatory baseline) ----
      if (P.showSpecificity) {
        const sc = toPx(-0.05, 0.78);
        const pulse = 0.5 + 0.5 * Math.sin(now / 600);
        ctx.setLineDash([4, 4]);
        ctx.strokeStyle = dvRgba(DV_PALETTE.amber, 0.35 + 0.2 * pulse);
        ctx.lineWidth = 1.4;
        ctx.beginPath(); ctx.ellipse(sc[0], sc[1], 60, 30, -0.3, 0, Math.PI * 2); ctx.stroke();
        ctx.setLineDash([]);
        ctx.font = "10px 'JetBrains Mono', monospace"; ctx.fillStyle = dvRgba(DV_PALETTE.amber, 0.85); ctx.textAlign = "center";
        ctx.fillText("inflammatory comparator", sc[0], sc[1] + 46);
      }

      // ---- points ----
      for (const p of st.pts) {
        const a = toPx(p.cur[0], p.cur[1]);
        if (p.abstain) {
          if (!P.showAbstain) continue;
          const amb = dvMix(DV_PALETTE.muted, DV_PALETTE.amber, ease);
          ctx.beginPath(); ctx.arc(a[0], a[1], 3, 0, Math.PI * 2);
          ctx.fillStyle = dvRgba(DV_PALETTE.line, 0.9); ctx.fill();
          ctx.lineWidth = 1.3; ctx.strokeStyle = amb; ctx.stroke();
          continue;
        }
        const col = dvMix(DV_PALETTE.muted, DV_CLASS_COLORS[p.cls], ease);
        ctx.beginPath(); ctx.arc(a[0], a[1], 2.6, 0, Math.PI * 2);
        ctx.fillStyle = col; ctx.fill();
        // responder class gets a faint halo when resolved
        if (p.cls === 0 && ease > 0.5) {
          ctx.beginPath(); ctx.arc(a[0], a[1], 5, 0, Math.PI * 2);
          ctx.strokeStyle = dvRgba(DV_CLASS_COLORS[0], 0.12 * ease); ctx.lineWidth = 1; ctx.stroke();
        }
      }

      // class labels
      ctx.font = "600 11px 'JetBrains Mono', monospace"; ctx.textAlign = "center";
      for (const [lx, ly, lab, k] of labels) {
        ctx.fillStyle = dvRgba(DV_CLASS_COLORS[k], Math.min(1, (ease - 0.4) * 2));
        ctx.fillText(lab, lx, ly + 4);
      }

      // ---- readouts ----
      if (P.onReadout && now - st.lastReadout > 90) {
        st.lastReadout = now;
        const classes = 1 + Math.round(ease * 2);
        const entropy = (0.691 * (1 - ease) + 0.302 * ease);
        const effect = (0.31 + ease * 0.42);
        const nAb = st.pts.filter((p) => p.abstain).length;
        P.onReadout({ t: ease, classes, entropy, effect, abstain: nAb, demixed: ease > 0.92 });
      }

     } catch (err) {
      if (!window.__dverr) { window.__dverr = String(err && err.stack || err); console.error("DemixViz draw error:", err); }
     }
    };
    // Timer-driven so the canvas always paints (rAF is paused when the iframe
    // isn't foregrounded). 30fps is plenty for this; easing is time-aware.
    draw(performance.now());
    const iv = setInterval(() => draw(performance.now()), 33);
    return () => { clearInterval(iv); ro.disconnect(); };
  }, [height]);

  return (
    <div ref={wrapRef} style={{ width: "100%", position: "relative" }}>
      <canvas ref={canvasRef} style={{ display: "block", width: "100%", borderRadius: "var(--radius-lg)" }} />
    </div>
  );
}

function dvRgba(hex, a) {
  const [r, g, b] = dvHexToRgb(hex);
  return `rgba(${r},${g},${b},${a})`;
}

window.DemixViz = DemixViz;
window.DV_PALETTE = DV_PALETTE;
window.DV_CLASS_COLORS = DV_CLASS_COLORS;
