/* Track & Throw — utilities & shared components */
const { useState, useEffect, useRef, useMemo, useCallback, createContext, useContext, Fragment } = React;

// ---------- Utilities ----------
const uid = (p='id') => `${p}_${Math.random().toString(36).slice(2,9)}${Date.now().toString(36).slice(-3)}`;

const ordinalize = (n) => {
  const v = parseInt(n, 10);
  if (!Number.isFinite(v) || v <= 0) return '';
  const s = ['th','st','nd','rd'];
  const k = v % 100;
  return v + (s[(k - 20) % 10] || s[k] || s[0]);
};

const seasonOf = (dateStr) => {
  if (!dateStr) return null;
  const y = new Date(dateStr).getFullYear();
  return Number.isFinite(y) ? y : null;
};

const formatDate = (dateStr, opts = {}) => {
  if (!dateStr) return '';
  const d = new Date(dateStr);
  if (Number.isNaN(d.getTime())) return '';
  const { day = 'numeric', month = 'short', year } = opts;
  return d.toLocaleDateString('en-GB', { day, month, year });
};

const formatDistance = (d) => {
  if (d == null || !Number.isFinite(d)) return '—';
  return d.toFixed(2);
};

// deterministic pseudo-random in [0,1) from a string id
const hashRandom = (str, salt = 0) => {
  let h = 2166136261 ^ salt;
  for (let i = 0; i < str.length; i++) {
    h ^= str.charCodeAt(i);
    h = Math.imul(h, 16777619);
  }
  // unsigned right shift, normalize
  return ((h >>> 0) % 100000) / 100000;
};

// EVENTS: angle is in degrees for the sector
const EVENTS = {
  javelin: { name: 'Javelin', angle: 29, minScale: 30, defaultMax: 90 },
  shotput: { name: 'Shot put', angle: 34.92, minScale: 8, defaultMax: 25 },
  discus:  { name: 'Discus', angle: 34.92, minScale: 25, defaultMax: 75 },
  hammer:  { name: 'Hammer', angle: 34.92, minScale: 25, defaultMax: 90 },
};

// Compute a nice round upper bound for a max distance value
const niceMax = (max, minScale = 10) => {
  const m = Math.max(max || 0, minScale);
  // step size by magnitude
  let step;
  if (m < 15) step = 2;
  else if (m < 30) step = 5;
  else if (m < 75) step = 10;
  else step = 10;
  return Math.ceil((m * 1.08) / step) * step;
};

// Best of throws (legal only)
const bestThrow = (throws) => {
  if (!throws || !throws.length) return null;
  let b = null;
  for (const t of throws) {
    if (t.foul) continue;
    if (t.distance == null) continue;
    if (b == null || t.distance > b.distance) b = t;
  }
  return b;
};

// Throws with a numeric distance entered OR marked foul → counts as "attempted"
const countAttempts = (throws) => (throws || []).filter(isAttempted).length;
const countLegal = (throws) => (throws || []).filter(t => isAttempted(t) && !t.foul).length;
const isAttempted = (t) => (t.foul === true) || (t.distance != null && Number.isFinite(t.distance));

// ---------- Storage ----------
const STORAGE_KEY = 'track-throw-data-v1';

const loadData = () => {
  try {
    const raw = localStorage.getItem(STORAGE_KEY);
    if (!raw) return null;
    return JSON.parse(raw);
  } catch (e) { return null; }
};

const saveData = (data) => {
  try { localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); } catch (e) {}
};

