// ─── RACE ANALYSIS (1/2) — shared helpers + race trace chart (Gap · Positions · Lap times) ──────
// Data: useReplayStats (ur2d-replay-stats.jsx). Mounted under the round results by URTrackHero's renderBelow.
const FRA_TYRE = { S: ['#E8102A', '#fff', 'Soft'], M: ['#FFD200', '#111', 'Medium'], H: ['#F2F2F2', '#111', 'Hard'], I: ['#3AB54A', '#fff', 'Inter'], W: ['#2F7FD8', '#fff', 'Wet'] };
const FRA_IND = ['#8FB8DE', '#C9A0DC', '#F0B27A', '#7ED6C2', '#E8D36F', '#B8BEC6', '#E59880', '#9AD38A'];
const fraFmt = (s) => { if (s == null || !isFinite(s)) return '—'; const m = Math.floor(s / 60), r = s - m * 60; return (m ? m + ':' + (r < 10 ? '0' : '') : '') + r.toFixed(3); };
const fraSeasonCfg = (league, label) => { try { const a = UR2D_ADAPTERS[league]; return (a && a.seasons.find((s) => s.label === label)) || {}; } catch (e) { return {}; } };
function fraTeamColor(league, label, sh) {
  const s = fraSeasonCfg(league, label);
  return sh && ((s.teamColors || {})[sh] || ((s.liveSource || {}).teamColors || {})[sh]) || null;
}
// Team-colour look-alike test (OKLab), dark colours read at the lightness the panels lift them to.
let _fraCtx = null;
const fraRgb = (c) => { _fraCtx = _fraCtx || document.createElement('canvas').getContext('2d'); _fraCtx.fillStyle = '#000'; _fraCtx.fillStyle = c; const m = String(_fraCtx.fillStyle).match(/^#([0-9a-f]{6})$/i); return m ? [0, 2, 4].map((i) => parseInt(m[1].slice(i, i + 2), 16)) : [128, 128, 128]; };
function fraLook(c) {
  const [R, G, B] = fraRgb(c).map((v) => { v /= 255; return v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4; });
  const l = Math.cbrt(0.4122214708 * R + 0.5363325363 * G + 0.0514459929 * B), m = Math.cbrt(0.2119034982 * R + 0.6806995451 * G + 0.1073969566 * B), s = Math.cbrt(0.0883024619 * R + 0.2817188376 * G + 0.6299787005 * B);
  return [Math.max(0.62, 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s), 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s, 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s];
}
// Look-alike = same hue family (within 15°) at a similar lightness; greys/whites by plain distance.
const fraNear = (p, q) => {
  const cp = Math.hypot(p[1], p[2]), cq = Math.hypot(q[1], q[2]);
  if (cp < 0.06 || cq < 0.06) return Math.hypot(p[0] - q[0], p[1] - q[1], p[2] - q[2]) < 0.06;
  let dh = Math.abs(Math.atan2(p[2], p[1]) - Math.atan2(q[2], q[1])); if (dh > Math.PI) dh = 2 * Math.PI - dh;
  return dh < 0.26 && Math.abs(p[0] - q[0]) < 0.12;
};
// Replay names ("Glow (3)") → site identity: sheet name, team, colour, car. Teammates share a colour; the 2nd is dashed.
// Team colours per race (user 2026-10-13): teams higher in the constructors' standings (else best finish here) keep their colour;
// a team whose colour looks like one already taken switches to its secondary (urTeamColor2); if that's taken too it keeps its
// colour and is striped two-tone with the secondary (`stripe`). `color2` = the team's other colour.
function fraIdentify(S, ctx) {
  const roster = (typeof getRoster === 'function' && getRoster(ctx.league, ctx.season)) || null;
  const rosterTeam = (h) => { if (!roster || !h) return null; for (const [sh, t] of Object.entries(roster.teams || {})) if (t.mains.includes(h) || t.reserve === h) return sh; return null; };
  const known = typeof matchReplayNames === 'function' ? matchReplayNames(S.cars, ctx.res, ctx.league + ':' + ctx.season + ' R' + ctx.round) : {};
  const base = S.cars.map((c) => {
    const who = known[c.id] ? known[c.id].who : null, label = known[c.id] ? known[c.id].nm : c.base;
    let team = (ctx.teamFor && (ctx.teamFor(label) || ctx.teamFor(c.base))) || rosterTeam(who && who.handle);
    if (team === 'IND') team = null;
    return { c, label, team };
  });
  const tpos = {}, best = {}, tc = {}, taken = [];
  ((ctx.std && ctx.std.teams) || []).forEach((t) => { if (t && t.short) tpos[t.short] = t.pos || 99; });
  base.forEach(({ c, team }) => { if (team) best[team] = Math.min(best[team] || 99, c.dnf ? 98 : c.pos || 98); });
  Object.keys(best).sort((a, b) => (tpos[a] || 99) - (tpos[b] || 99) || best[a] - best[b]).forEach((team) => {
    const c1 = (ctx.colorFor && ctx.colorFor(team)) || fraTeamColor(ctx.league, ctx.season, team);
    if (!c1) return;
    const c2 = typeof urTeamColor2 === 'function' ? urTeamColor2(ctx.league, ctx.season, team) : null;
    const p1 = fraLook(c1), p2 = c2 ? fraLook(c2) : null, hit = (p) => taken.some((q) => fraNear(p, q));
    if (hit(p1) && p2 && !hit(p2)) { tc[team] = { color: c2, color2: c1 }; taken.push(p2); }
    else { tc[team] = { color: c1, color2: c2, stripe: hit(p1) && !!c2 }; taken.push(p1); }
  });
  const out = {}, seen = {}; let ind = 0;
  base.forEach(({ c, label, team }) => {
    const t = team && tc[team], color = (t && t.color) || FRA_IND[ind++ % FRA_IND.length];
    const k = team || ('_' + c.id); seen[k] = (seen[k] || 0) + 1;
    out[c.id] = { label, team, color, color2: (t && t.color2) || null, stripe: !!(t && t.stripe), dash: team && seen[k] > 1, car: ctx.carFor ? ctx.carFor(label) : null };
  });
  return out;
}
function useFraWidth() {
  const ref = React.useRef(null); const [w, setW] = React.useState(900);
  React.useLayoutEffect(() => { const el = ref.current; if (!el) return; const f = () => setW(Math.max(320, el.clientWidth)); f(); const ro = new ResizeObserver(f); ro.observe(el); return () => ro.disconnect(); }, []);
  return [ref, w];
}

function FRARaceTrace({ S, drv, mode, foc = [] }) {
  const [hov, setHover] = React.useState(null); // local: hovering never re-highlights the rest of the page
  const [box, W] = useFraWidth();
  const [tip, setTip] = React.useState(null);
  const narrow = W < 640, H = narrow ? 360 : 440, M = { l: narrow ? 44 : 56, r: mode === 'pos' ? (narrow ? 96 : 132) : 18, t: 14, b: 34 };
  const lead = S.cars[0], L = lead.done, ref = S.raceTime / S.laps;
  const val = (c, n) => mode === 'pos' ? c.posByLap[n] : mode === 'gap' ? (c.x[n] == null ? null : n * ref - c.x[n]) : (n >= 1 && c.lt[n - 1] != null ? c.lt[n - 1] : null);
  const all = []; S.cars.forEach((c) => { for (let n = mode === 'laps' ? 1 : 0; n <= c.done; n++) { const v = val(c, n); if (v != null) all.push(v); } });
  all.sort((a, b) => a - b); const q = (f) => all[Math.floor(f * (all.length - 1))];
  let y0, y1; // y0 = bottom value, y1 = top value
  if (mode === 'pos') { y0 = S.cars.length + 0.5; y1 = 0.5; }
  else if (mode === 'gap') { y0 = Math.floor(all[0] / 5) * 5; y1 = Math.max(1, Math.ceil(all[all.length - 1] / 2) * 2); }
  else { y1 = Math.floor((q(0.01) - 0.3) * 2) / 2; y0 = Math.ceil(Math.min(all[all.length - 1], q(0.93) + 1.2)); }
  const pw = W - M.l - M.r, ph = H - M.t - M.b, x0 = mode === 'laps' ? 1 : 0;
  const X = (n) => M.l + ((n - x0) / Math.max(1, L - x0)) * pw;
  const Y = (v) => { const t = (v - y0) / (y1 - y0); return M.t + (1 - Math.max(-0.02, Math.min(1.02, t))) * ph; };
  const step = mode === 'pos' ? (S.cars.length > 12 ? 2 : 1) : mode === 'gap' ? (y1 - y0 > 40 ? 10 : 5) : (y0 - y1 > 6 ? 2 : 1);
  const yt = []; if (mode === 'pos') { for (let v = 1; v <= S.cars.length; v += step) yt.push(v); } else { const lo = Math.min(y0, y1), hi2 = Math.max(y0, y1); for (let v = Math.ceil(lo / step) * step; v <= hi2; v += step) yt.push(v); }
  const xt = []; const xs = L > 30 ? 5 : L > 14 ? 2 : 1; for (let n = x0 === 1 ? xs : 0; n <= L; n += xs) xt.push(n); if (x0 === 1) xt.unshift(1);
  const on = (id) => !foc.length || foc.includes(id) || id === hov;
  // Wet track: ONE band shaded by a horizontal gradient of the smoothed wetness (was a rect per sample — read as a barcode).
  const wxId = 'frawx' + String(React.useId()).replace(/[^a-zA-Z0-9]/g, '');
  const wxSm = React.useMemo(() => {
    const w = S.wx || []; if (w.length < 2) return [];
    const a = w.map((p) => Math.min(0.3, p.wet * 0.45 + p.rain * 0.25)), k = Math.max(2, Math.round(w.length / 40));
    const sm = a.map((_, i) => { let s = 0, n = 0; for (let j = Math.max(0, i - k); j <= Math.min(a.length - 1, i + k); j++) { s += a[j]; n++; } return s / n; });
    const st = Math.max(1, Math.floor(w.length / 90)), out = [];
    for (let i = 0; i < w.length; i += st) out.push({ l: w[i].l, a: sm[i] });
    return out;
  }, [S]);
  const wxOn = wxSm.some((p) => p.a > 0.012);
  const wxRects = wxOn ? <React.Fragment>
    <defs><linearGradient id={wxId} gradientUnits="userSpaceOnUse" x1={M.l} x2={M.l + pw} y1="0" y2="0">{wxSm.map((p, i) => <stop key={i} offset={Math.max(0, Math.min(1, (p.l - x0) / Math.max(1, L - x0))).toFixed(4)} stopColor="#2F7FD8" stopOpacity={p.a.toFixed(3)}></stop>)}</linearGradient></defs>
    <rect x={M.l} y={M.t} width={pw} height={ph} fill={`url(#${wxId})`}></rect>
  </React.Fragment> : null;
  const move = (e) => {
    const r = e.currentTarget.getBoundingClientRect(), mx = e.clientX - r.left, my = e.clientY - r.top;
    const n = Math.max(x0, Math.min(L, Math.round(x0 + ((mx - M.l) / pw) * (L - x0))));
    let best = null, bd = 1e9; S.cars.forEach((c) => { const v = val(c, n); if (v == null) return; const d = Math.abs(Y(v) - my); if (d < bd) { bd = d; best = c; } });
    if (best && bd < 40) { setHover(best.id); setTip({ n, c: best, x: X(n), y: Y(val(best, n)) }); } else { setHover(null); setTip(null); }
  };
  const leave = () => { setHover(null); setTip(null); };
  const tyreAt = (c, n) => { const s = c.stints.find((s) => n - 0.5 >= s.from && n - 0.5 < s.to) || c.stints[c.stints.length - 1]; return s && s.c; };
  const rank = (id) => (id === hov ? 2 : foc.includes(id) ? 1 : 0);
  const order = [...S.cars].sort((a, b) => rank(a.id) - rank(b.id));
  return (
    <div ref={box} style={{ position: 'relative' }}>
      <svg width={W} height={H} onMouseMove={move} onMouseLeave={leave} style={{ display: 'block', cursor: 'crosshair' }}>
        {wxRects}
        {yt.map((v) => <g key={'y' + v}><line x1={M.l} x2={M.l + pw} y1={Y(v)} y2={Y(v)} stroke="rgba(255,255,255,0.07)"></line><text x={M.l - 10} y={Y(v) + 4} textAnchor="end" className="fra-tick">{mode === 'pos' ? 'P' + v : mode === 'gap' ? (v > 0 ? '+' : v < 0 ? '−' : '') + Math.abs(v) + 's' : v + 's'}</text></g>)}
        {xt.map((n) => <text key={'x' + n} x={X(n)} y={H - 10} textAnchor="middle" className="fra-tick">{n === 0 ? 'Grid' : 'L' + n}</text>)}
        {mode === 'gap' && <line x1={M.l} x2={M.l + pw} y1={Y(0)} y2={Y(0)} stroke="rgba(255,255,255,0.28)" strokeDasharray="2 4"></line>}
        {order.map((c) => {
          const d = drv[c.id], pts = []; for (let n = x0; n <= c.done; n++) { const v = val(c, n); if (v != null) pts.push(X(n).toFixed(1) + ',' + Y(v).toFixed(1)); }
          const P = pts.join(' '), w = hov === c.id ? 3.4 : foc.includes(c.id) ? 2.8 : 1.7;
          return <g key={c.id} opacity={on(c.id) ? 1 : 0.14} style={{ transition: 'opacity .15s' }}><polyline points={P} fill="none" stroke={d.color} strokeWidth={w} strokeDasharray={d.dash ? '6 4' : null} strokeLinejoin="round"></polyline>{d.stripe && d.color2 && !d.dash && <polyline points={P} fill="none" stroke={d.color2} strokeWidth={w} strokeDasharray="6 6" strokeLinejoin="round"></polyline>}</g>;
        })}
        {[...new Set([...foc, hov].filter(Boolean))].map((id) => { const c = S.cars.find((x) => x.id === id); return c ? <g key={id}>{c.pits.map((p, i) => { const n = Math.min(c.done, Math.max(x0, Math.round(p.at))), v = val(c, n); return v == null ? null : <g key={i}><rect x={X(n) - 6.5} y={Y(v) - 6.5} width="13" height="13" fill="#FF8A00" stroke="#000"></rect><text x={X(n)} y={Y(v) + 4} textAnchor="middle" style={{ font: "800 10px 'Archivo',sans-serif", fill: '#000' }}>P</text></g>; })}</g> : null; })}
        {mode === 'laps' && S.fastest && (() => { const c = S.cars.find((x) => x.id === S.fastest.id); return c && on(c.id) ? <circle cx={X(S.fastest.lap)} cy={Y(S.fastest.t)} r="5.5" fill="#B44DFF" stroke="#000" strokeWidth="1.5"></circle> : null; })()}
        {mode === 'pos' && S.cars.map((c) => { const d = drv[c.id]; return <text key={'e' + c.id} x={M.l + pw + 10} y={Y(c.posByLap[c.done] || c.pos) + 4} className="fra-end" fill={on(c.id) ? d.color : 'rgba(255,255,255,0.2)'}>{(c.dnf ? 'DNF ' : '') + d.label}</text>; })}
        {tip && <line x1={tip.x} x2={tip.x} y1={M.t} y2={M.t + ph} stroke="rgba(255,255,255,0.35)"></line>}
      </svg>
      {tip && (() => { const d = drv[tip.c.id], n = tip.n, ty = tyreAt(tip.c, n), lt = n >= 1 ? tip.c.lt[n - 1] : null; return (
        <div className="fra-tip" style={{ left: Math.min(W - 230, Math.max(0, tip.x + 14)), top: Math.max(0, tip.y - 70) }}>
          <b style={{ color: d.color }}>{d.label}</b><span>{n === 0 ? 'Grid' : 'Lap ' + n} · P{tip.c.posByLap[n] || '—'}</span>
          {n >= 1 && <span>Lap {fraFmt(lt)}{ty ? ' · ' + FRA_TYRE[ty][2] : ''}</span>}
          {mode === 'gap' && n >= 1 && <span>{(() => { const v = val(tip.c, n); return (v >= 0 ? '+' : '−') + Math.abs(v).toFixed(1) + 's vs winner pace'; })()}</span>}
        </div>); })()}
    </div>);
}

Object.assign(window, { FRA_TYRE, fraFmt, fraIdentify, fraLook, fraNear, useFraWidth, FRARaceTrace });
