/* facts-charts.jsx — animated chart primitives */

const { useState, useEffect, useRef, useMemo } = React;

/* Hook: returns true once element scrolls into view (and stays true) */
function useInView(threshold = 0.25){
  const ref = useRef(null);
  const [seen, setSeen] = useState(false);
  useEffect(() => {
    if (!ref.current || seen) return;
    const io = new IntersectionObserver((entries) => {
      entries.forEach(e => { if (e.isIntersecting) setSeen(true); });
    }, { threshold });
    io.observe(ref.current);
    return () => io.disconnect();
  }, [seen, threshold]);
  return [ref, seen];
}

/* Easing */
const easeOutCubic = t => 1 - Math.pow(1 - t, 3);

/* Hook: tween a number from 0 → target over duration ms once `start` flips true */
function useTween(target, start, duration = 1400, delay = 0){
  const [val, setVal] = useState(0);
  useEffect(() => {
    if (!start) return;
    let raf, t0;
    const startT = performance.now() + delay;
    const tick = (now) => {
      if (now < startT){ raf = requestAnimationFrame(tick); return; }
      if (t0 == null) t0 = now;
      const p = Math.min(1, (now - t0) / duration);
      setVal(target * easeOutCubic(p));
      if (p < 1) raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [target, start, duration, delay]);
  return val;
}

/* ─────────────── Big Counter ─────────────── */
function Counter({ to, decimals = 0, prefix = "", suffix = "", duration = 1600, delay = 0 }){
  const [ref, seen] = useInView();
  const v = useTween(to, seen, duration, delay);
  const display = decimals ? v.toFixed(decimals) : Math.round(v).toLocaleString();
  return (
    <span ref={ref} className="j-counter">
      {prefix}{display}{suffix}
    </span>
  );
}

/* ─────────────── Animated Bar Chart ─────────────── */
function BarChart({ data, max, unit = "%", barColor = "var(--accent-ink)", labelWidth = 240 }){
  const [ref, seen] = useInView(0.2);
  const m = max ?? Math.max(...data.map(d => d.rate)) * 1.1;
  return (
    <div ref={ref} className="j-bars" style={{ ['--label-w']: labelWidth + 'px' }}>
      {data.map((d, i) => (
        <div className="j-bar-row" key={d.name}>
          <div className="j-bar-label">{d.name}</div>
          <div className="j-bar-track">
            <div
              className="j-bar-fill"
              style={{
                width: seen ? `${(d.rate / m) * 100}%` : '0%',
                background: d.color || barColor,
                transitionDelay: `${i * 90}ms`,
              }}
            />
          </div>
          <span className="j-bar-value" style={{ opacity: seen ? 1 : 0, transitionDelay: `${i*90 + 600}ms` }}>
            {d.rate}{unit}
          </span>
          {d.n && <div className="j-bar-meta">{d.n}</div>}
        </div>
      ))}
    </div>
  );
}

/* ─────────────── Demographic Pyramid (female | age band | male) ─────────────── */
function Pyramid({ data }){
  const [ref, seen] = useInView(0.25);
  const max = Math.max(...data.flatMap(d => [d.female, d.male])) * 1.05;
  return (
    <div ref={ref} className="j-pyramid">
      <div className="j-pyramid-head">
        <div className="j-pyramid-title left">Women</div>
        <div className="j-pyramid-title center">Age</div>
        <div className="j-pyramid-title right">Men</div>
      </div>
      {data.map((d, i) => (
        <div key={d.band} className="j-pyramid-row">
          <div className="j-pyramid-side left">
            <span className="j-pyramid-val" style={{ opacity: seen ? 1 : 0, transitionDelay:`${i*80+700}ms` }}>{d.female}%</span>
            <div className="j-pyramid-bar female" style={{
              width: seen ? `${(d.female/max)*100}%` : '0%',
              transitionDelay: `${i*80}ms`,
            }} />
          </div>
          <div className="j-pyramid-band">{d.band}</div>
          <div className="j-pyramid-side right">
            <div className="j-pyramid-bar male" style={{
              width: seen ? `${(d.male/max)*100}%` : '0%',
              transitionDelay: `${i*80}ms`,
            }} />
            <span className="j-pyramid-val" style={{ opacity: seen ? 1 : 0, transitionDelay:`${i*80+700}ms` }}>{d.male}%</span>
          </div>
        </div>
      ))}
    </div>
  );
}

/* ─────────────── Donut Chart ─────────────── */
function Donut({ slices, label, sublabel, size = 280 }){
  const [ref, seen] = useInView(0.3);
  const total = slices.reduce((s, x) => s + x.value, 0);
  const C = 2 * Math.PI * 90; // circumference (r=90)
  let acc = 0;
  return (
    <div ref={ref} className="j-donut">
      <svg viewBox="0 0 220 220" width={size} height={size} role="img">
        <circle cx="110" cy="110" r="90" fill="none" stroke="var(--line-soft)" strokeWidth="22" />
        {slices.map((s, i) => {
          const frac = s.value / total;
          const len = C * frac * (seen ? 1 : 0);
          const dash = `${len} ${C}`;
          const offset = -C * (acc / total);
          acc += s.value;
          return (
            <circle key={s.label}
              cx="110" cy="110" r="90"
              fill="none" stroke={s.color} strokeWidth="22"
              strokeDasharray={dash} strokeDashoffset={offset}
              strokeLinecap="butt"
              transform="rotate(-90 110 110)"
              style={{ transition:`stroke-dasharray 1200ms cubic-bezier(.2,.7,.2,1) ${i*120}ms` }}
            />
          );
        })}
      </svg>
      <div className="j-donut-center">
        <div className="j-donut-label">{label}</div>
        {sublabel && <div className="j-donut-sub">{sublabel}</div>}
      </div>
      <div className="j-donut-legend">
        {slices.map(s => (
          <div key={s.label} className="j-donut-legend-row">
            <span className="j-donut-swatch" style={{ background:s.color }} />
            <span className="j-donut-legend-label">{s.label}</span>
            <span className="j-donut-legend-val">{Math.round((s.value/total)*100)}%</span>
          </div>
        ))}
      </div>
    </div>
  );
}

/* ─────────────── Animated line / area chart with axis ─────────────── */
function LineChart({ data, xKey, yKey, height = 320, color = "var(--accent-ink)", areaColor }){
  const [ref, seen] = useInView(0.3);
  const W = 800, H = height, P = { l: 50, r: 30, t: 30, b: 36 };
  const xs = data.map(d => d[xKey]);
  const ys = data.map(d => d[yKey]);
  const xMin = Math.min(...xs), xMax = Math.max(...xs);
  const yMin = 0, yMax = Math.ceil(Math.max(...ys) / 10) * 10 + 5;
  const X = v => P.l + ((v - xMin) / (xMax - xMin)) * (W - P.l - P.r);
  const Y = v => H - P.b - ((v - yMin) / (yMax - yMin)) * (H - P.t - P.b);

  const path = data.map((d, i) => `${i?'L':'M'}${X(d[xKey])},${Y(d[yKey])}`).join(' ');
  const area = `M${X(xs[0])},${H-P.b} ${data.map(d=>`L${X(d[xKey])},${Y(d[yKey])}`).join(' ')} L${X(xs[xs.length-1])},${H-P.b}Z`;

  // path length to animate stroke
  const pathRef = useRef(null);
  const [len, setLen] = useState(0);
  useEffect(() => {
    if (pathRef.current) setLen(pathRef.current.getTotalLength());
  }, []);

  const yTicks = [];
  for (let v = 0; v <= yMax; v += 10) yTicks.push(v);

  return (
    <div ref={ref} className="j-line">
      <svg viewBox={`0 0 ${W} ${H}`} width="100%" preserveAspectRatio="xMidYMid meet" role="img">
        {/* Grid */}
        {yTicks.map(v => (
          <g key={v}>
            <line x1={P.l} x2={W-P.r} y1={Y(v)} y2={Y(v)} stroke="var(--line-soft)" strokeWidth="1"/>
            <text x={P.l-10} y={Y(v)+4} fontSize="11" fill="var(--muted)" textAnchor="end" fontFamily="var(--mono)">{v}%</text>
          </g>
        ))}
        {/* X labels */}
        {data.map(d => (
          <text key={d[xKey]} x={X(d[xKey])} y={H-P.b+22} fontSize="11" fill="var(--muted)" textAnchor="middle" fontFamily="var(--mono)">
            {d[xKey]}
          </text>
        ))}
        {/* Area */}
        <path d={area} fill={areaColor || `color-mix(in oklab, ${color} 14%, transparent)`}
          style={{ opacity: seen ? 1 : 0, transition:'opacity 800ms ease 800ms' }} />
        {/* Line */}
        <path ref={pathRef} d={path} fill="none" stroke={color} strokeWidth="2.5"
          strokeLinecap="round" strokeLinejoin="round"
          style={{
            strokeDasharray: len,
            strokeDashoffset: seen ? 0 : len,
            transition: 'stroke-dashoffset 1800ms cubic-bezier(.2,.7,.2,1)',
          }} />
        {/* Points */}
        {data.map((d, i) => (
          <circle key={d[xKey]} cx={X(d[xKey])} cy={Y(d[yKey])} r="4" fill={color}
            style={{ opacity: seen ? 1 : 0, transition:`opacity 300ms ease ${1200 + i*60}ms` }}/>
        ))}
        {/* Annotation: 2050 projection */}
        {seen && (() => {
          const last = data[data.length-1];
          return (
            <g style={{ opacity: seen ? 1 : 0, transition:'opacity 500ms ease 2200ms' }}>
              <line x1={X(last[xKey])} x2={X(last[xKey])} y1={Y(last[yKey])-8} y2={P.t+10} stroke={color} strokeDasharray="2 3" strokeWidth="1"/>
              <text x={X(last[xKey])} y={P.t+4} fontSize="11" fill="var(--ink)" textAnchor="end" fontFamily="var(--mono)" fontWeight="600">
                projected {last[yKey]}%
              </text>
            </g>
          );
        })()}
      </svg>
    </div>
  );
}

/* ─────────────── US Choropleth Map ─────────────── */
/* Simple grid-tile US (Topojson would be heavier). Each tile is one state. */
const US_TILES = [
  // [code, col, row]
  ['AK',0,0],['ME',10,0],
  ['VT',9,1],['NH',10,1],
  ['WA',1,1],['ID',2,1],['MT',3,1],['ND',4,1],['MN',5,1],['WI',6,1],['MI',7,1],['NY',8,1],['MA',9,2],['RI',10,2],
  ['OR',1,2],['UT',2,2],['WY',3,2],['SD',4,2],['IA',5,2],['IL',6,2],['IN',7,2],['OH',8,2],['PA',9,2.99]/*hack: see CT below*/,['CT',9,3],
  ['CA',1,3],['NV',2,3],['CO',3,3],['NE',4,3],['MO',5,3],['KY',6,3],['WV',7,3],['VA',8,3],['NJ',10,3],
  ['HI',0,4],['AZ',2,4],['NM',3,4],['KS',4,4],['AR',5,4],['TN',6,4],['NC',7,4],['SC',8,4],['DC',9,4],['DE',10,4],
  ['TX',4,5],['OK',5,4.99],['LA',5,5],['MS',6,5],['AL',7,5],['GA',8,5],['FL',9,5],
];
// simplified — re-define cleanly:
const US_GRID = [
  ['AK',0,0],['ME',10,0],
  ['VT',9,1],['NH',10,1],
  ['WA',1,1],['ID',2,1],['MT',3,1],['ND',4,1],['MN',5,1],['WI',6,1],['MI',7,1],['NY',8,1],
  ['OR',1,2],['UT',2,2],['WY',3,2],['SD',4,2],['IA',5,2],['IL',6,2],['IN',7,2],['OH',8,2],['PA',9,2],['MA',10,2],
  ['CA',1,3],['NV',2,3],['CO',3,3],['NE',4,3],['MO',5,3],['KY',6,3],['WV',7,3],['VA',8,3],['NJ',9,3],['CT',10,3],
  ['HI',0,4],['AZ',2,4],['NM',3,4],['KS',4,4],['AR',5,4],['TN',6,4],['NC',7,4],['SC',8,4],['MD',9,4],['DE',10,4],['RI',9.5,2.5],
  ['OK',4,5],['TX',3,5],['LA',5,5],['MS',6,5],['AL',7,5],['GA',8,5],['FL',9,5],['DC',8.5,4],
];

// FIPS code → state postal code (used to look up data values)
const FIPS_TO_CODE = {
  "01":"AL","02":"AK","04":"AZ","05":"AR","06":"CA","08":"CO","09":"CT",
  "10":"DE","11":"DC","12":"FL","13":"GA","15":"HI","16":"ID","17":"IL",
  "18":"IN","19":"IA","20":"KS","21":"KY","22":"LA","23":"ME","24":"MD",
  "25":"MA","26":"MI","27":"MN","28":"MS","29":"MO","30":"MT","31":"NE",
  "32":"NV","33":"NH","34":"NJ","35":"NM","36":"NY","37":"NC","38":"ND",
  "39":"OH","40":"OK","41":"OR","42":"PA","44":"RI","45":"SC","46":"SD",
  "47":"TN","48":"TX","49":"UT","50":"VT","51":"VA","53":"WA","54":"WV",
  "55":"WI","56":"WY"
};

function USChoropleth({ data }){
  const [ref, seen] = useInView(0.2);
  const [hover, setHover] = useState(null);
  const [topoData, setTopoData] = useState(null);

  React.useEffect(() => {
    fetch('https://cdn.jsdelivr.net/npm/us-atlas@3/states-10m.json')
      .then(r => r.json())
      .then(setTopoData)
      .catch(err => console.error('US map data failed to load:', err));
  }, []);

  const values = Object.values(data);
  const min = Math.min(...values), max = Math.max(...values);
  const colorFor = v => {
    const t = (v - min) / (max - min);
    return `oklch(${75 - t*40}% ${0.06 + t*0.10} ${230 + t*30})`;
  };

  // Build features + path generator once topojson is loaded
  const { features, pathGen, error } = React.useMemo(() => {
    if (!topoData) return { features: [], pathGen: null, error: null };
    try {
      if (!window.topojson || !window.d3 || !window.d3.geoAlbersUsa) {
        return { features: [], pathGen: null, error: 'mapping libs missing' };
      }
      const features = window.topojson.feature(topoData, topoData.objects.states).features;
      const projection = window.d3.geoAlbersUsa().scale(1300).translate([487.5, 305]);
      const pathGen = window.d3.geoPath(projection);
      return { features, pathGen, error: null };
    } catch (e) {
      console.error('US map render failed:', e);
      return { features: [], pathGen: null, error: e.message };
    }
  }, [topoData]);

  return (
    <div ref={ref} className="j-map-wrap">
      <svg viewBox="0 0 975 610" width="100%" preserveAspectRatio="xMidYMid meet" role="img" aria-label="US TMD prevalence by state">
        {pathGen && features.map((feature, i) => {
          const code = FIPS_TO_CODE[feature.id];
          if (!code) return null;
          const v = data[code];
          const isHovered = hover?.code === code;
          return (
            <path key={feature.id}
              d={pathGen(feature)}
              fill={seen && v != null ? colorFor(v) : 'var(--bg-2)'}
              stroke={isHovered ? 'var(--ink)' : 'var(--paper)'}
              strokeWidth={isHovered ? 1.5 : 0.6}
              onMouseEnter={(e) => {
                const bbox = e.currentTarget.getBBox();
                setHover({ code, v: v ?? 0, x: bbox.x + bbox.width/2, y: bbox.y });
              }}
              onMouseLeave={() => setHover(null)}
              style={{ cursor:'default', transition:`fill 600ms ease ${i*8}ms, stroke 150ms` }}
            />
          );
        })}
        {!pathGen && (
          <text x="487" y="305" textAnchor="middle" fill="var(--muted)" fontFamily="var(--mono)" fontSize="14">
            {error ? 'Map unavailable' : 'Loading map…'}
          </text>
        )}
      </svg>
      <div className="j-map-legend">
        <span className="j-map-legend-label">Lower</span>
        <div className="j-map-legend-bar" />
        <span className="j-map-legend-label">Higher</span>
        <span className="j-map-legend-range">{min.toFixed(1)}%–{max.toFixed(1)}%</span>
      </div>
      {hover && (
        <div className="j-map-tip" style={{ left: `calc(${(hover.x/975)*100}% - 60px)`, top: `calc(${(hover.y/610)*100}% - 56px)` }}>
          <div className="j-map-tip-code">{hover.code}</div>
          <div className="j-map-tip-val">{hover.v}%</div>
        </div>
      )}
    </div>
  );
}

/* ─────────────── World Dot Map ─────────────── */
/* abstract: continent dots sized by prevalence */
function WorldDotMap({ data }){
  const [ref, seen] = useInView(0.3);
  // approximate centroids on a 1000x500 equirectangular projection
  const POS = {
    "Europe":        { x: 510, y: 165 },
    "Asia":          { x: 700, y: 220 },
    "South America": { x: 330, y: 360 },
    "Africa":        { x: 530, y: 290 },
    "North America": { x: 230, y: 200 },
  };
  const max = Math.max(...data.map(d => d.rate));
  // Slug for unique gradient IDs (handles spaces in continent names)
  const slug = (s) => s.toLowerCase().replace(/[^a-z]/g, '');
  return (
    <div ref={ref} className="j-world">
      <svg viewBox="0 0 1000 500" width="100%" preserveAspectRatio="xMidYMid meet" role="img">
        <defs>
          <pattern id="dotgrid" x="0" y="0" width="14" height="14" patternUnits="userSpaceOnUse">
            <circle cx="2" cy="2" r="1.1" fill="var(--line)" />
          </pattern>
          {data.map(d => {
            const color = d.color || "var(--accent-ink)";
            return (
              <radialGradient key={`g-${slug(d.name)}`} id={`hot-${slug(d.name)}`} cx="50%" cy="50%" r="50%">
                <stop offset="0%" stopColor={color} stopOpacity=".95"/>
                <stop offset="60%" stopColor={color} stopOpacity=".25"/>
                <stop offset="100%" stopColor={color} stopOpacity="0"/>
              </radialGradient>
            );
          })}
        </defs>
        <rect width="1000" height="500" fill="url(#dotgrid)" />
        {data.map((d, i) => {
          const p = POS[d.name];
          if (!p) return null;
          const r = 18 + (d.rate / max) * 64;
          const color = d.color || "var(--accent-ink)";
          // Use dark text on lighter circles so it stays readable
          const isLight = d.name === "North America" || d.name === "Africa";
          return (
            <g key={d.name} style={{ opacity: seen ? 1 : 0, transform: seen ? 'scale(1)' : 'scale(0)', transformOrigin:`${p.x}px ${p.y}px`, transition:`opacity 600ms ease, transform 900ms cubic-bezier(.2,.7,.2,1) ${i*150}ms` }}>
              <circle cx={p.x} cy={p.y} r={r * 1.4} fill={`url(#hot-${slug(d.name)})`} />
              <circle cx={p.x} cy={p.y} r={r} fill={color} opacity=".92" />
              <text x={p.x} y={p.y - r - 8} fill="var(--ink)" fontSize="18" textAnchor="middle" fontFamily="var(--serif)" fontWeight="500">
                {d.name}
              </text>
              <text x={p.x} y={p.y + 5} fill={isLight ? "var(--ink)" : "var(--paper)"} fontSize="20" textAnchor="middle" fontFamily="var(--serif)" fontWeight="500">
                {d.rate}%
              </text>
            </g>
          );
        })}
      </svg>
    </div>
  );
}

/* ─────────────── Stacked Bar (single horizontal) ─────────────── */
function StackedBar({ segments, height = 56 }){
  const [ref, seen] = useInView(0.4);
  const total = segments.reduce((s, x) => s + x.value, 0);
  return (
    <div ref={ref} className="j-stack">
      <div className="j-stack-bar" style={{ height }}>
        {segments.map((seg, i) => (
          <div key={seg.label} className="j-stack-seg" style={{
            width: seen ? `${(seg.value/total)*100}%` : '0%',
            background: seg.color,
            transitionDelay: `${i*180}ms`,
          }}>
            <span className="j-stack-pct">{Math.round((seg.value/total)*100)}%</span>
          </div>
        ))}
      </div>
      <div className="j-stack-legend">
        {segments.map(seg => (
          <div key={seg.label} className="j-stack-legend-row">
            <span className="j-stack-swatch" style={{ background:seg.color }} />
            <span>{seg.label}</span>
          </div>
        ))}
      </div>
    </div>
  );
}

Object.assign(window, { Counter, BarChart, Pyramid, Donut, LineChart, USChoropleth, WorldDotMap, StackedBar, useInView, useTween });
