/* ═══════ Shared entrance animations (numbers + charts) ═══════
   Loaded before the component scripts. Everything here degrades to a static
   value when the visitor has `prefers-reduced-motion: reduce` set, so the
   accessibility contract is never broken.

   Exposes:
     window.PREFERS_REDUCED_MOTION  — boolean, read by chart components
     window.useCountUp(target)      — hook: animates a number 0 → target
     window.AnimatedNumber          — component: <AnimatedNumber value={…} />
                                       count-up that preserves prefix/suffix
                                       ("34%", "40 posts") and thousands commas.

   These are the on-screen "真數字" layer used both in the live UI and as the
   foreground for the marketing screen-recording — see
   docs/marketing-video-playbook.md. */
(function () {
  const { useState, useEffect, useRef } = React;

  const PREFERS_REDUCED_MOTION =
    typeof window !== 'undefined' &&
    window.matchMedia &&
    window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  window.PREFERS_REDUCED_MOTION = PREFERS_REDUCED_MOTION;

  /* Animate `target` from 0 → target on mount using easeOutCubic.
     Returns the raw (un-rounded) in-flight value so the caller controls
     formatting. Non-numbers / reduced-motion resolve instantly. */
  window.useCountUp = function (target, opts) {
    opts = opts || {};
    var dur = opts.duration || 900;
    var animatable =
      typeof target === 'number' && isFinite(target) && !PREFERS_REDUCED_MOTION;

    var [val, setVal] = useState(animatable ? 0 : target);
    var raf = useRef(0);

    useEffect(function () {
      if (!animatable) { setVal(target); return; }
      var startTs = null;
      function tick(ts) {
        if (startTs === null) startTs = ts;
        var p = Math.min(1, (ts - startTs) / dur);
        var eased = 1 - Math.pow(1 - p, 3);
        setVal(target * eased);
        if (p < 1) raf.current = requestAnimationFrame(tick);
        else setVal(target);
      }
      raf.current = requestAnimationFrame(tick);
      return function () { cancelAnimationFrame(raf.current); };
      // eslint-disable-next-line
    }, [target]);

    return val;
  };

  /* <AnimatedNumber value={34} /> or value="34%" or value="40 posts".
     Parses an optional non-digit prefix, the number (commas/decimals ok), and
     a trailing suffix; counts up the numeric part only. Anything we can't parse
     as a number renders verbatim. */
  window.AnimatedNumber = function (props) {
    var raw = props.value;
    var prefix = '', suffix = '', num = null, decimals = 0;

    if (typeof raw === 'number') {
      num = raw;
    } else if (typeof raw === 'string') {
      var m = raw.match(/^(\D*)([\d,]+(?:\.\d+)?)(.*)$/);
      if (m) {
        prefix = m[1];
        var digits = m[2].replace(/,/g, '');
        num = parseFloat(digits);
        suffix = m[3];
        if (digits.indexOf('.') >= 0) decimals = (digits.split('.')[1] || '').length;
      }
    }

    // Hook must run every render — call it unconditionally, then decide output.
    var animated = window.useCountUp(num !== null ? num : 0, { duration: props.duration });

    if (num === null || !isFinite(num)) return raw;
    if (decimals === 0 && !Number.isInteger(num)) decimals = 1;

    var shown = Number(animated).toLocaleString(undefined, {
      minimumFractionDigits: decimals,
      maximumFractionDigits: decimals,
    });
    return prefix + shown + suffix;
  };

  /* <Sparkline data={[…numbers]} /> — a tiny area+line trend chart that draws
     itself in left-to-right (the "走勢線" effect) via an animated
     stroke-dashoffset. Falls back to a static line under reduced-motion.
     Returns null for <2 points. */
  window.Sparkline = function (props) {
    var data = (props.data || []).map(Number).filter(function (v) { return isFinite(v); });
    if (data.length < 2) return null;

    var w = props.width || 220;
    var h = props.height || 38;
    var pad = 3;
    var stroke = props.stroke || 'var(--accent)';
    var fill = props.fill || 'rgba(249,115,22,0.10)';
    var reduced = !!window.PREFERS_REDUCED_MOTION;

    var max = Math.max.apply(null, data);
    var min = Math.min.apply(null, data);
    var range = (max - min) || 1;
    var n = data.length;
    var X = function (i) { return pad + (i / (n - 1)) * (w - pad * 2); };
    var Y = function (v) { return h - pad - ((v - min) / range) * (h - pad * 2); };

    var linePts = data.map(function (v, i) { return X(i) + ',' + Y(v); }).join(' ');
    var areaPts = linePts + ' ' + X(n - 1) + ',' + (h - pad) + ' ' + X(0) + ',' + (h - pad);
    var lastX = X(n - 1), lastY = Y(data[n - 1]);

    return (
      <svg width={w} height={h} viewBox={'0 0 ' + w + ' ' + h}
           style={{ display: 'block', overflow: 'visible' }} aria-hidden="true">
        <polygon points={areaPts} fill={fill} stroke="none" opacity={reduced ? 1 : 0}>
          {!reduced && <animate attributeName="opacity" from="0" to="1" dur="0.6s" begin="0.45s" fill="freeze" />}
        </polygon>
        <polyline points={linePts} fill="none" stroke={stroke} strokeWidth="1.5"
                  strokeLinecap="round" strokeLinejoin="round"
                  pathLength="1" strokeDasharray="1" strokeDashoffset={reduced ? 0 : 1}>
          {!reduced && <animate attributeName="stroke-dashoffset" from="1" to="0" dur="1.05s"
                                calcMode="spline" keyTimes="0;1" keySplines="0.22 1 0.36 1" fill="freeze" />}
        </polyline>
        <circle cx={lastX} cy={lastY} r="2.5" fill={stroke} opacity={reduced ? 1 : 0}>
          {!reduced && <animate attributeName="opacity" from="0" to="1" dur="0.3s" begin="0.95s" fill="freeze" />}
        </circle>
      </svg>
    );
  };
})();
