// arcsec-lib.jsx — shared primitives for the arcsec hero directions
// Exports (to window): useRaf, clamp, lerp, easeInOut, formatAngle,
//   readoutFromQ, halfFromQ, AngleDiagram, Readout, UnitChain, StarField
const { useRef, useEffect, useState, useCallback, useMemo } = React;

// ── math ───────────────────────────────────────────────────────────
const clamp = (v, a, b) => Math.min(b, Math.max(a, v));
const lerp = (a, b, t) => a + (b - a) * t;
const easeInOut = (t) => (t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2);

// requestAnimationFrame loop. cb(elapsedMs, absMs). Pauses when active=false.
function useRaf(cb, active = true) {
  const cbRef = useRef(cb); cbRef.current = cb;
  useEffect(() => {
    if (!active) return;
    let id, start = null;
    const loop = (t) => { if (start == null) start = t; cbRef.current(t - start, t); id = requestAnimationFrame(loop); };
    id = requestAnimationFrame(loop);
    return () => cancelAnimationFrame(id);
  }, [active]);
}

// ── the arcsecond chain ─────────────────────────────────────────────
// q in [0,1]: 0 = pinched to ~1 arcsecond, 1 = wide open (~32°).
// halfFromQ drives the DRAWN wedge (stays a legible sliver at the bottom);
// readoutFromQ drives the NUMBER, which dives log-uniformly into arcseconds.
// The decoupling is the honest "magnitude exaggerated" trick — a real 1″
// wedge would be invisible.
const Q_HALF_MIN = 0.55;   // smallest drawn half-angle (deg)
const Q_HALF_MAX = 16;     // widest drawn half-angle (deg)
const READ_MIN = 1 / 3600; // 1 arcsecond, in degrees
const READ_MAX = 32;       // widest readout, in degrees
const halfFromQ = (q) => lerp(Q_HALF_MIN, Q_HALF_MAX, clamp(q, 0, 1));
const qFromHalf = (h) => clamp((h - Q_HALF_MIN) / (Q_HALF_MAX - Q_HALF_MIN), 0, 1);
const readoutFromQ = (q) => Math.exp(lerp(Math.log(READ_MIN), Math.log(READ_MAX), clamp(q, 0, 1)));

// Format an angle (in DEGREES) into the most natural unit of the chain.
function formatAngle(deg) {
  const arcmin = deg * 60, arcsec = deg * 3600;
  if (deg >= 1) return { num: deg.toFixed(deg >= 10 ? 1 : 2), unit: '\u00b0', key: 'deg', name: 'degrees' };
  if (arcmin >= 1) return { num: arcmin.toFixed(arcmin >= 10 ? 1 : 2), unit: '\u2032', key: 'min', name: 'arcminutes' };
  return { num: arcsec.toFixed(arcsec >= 10 ? 1 : arcsec >= 1 ? 2 : 3), unit: '\u2033', key: 'sec', name: 'arcseconds' };
}

// ── the angle instrument ────────────────────────────────────────────
// Two rays symmetric about a horizontal axis from a vertex on the left.
// Pure SVG, sized by viewBox so pointer mapping is resolution-independent.
const DIAG = { W: 580, H: 460, vx: 70, L: 452, arcR: 104 };