// ---------- Demo seed ----------
const demoSeed = () => {
  const mkThrows = (vals) => vals.map(v => {
    if (v === 'X') return { id: uid('thr'), foul: true, distance: null, videoUrl: '' };
    return { id: uid('thr'), foul: false, distance: v, videoUrl: '' };
  });
  return {
    competitions: [
      { id: uid('cmp'), name: 'Spring Opener', location: 'Lyon', date: '2024-04-13', event: 'javelin',
        ranking: 4, notes: 'Cold day, opened the season under target.', throws: mkThrows([54.12, 58.30, 'X', 60.05, 61.22, 'X']) },
      { id: uid('cmp'), name: 'Regional Champs', location: 'Bordeaux', date: '2024-05-18', event: 'javelin',
        ranking: 2, notes: 'PB! Strong tailwind on throw 4.', throws: mkThrows([62.41, 'X', 64.10, 67.85, 65.20, 63.00]) },
      { id: uid('cmp'), name: 'National Meet', location: 'Paris', date: '2024-06-22', event: 'javelin',
        ranking: 3, notes: 'Backhand was bothering me, still landed top 3.', throws: mkThrows([63.50, 65.20, 66.40, 'X', 68.10, 'X']) },
      { id: uid('cmp'), name: 'Summer Throws', location: 'Nice', date: '2024-07-14', event: 'javelin',
        ranking: 1, notes: 'Best comp of the year — felt unstoppable.', throws: mkThrows(['X', 67.20, 70.85, 69.40, 71.10, 70.05]) },
      { id: uid('cmp'), name: 'Winter Indoor', location: 'Eaubonne', date: '2025-02-08', event: 'shotput',
        ranking: 5, notes: 'Tried shot put for cross-training.', throws: mkThrows([13.10, 13.85, 'X', 14.20, 14.05]) },
      { id: uid('cmp'), name: 'Season Opener 2025', location: 'Toulouse', date: '2025-04-26', event: 'javelin',
        ranking: 3, notes: '', throws: mkThrows([66.20, 68.40, 'X', 69.85, 70.10]) },
    ],
    settings: { unit: 'm', defaultEvent: 'javelin' },
  };
};

const blankData = () => ({ competitions: [], settings: { unit: 'm', defaultEvent: 'javelin' } });

