// Tempo — primitives d'animation (compteurs animés, horloge).
//
// Aucun formatage ici : chaque image passe par les formateurs communs de
// format-prefs.jsx (fmtMoneyCur, fmtNum, fmtPct, fmtR), pour que la valeur
// animée finisse exactement sur la chaîne affichée partout ailleurs dans l'app
// (charte §1 et §2).

const ANIM_NBSP = '\u00A0'; // espace insécable avant une unité (« 3 j »)

// Valeur numérique d'une prop : nombre tel quel, chaîne numérique convertie,
// absente ou illisible → NaN (affichée « — » par les composants).
function animToNum(v) {
  if (typeof v === 'number') return v;
  if (v == null || v === '' || typeof v === 'boolean') return NaN;
  const n = Number(v);
  return Number.isNaN(n) ? NaN : n;
}

// L'utilisateur a demandé moins d'animations (réglage système) : on affiche
// directement la valeur finale.
function animReducedMotion() {
  try {
    return !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches);
  } catch (e) {
    return false;
  }
}

// useCountUp(target, { duration, decimals, ease }) → nombre animé de la valeur
// précédente vers target. Les images intermédiaires sont arrondies à
// `decimals` ; une fois l'animation finie, la valeur rendue est EXACTEMENT
// target (jamais une version arrondie). Cible absente ou illisible → 0.
function useCountUp(target, { duration = 900, decimals = 0, ease = (t) => 1 - Math.pow(1 - t, 3) } = {}) {
  const raw = animToNum(target);
  const goal = Number.isFinite(raw) ? raw : 0;
  const [v, setV] = React.useState(0);
  const ref = React.useRef({ start: 0, from: 0 });
  React.useEffect(() => {
    const from = Number.isFinite(v) ? v : 0;
    if (from === goal || !(duration > 0) || animReducedMotion()) {
      setV(goal);
      return undefined;
    }
    ref.current = { start: performance.now(), from };
    let raf;
    const tick = (now) => {
      const t = Math.min(1, (now - ref.current.start) / duration);
      setV(t < 1 ? ref.current.from + (goal - ref.current.from) * ease(t) : goal);
      if (t < 1) raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [goal]);
  if (v === goal) return goal;
  const d = Math.max(0, Math.min(10, Math.floor(Number(decimals) || 0)));
  const p = Math.pow(10, d);
  return Math.round(v * p) / p;
}

// Style commun des compteurs : un chiffre ne se coupe jamais (charte §9).
function animNumStyle(size, weight, color, style) {
  return { fontSize: size, fontWeight: weight, color, letterSpacing: '-.018em', lineHeight: 1.05, whiteSpace: 'nowrap', ...style };
}

// Montant animé : la SEULE primitive d'animation de montant de l'app (charte §2).
// Chaque image est formatée aux décimales de la devise ; une fois arrivé, on
// affiche exactement fmtMoneyCur(value, devise, sign), donc la même chaîne, au
// centime près, que partout ailleurs : la devise imposée par `currency`
// (Backtest en 'USD'), sinon celle du profil — ce que fait fmtMoney (app.jsx).
// La devise du profil est relue à chaque rendu : un changement dans Réglages
// s'applique sans recharger la page.
// Signe « + » / « − » (U+2212) ; zéro « 0,00 € » sans signe. La couleur vient
// du conteneur (color='inherit' par défaut) : vert, rouge ou var(--fg) pour zéro.
// Valeur absente ou illisible → « — », sans animation.
function AnimatedMoney({ value, sign = true, currency, size = 28, weight = 600, color = 'inherit', duration = 1000, style }) {
  const n = animToNum(value);
  const valid = Number.isFinite(n);
  const code = currency || window.__appCurrency;
  const dec = currencyDecimals(code);
  const v = useCountUp(valid ? n : 0, { duration, decimals: dec });
  const text = valid ? fmtMoneyCur(v, code, sign) : '—';
  return (
    <span className="num" style={animNumStyle(size, weight, color, style)}>{text}</span>
  );
}

// Nombre animé (taux, R, profit factor, compteurs). Format final :
//   - format={fn}      : fn(n) formate chaque image (ex. n => fmtPct(n, 1)) ;
//   - suffix="%"       : fmtPct(n, decimals, sign)       → « 52,2 % » ;
//   - suffix="R"       : fmtR(n), 2 décimales, signé     → « +0,51 R » ;
//   - autre suffixe    : fmtNum(n, decimals, sign) + espace insécable + unité ;
//   - sans suffixe     : fmtNum(n, decimals, sign)       → « 2,46 ».
// Infinity n'est jamais ramené à 0 : il s'affiche « ∞ » (profit factor sans
// perte) ; valeur absente ou illisible → « — » (état vide des indicateurs).
function AnimatedNumber({ value, decimals = 0, suffix = '', sign = false, format, size = 28, weight = 600, color = 'inherit', duration = 1000, style }) {
  const n = animToNum(value);
  const valid = Number.isFinite(n);
  const unit = String(suffix == null ? '' : suffix).trim();
  let dec = decimals;
  let fmt;
  if (typeof format === 'function') fmt = format;
  else if (unit === '%') fmt = (x) => fmtPct(x, decimals, sign);
  else if (unit === 'R') { dec = 2; fmt = (x) => fmtR(x, 2); }
  else fmt = (x) => fmtNum(x, decimals, sign) + (unit ? ANIM_NBSP + unit : '');
  const v = useCountUp(valid ? n : 0, { duration, decimals: dec });
  let text;
  if (n === Infinity) text = '\u221E';
  else if (n === -Infinity) text = '\u2212\u221E';
  else if (!valid) text = '—';
  else text = fmt(v);
  return (
    <span className="num" style={animNumStyle(size, weight, color, style)}>{text}</span>
  );
}

// Instant courant, rafraîchi chaque seconde. Il ne se formate pas tel quel :
// passer par tzTimeIn / tzClock / localDayInTz (fuseau du profil, charte §4).
function useLiveTime() {
  const [t, setT] = React.useState(() => new Date());
  React.useEffect(() => {
    const id = setInterval(() => setT(new Date()), 1000);
    return () => clearInterval(id);
  }, []);
  return t;
}

Object.assign(window, { useCountUp, AnimatedMoney, AnimatedNumber, useLiveTime });