function AngleDiagram({
  halfDeg, accent, faint = 'rgba(255,255,255,0.10)',
  handles = false, protractor = false, fingers = false, sweepDeg = null,
  svgRef, onPointer, style,
}) {
  const { W, H, vx, L, arcR } = DIAG;
  const vy = H / 2;
  const th = (halfDeg * Math.PI) / 180;
  const c = Math.cos(th), s = Math.sin(th);
  const topEnd = [vx + L * c, vy - L * s];
  const botEnd = [vx + L * c, vy + L * s];
  const arcTop = [vx + arcR * c, vy - arcR * s];
  const arcBot = [vx + arcR * c, vy + arcR * s];
  const arc = `M ${arcTop[0].toFixed(2)} ${arcTop[1].toFixed(2)} A ${arcR} ${arcR} 0 0 1 ${arcBot[0].toFixed(2)} ${arcBot[1].toFixed(2)}`;
  const gid = useMemo(() => 'g' + Math.random().toString(36).slice(2, 8), []);

  // protractor tick ring
  const ticks = [];
  if (protractor) {
    for (let d = -30; d <= 30; d += 5) {
      const a = (d * Math.PI) / 180, big = d % 15 === 0;
      const r0 = L * 0.86, r1 = L * 0.86 + (big ? 16 : 9);
      ticks.push(
        <line key={d} x1={vx + r0 * Math.cos(a)} y1={vy + r0 * Math.sin(a)}
          x2={vx + r1 * Math.cos(a)} y2={vy + r1 * Math.sin(a)}
          stroke={faint} strokeWidth={big ? 1.4 : 1} />
      );
    }
  }

  // capsule fingertips that pinch from outside each ray
  let fingerEls = null;
  if (fingers) {
    const dt = L * 0.55, gap = 15 + halfDeg * 0.7, fw = 88, fh = 30;
    const mk = (sign) => {
      const a = sign * th; // screen angle of this ray
      const bx = vx + dt * Math.cos(a), by = vy + dt * Math.sin(a);
      // offset outward (perpendicular, away from axis)
      const ox = Math.sin(a) * gap * (sign > 0 ? 1 : 1);
      const px = bx - Math.sin(a) * gap * sign, py = by + Math.cos(a) * gap * sign;
      const deg = (a * 180) / Math.PI;
      return (
        <g key={sign} transform={`translate(${px.toFixed(2)} ${py.toFixed(2)}) rotate(${deg.toFixed(2)})`}>
          <rect x={-fw / 2} y={-fh / 2} width={fw} height={fh} rx={fh / 2}
            fill="#16161c" stroke={accent} strokeWidth="1.6" />
          <circle cx={fw / 2 - fh / 2} cy="0" r="4" fill={accent} fillOpacity="0.5" />
        </g>
      );
    };
    fingerEls = <g>{mk(-1)}{mk(1)}</g>;
  }

  return (
    <svg ref={svgRef} viewBox={`0 0 ${W} ${H}`} width="100%" height="100%"
      style={{ display: 'block', touchAction: 'none', ...style }}
      onPointerDown={onPointer} onPointerMove={onPointer} onPointerUp={onPointer} onPointerCancel={onPointer}>
      <defs>
        <radialGradient id={gid + 'glow'} cx={(vx / W) * 100 + '%'} cy="50%" r="62%">
          <stop offset="0%" stopColor={accent} stopOpacity="0.16" />
          <stop offset="55%" stopColor={accent} stopOpacity="0.03" />
          <stop offset="100%" stopColor={accent} stopOpacity="0" />
        </radialGradient>
        <linearGradient id={gid + 'wedge'} x1="0" y1="0" x2="1" y2="0">
          <stop offset="0%" stopColor={accent} stopOpacity="0.20" />
          <stop offset="100%" stopColor={accent} stopOpacity="0.02" />
        </linearGradient>
      </defs>

      <rect x="0" y="0" width={W} height={H} fill={`url(#${gid}glow)`} />

      {/* bisector / sight-line axis */}
      <line x1={vx} y1={vy} x2={vx + L} y2={vy} stroke={faint} strokeWidth="1" strokeDasharray="2 7" />
      {ticks}

      {/* the wedge */}
      <path d={`M ${vx} ${vy} L ${topEnd[0].toFixed(2)} ${topEnd[1].toFixed(2)} L ${botEnd[0].toFixed(2)} ${botEnd[1].toFixed(2)} Z`}
        fill={`url(#${gid}wedge)`} />

      {/* the two rays */}
      <line x1={vx} y1={vy} x2={topEnd[0]} y2={topEnd[1]} stroke={accent} strokeWidth="2.4" strokeLinecap="round" />
      <line x1={vx} y1={vy} x2={botEnd[0]} y2={botEnd[1]} stroke={accent} strokeWidth="2.4" strokeLinecap="round" />

      {/* angle arc at the vertex */}
      <path d={arc} fill="none" stroke={accent} strokeWidth="1.6" strokeOpacity="0.85" />

      {fingerEls}

      {/* vertex */}
      <circle cx={vx} cy={vy} r="4.5" fill={accent} />
      <circle cx={vx} cy={vy} r="9" fill="none" stroke={accent} strokeWidth="1" strokeOpacity="0.4" />

      {handles && (
        <g>
          {[topEnd, botEnd].map((p, i) => (
            <g key={i} style={{ cursor: 'ns-resize' }}>
              <circle cx={p[0]} cy={p[1]} r="18" fill={accent} fillOpacity="0.08" />
              <circle cx={p[0]} cy={p[1]} r="9" fill="#0a0a0c" stroke={accent} strokeWidth="2" />
              <circle cx={p[0]} cy={p[1]} r="3" fill={accent} />
            </g>
          ))}
        </g>
      )}
    </svg>
  );
}