// ---------- Icons (inline SVG) ----------
const Icon = {
  list:    (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M3.5 6h17M3.5 12h17M3.5 18h11"/></svg>,
  chart:   (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M4 19V5M4 19h16"/><path d="M8 15l3.5-4 3 2.5L20 7"/></svg>,
  stats:   (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M5 20V11M12 20V4M19 20v-6"/></svg>,
  settings:(p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .35 1.85l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.7 1.7 0 0 0-1.85-.35 1.7 1.7 0 0 0-1 1.55V21a2 2 0 0 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.55 1.7 1.7 0 0 0-1.85.35l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.7 1.7 0 0 0 .35-1.85 1.7 1.7 0 0 0-1.55-1H3a2 2 0 0 1 0-4h.1a1.7 1.7 0 0 0 1.55-1.1 1.7 1.7 0 0 0-.35-1.85l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.7 1.7 0 0 0 1.85.35h0a1.7 1.7 0 0 0 1-1.55V3a2 2 0 0 1 4 0v.1a1.7 1.7 0 0 0 1 1.55 1.7 1.7 0 0 0 1.85-.35l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.7 1.7 0 0 0-.35 1.85v0a1.7 1.7 0 0 0 1.55 1H21a2 2 0 0 1 0 4h-.1a1.7 1.7 0 0 0-1.55 1z"/></svg>,
  plus:    (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M12 5v14M5 12h14"/></svg>,
  back:    (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.9" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M15 6l-6 6 6 6"/></svg>,
  trash:   (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M4 7h16M9 7V5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2"/><path d="M6 7l1.2 12a2 2 0 0 0 2 1.8h5.6a2 2 0 0 0 2-1.8L18 7"/></svg>,
  video:   (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><rect x="2.5" y="6.5" width="13" height="11" rx="2"/><path d="M15.5 10l5-2.5v9L15.5 14"/></svg>,
  close:   (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.9" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M6 6l12 12M18 6l-12 12"/></svg>,
  chev:    (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M9 6l6 6-6 6"/></svg>,
  chevDown:(p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M6 9l6 6 6-6"/></svg>,
  download:(p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M12 4v12M6 12l6 6 6-6M5 20h14"/></svg>,
  upload:  (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M12 20V8M6 12l6-6 6 6M5 4h14"/></svg>,
  dots:    (p) => <svg viewBox="0 0 24 24" fill="currentColor" {...p}><circle cx="6" cy="12" r="1.5"/><circle cx="12" cy="12" r="1.5"/><circle cx="18" cy="12" r="1.5"/></svg>,
  edit:    (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M4 20h4l10-10-4-4L4 16v4z"/><path d="M14 6l4 4"/></svg>,
  link:    (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M10 13a5 5 0 0 0 7 0l3-3a5 5 0 1 0-7-7l-1 1"/><path d="M14 11a5 5 0 0 0-7 0l-3 3a5 5 0 1 0 7 7l1-1"/></svg>,
  target:  (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="5"/><circle cx="12" cy="12" r="1"/></svg>,
  flag:    (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M5 21V4"/><path d="M5 4h12l-2.5 4L17 12H5"/></svg>,
  check:   (p) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" {...p}><path d="M5 12l5 5L20 7"/></svg>,
};

// ---------- Toast ----------
const ToastContext = createContext(null);
const useToast = () => useContext(ToastContext);
const ToastProvider = ({ children }) => {
  const [msg, setMsg] = useState(null);
  const tRef = useRef(null);
  const show = useCallback((text) => {
    setMsg(text);
    clearTimeout(tRef.current);
    tRef.current = setTimeout(() => setMsg(null), 1800);
  }, []);
  return (
    <ToastContext.Provider value={show}>
      {children}
      <div className={'toast ' + (msg ? 'show' : '')}>{msg}</div>
    </ToastContext.Provider>
  );
};

// ---------- Sheet (bottom modal) ----------
const Sheet = ({ open, onClose, title, children, action }) => {
  // close on Escape
  useEffect(() => {
    if (!open) return;
    const h = (e) => { if (e.key === 'Escape') onClose(); };
    window.addEventListener('keydown', h);
    return () => window.removeEventListener('keydown', h);
  }, [open, onClose]);
  return (
    <Fragment>
      <div className={'scrim ' + (open ? 'open' : '')} onClick={onClose}></div>
      <div className={'sheet ' + (open ? 'open' : '')}>
        <div className="sheet-grabber"></div>
        <div className="sheet-head">
          <h2>{title}</h2>
          {action}
          <button className="icon-btn" onClick={onClose}><Icon.close/></button>
        </div>
        <div className="sheet-body">
          {children}
        </div>
      </div>
    </Fragment>
  );
};

// ---------- Modal (full screen, for competition detail) ----------
const ModalFull = ({ open, onClose, title, children, headRight }) => {
  const bodyRef = useRef(null);
  const [scrolled, setScrolled] = useState(false);
  useEffect(() => {
    if (!open) return;
    const h = (e) => { if (e.key === 'Escape') onClose(); };
    window.addEventListener('keydown', h);
    return () => window.removeEventListener('keydown', h);
  }, [open, onClose]);
  useEffect(() => {
    if (!open) { setScrolled(false); return; }
    const el = bodyRef.current; if (!el) return;
    const onScroll = () => setScrolled(el.scrollTop > 4);
    el.addEventListener('scroll', onScroll, { passive: true });
    return () => el.removeEventListener('scroll', onScroll);
  }, [open]);
  return (
    <div className={'modal-full ' + (open ? 'open' : '')} aria-hidden={!open}>
      <div className={'modal-head ' + (scrolled ? 'scrolled' : '')}>
        <button className="back" onClick={onClose}><Icon.back/><span>Back</span></button>
        <div className="title">{title}</div>
        <div className="actions">{headRight}</div>
      </div>
      <div className="modal-body" ref={bodyRef}>{children}</div>
    </div>
  );
};

// ---------- Confirm dialog (simple) ----------
const useConfirm = () => {
  const [state, setState] = useState(null);
  const ask = useCallback((opts) => new Promise(res => setState({ ...opts, res })), []);
  const close = (v) => { if (state) { state.res(v); setState(null); } };
  const ui = state && (
    <Fragment>
      <div className="scrim open" onClick={() => close(false)}></div>
      <div className="sheet open" style={{maxWidth: 360}}>
        <div className="sheet-grabber"></div>
        <div className="sheet-body">
          <h3 style={{margin:'4px 0 6px', fontSize:17}}>{state.title}</h3>
          {state.message && <p className="muted" style={{margin:'0 0 16px', fontSize:13.5}}>{state.message}</p>}
          <div className="flex gap-8">
            <button className="btn secondary flex-1" onClick={() => close(false)}>{state.cancelLabel || 'Cancel'}</button>
            <button className={'btn flex-1 ' + (state.danger ? 'danger' : '')} onClick={() => close(true)}>{state.confirmLabel || 'OK'}</button>
          </div>
        </div>
      </div>
    </Fragment>
  );
  return { ask, ui };
};

// Expose globals for other scripts
Object.assign(window, {
  React, useState, useEffect, useRef, useMemo, useCallback, Fragment,
  uid, ordinalize, seasonOf, formatDate, formatDistance, hashRandom,
  EVENTS, niceMax, bestThrow, countAttempts, countLegal, isAttempted,
  loadData, saveData, demoSeed, blankData,
  Icon, ToastProvider, useToast, Sheet, ModalFull, useConfirm,
});
