/* scope-screen.jsx — shared oscilloscope screen renderer.
   Used by scope.html (the primer) and anywhere else a trace is needed.
   <ScopeScreen signal={f(tMs)->volts} vDiv tDiv /> draws a 10×8-division
   phosphor screen. Knob math: height = divisions × V/div, period =
   divisions × t/div — the whole skill the primer teaches. */

function ScopeScreen({ signal, vDiv = 2, tDiv = 100, label, mini }) {
  const W = 520, H = 280;                 // 10 × 8 divisions
  const divW = W / 10, divH = H / 8;
  const span = tDiv * 10;                 // ms across the screen
  const N = 280;
  let pts = "";
  for (let i = 0; i <= N; i++) {
    const t = (i / N) * span;
    const v = signal(t);
    const y = H / 2 - (v / vDiv) * divH;
    pts += `${((i / N) * W).toFixed(1)},${Math.max(3, Math.min(H - 3, y)).toFixed(1)} `;
  }
  return (
    <div style={{ background: "#0c1d14", border: "3px solid var(--ink)", borderRadius: 10,
                  padding: "10px 10px 6px" }}>
      <svg viewBox={`0 0 ${W} ${H}`} width="100%" style={{ display: "block" }}>
        {Array.from({ length: 11 }, (_, i) => (
          <line key={"v" + i} x1={i * divW} y1="0" x2={i * divW} y2={H}
                stroke="#2a4636" strokeWidth={i === 5 ? 1.6 : 0.8} />
        ))}
        {Array.from({ length: 9 }, (_, i) => (
          <line key={"h" + i} x1="0" y1={i * divH} x2={W} y2={i * divH}
                stroke="#2a4636" strokeWidth={i === 4 ? 1.6 : 0.8} />
        ))}
        {Array.from({ length: 50 }, (_, i) => (
          <line key={"t" + i} x1={i * (W / 50)} y1={H / 2 - 3} x2={i * (W / 50)} y2={H / 2 + 3}
                stroke="#2a4636" strokeWidth="0.8" />
        ))}
        <polyline points={pts} fill="none" stroke="#7dff9a" strokeWidth="4.5" opacity="0.25" />
        <polyline points={pts} fill="none" stroke="#7dff9a" strokeWidth="2" strokeLinejoin="round" />
        <text x="6" y={H / 2 - 6} fontFamily="IBM Plex Mono, monospace" fontSize="11" fill="#5f8f72">0 V</text>
      </svg>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline",
                    fontFamily: "IBM Plex Mono, monospace", fontSize: mini ? 10.5 : 12,
                    color: "#7fae90", padding: "6px 4px 2px", gap: 8, flexWrap: "wrap" }}>
        <span>{vDiv} V/div · {tDiv} ms/div</span>
        {label && <span style={{ color: "#a8d4b6", textAlign: "right" }}>{label}</span>}
      </div>
    </div>
  );
}

/* a few ready-made signals (t in ms) */
const SCOPE_SIGS = {
  dc: (level) => () => level,
  sine: (amp, hz, off = 0) => (t) => off + amp * Math.sin(2 * Math.PI * hz * t / 1000),
  clippedSine: (amp, hz, rail) => (t) => Math.max(-rail, Math.min(rail, amp * Math.sin(2 * Math.PI * hz * t / 1000))),
  square: (lo, hi, periodMs) => (t) => (t % periodMs) < periodMs / 2 ? hi : lo,
  halfWave: (amp, hz) => (t) => Math.max(0, amp * Math.sin(2 * Math.PI * hz * t / 1000)),
  ripple: (level, amp, periodMs) => (t) => {
    const p = (t % periodMs) / periodMs;
    return level + amp * (p < 0.15 ? 1 - p / 0.15 * 2 : -1 + (p - 0.15) / 0.85 * 2 * 0.5 - 0 + (p - 0.15) / 0.85 * 1.0);
  },
  rcCharge: (target, tauMs) => (t) => target * (1 - Math.exp(-t / tauMs)),
  noise: (amp) => (t) => amp * (Math.sin(t * 7.3) * 0.4 + Math.sin(t * 23.7) * 0.35 + Math.sin(t * 61.1) * 0.25),
  spike: (run, kick) => (t) => {
    const cyc = t % 500;
    if (cyc < 300) return run;
    if (cyc < 312) return kick * Math.exp(-(cyc - 300) / 5);
    return 0;
  },
};

Object.assign(window, { ScopeScreen, SCOPE_SIGS });