// Map a client point to the diagram's viewBox space, then to a half-angle.
function pointerToHalfDeg(svg, clientX, clientY) {
  const r = svg.getBoundingClientRect();
  const x = ((clientX - r.left) / r.width) * DIAG.W;
  const y = ((clientY - r.top) / r.height) * DIAG.H;
  const dx = Math.max(8, x - DIAG.vx), dy = Math.abs(y - DIAG.H / 2);
  const half = (Math.atan2(dy, dx) * 180) / Math.PI;
  return clamp(half, Q_HALF_MIN, Q_HALF_MAX);
}

// ── readout ─────────────────────────────────────────────────────────
function UnitChain({ activeKey, accent }) {
  const items = [['deg', '\u00b0'], ['min', '\u2032'], ['sec', '\u2033']];
  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
      {items.map(([k, g], i) => (
        <React.Fragment key={k}>
          {i > 0 && <span style={{ color: 'rgba(255,255,255,0.25)', fontSize: 13 }}>{'\u2192'}</span>}
          <span style={{
            fontSize: 20, lineHeight: 1, color: k === activeKey ? accent : 'rgba(255,255,255,0.28)',
            transition: 'color .25s',
          }}>{g}</span>
        </React.Fragment>
      ))}
    </div>
  );
}

function Readout({ deg, accent, align = 'left', size = 76 }) {
  const f = formatAngle(deg);
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: size > 60 ? 12 : 9, alignItems: align === 'center' ? 'center' : 'flex-start' }}>
      <div style={{ display: 'flex', alignItems: 'baseline', gap: 4, color: 'rgba(255,255,255,0.95)' }}>
        <span style={{ fontSize: size, fontWeight: 500, letterSpacing: '-0.04em', fontVariantNumeric: 'tabular-nums', lineHeight: 0.9 }}>{f.num}</span>
        <span style={{ fontSize: size * 0.58, fontWeight: 400, color: accent }}>{f.unit}</span>
      </div>
      <UnitChain activeKey={f.key} accent={accent} />
    </div>
  );
}

// ── sparse star field (decorative, very subtle) ─────────────────────
function StarField({ seed = 1, count = 46, color = 'rgba(255,255,255,0.55)', scale = 1, origin = '50% 50%' }) {
  const stars = useMemo(() => {
    let x = seed * 9301 + 49297;
    const rnd = () => { x = (x * 9301 + 49297) % 233280; return x / 233280; };
    return Array.from({ length: count }, () => ({
      left: rnd() * 100, top: rnd() * 100, s: 0.6 + rnd() * 1.6, o: 0.12 + rnd() * 0.5,
    }));
  }, [seed, count]);
  return (
    <div style={{ position: 'absolute', inset: 0, overflow: 'hidden', pointerEvents: 'none' }}>
      <div style={{ position: 'absolute', inset: 0, transform: `scale(${scale})`, transformOrigin: origin, willChange: 'transform' }}>
        {stars.map((st, i) => (
          <div key={i} style={{
            position: 'absolute', left: st.left + '%', top: st.top + '%',
            width: st.s, height: st.s, borderRadius: '50%', background: color, opacity: st.o,
          }} />
        ))}
      </div>
    </div>
  );
}

// thousands-separated integer
const fmtInt = (n) => Math.round(n).toLocaleString('en-US');

Object.assign(window, {
  useRaf, clamp, lerp, easeInOut, formatAngle, fmtInt,
  readoutFromQ, halfFromQ, qFromHalf, pointerToHalfDeg,
  AngleDiagram, Readout, UnitChain, StarField,
});
