/* TCM Herbs explorer — main React app */
const { useState, useEffect, useRef, useMemo, useCallback, useLayoutEffect } = React;

const HERBS = window.HERBS;
const BOTANICAL = window.BOTANICAL;

/* Deterministic per-herb visual variation so herbs sharing a base illustration
   render distinctly. Hash the herb id into rotation, scale, and a subtle hue. */
function hashStr(s) {
  let h = 2166136261;
  for (let i = 0; i < s.length; i++) h = Math.imul(h ^ s.charCodeAt(i), 16777619);
  return h >>> 0;
}
function HerbIcon({ herb, size = 38 }) {
  const svg = BOTANICAL[herb.illustration] || BOTANICAL.ginger;
  return (
    <svg viewBox="0 0 100 100" width={size} height={size}>
      <g dangerouslySetInnerHTML={{__html: svg}} />
    </svg>
  );
}

/* ─── Classification configurations ─── */
const VIEWS = {
  category: {
    label: 'Category', cn: '功效',
    desc: 'How each herb works in classical formulas',
    groups: [
      { id: 'tonifying', label: 'Tonifying', cn: '补益' },
      { id: 'heat-clearing', label: 'Heat-clearing', cn: '清热' },
      { id: 'warming', label: 'Warming Interior', cn: '温里' },
      { id: 'wind-warm', label: 'Release Wind-Cold', cn: '辛温解表' },
      { id: 'wind-cool', label: 'Release Wind-Heat', cn: '辛凉解表' },
      { id: 'qi-regulating', label: 'Qi Regulating', cn: '理气' },
      { id: 'blood-moving', label: 'Blood-moving', cn: '活血' },
      { id: 'damp-draining', label: 'Damp-draining', cn: '利湿' },
      { id: 'phlegm', label: 'Phlegm Transforming', cn: '化痰' },
      { id: 'wind-extinguishing', label: 'Wind-extinguishing', cn: '息风' },
      { id: 'astringent', label: 'Astringent', cn: '收涩' },
      { id: 'food-stagnation', label: 'Digestive', cn: '消食' },
      { id: 'purgative', label: 'Purgative', cn: '泻下' }
    ],
    field: 'category'
  },
  rarity: {
    label: 'Rarity', cn: '珍稀',
    desc: 'From everyday pharmacy staples to vanishing wild treasures',
    groups: [
      { id: '1', label: 'Common', cn: '常见', detail: 'Cultivated at scale, in every clinic.' },
      { id: '2', label: 'Frequent', cn: '常用', detail: 'Standard pharmacy stock.' },
      { id: '3', label: 'Uncommon', cn: '少见', detail: 'Regional or harder to source.' },
      { id: '4', label: 'Rare', cn: '稀有', detail: 'Premium-grade, often graded.' },
      { id: '5', label: 'Endangered / Treasured', cn: '极珍', detail: 'Wild-collected, protected, or fabled.' }
    ],
    field: 'rarity', valueType: 'string'
  },
  origin: {
    label: 'Origin', cn: '产地',
    desc: 'Province or region where the best material grows',
    groups: null, // dynamic
    field: 'origin'
  },
  price: {
    label: 'Market Value', cn: '价值',
    desc: 'Approximate wholesale price, USD per kilogram',
    groups: [
      { id: 'p1', label: 'Under $20', cn: '$ — daily', test: p => p < 20 },
      { id: 'p2', label: '$20–$50', cn: '$$ — common', test: p => p >= 20 && p < 50 },
      { id: 'p3', label: '$50–$200', cn: '$$$ — premium', test: p => p >= 50 && p < 200 },
      { id: 'p4', label: '$200–$1,000', cn: '$$$$ — luxury', test: p => p >= 200 && p < 1000 },
      { id: 'p5', label: 'Over $1,000', cn: '$$$$$ — treasure', test: p => p >= 1000 }
    ],
    field: 'price', custom: true
  },
  nature: {
    label: 'Nature', cn: '四气',
    desc: 'Thermal energetics: cold, cool, neutral, warm, hot',
    groups: [
      { id: 'cold', label: 'Cold', cn: '寒' },
      { id: 'cool', label: 'Cool', cn: '凉' },
      { id: 'neutral', label: 'Neutral', cn: '平' },
      { id: 'warm', label: 'Warm', cn: '温' },
      { id: 'hot', label: 'Hot', cn: '热' }
    ],
    field: 'nature'
  },
  taste: {
    label: 'Taste', cn: '五味',
    desc: 'The five flavours that direct an herb\u2019s action',
    groups: [
      { id: 'sweet', label: 'Sweet', cn: '甘' },
      { id: 'bitter', label: 'Bitter', cn: '苦' },
      { id: 'sour', label: 'Sour', cn: '酸' },
      { id: 'pungent', label: 'Pungent', cn: '辛' },
      { id: 'salty', label: 'Salty', cn: '咸' }
    ],
    field: 'taste'
  },
  part: {
    label: 'Part Used', cn: '药用部位',
    desc: 'The plant or animal part that becomes medicine',
    groups: [
      { id: 'root', label: 'Root', cn: '根' },
      { id: 'rhizome', label: 'Rhizome', cn: '根茎' },
      { id: 'flower', label: 'Flower', cn: '花' },
      { id: 'fruit', label: 'Fruit', cn: '果实' },
      { id: 'seed', label: 'Seed', cn: '种子' },
      { id: 'leaf', label: 'Leaf', cn: '叶' },
      { id: 'bark', label: 'Bark', cn: '皮' },
      { id: 'peel', label: 'Peel', cn: '果皮' },
      { id: 'twig', label: 'Twig', cn: '枝' },
      { id: 'stem', label: 'Stem', cn: '茎' },
      { id: 'tuber', label: 'Tuber', cn: '块根' },
      { id: 'bulb', label: 'Bulb', cn: '鳞茎' },
      { id: 'sprout', label: 'Sprout', cn: '芽' },
      { id: 'fungus', label: 'Fungus', cn: '菌' },
      { id: 'sclerotium', label: 'Sclerotium', cn: '菌核' },
      { id: 'fungus-larva', label: 'Fungus + larva', cn: '虫草' },
      { id: 'antler', label: 'Antler', cn: '角' },
      { id: 'gelatin', label: 'Animal gelatin', cn: '胶' },
      { id: 'bezoar', label: 'Bezoar', cn: '黄' },
      { id: 'mineral', label: 'Mineral', cn: '矿' },
      { id: 'whole plant', label: 'Whole plant', cn: '全草' }
    ],
    field: 'part'
  }
};

/* derive origin groups from data */
{
  const provs = [...new Set(HERBS.map(h => h.origin))].sort();
  VIEWS.origin.groups = provs.map(p => ({ id: p, label: p, cn: '' }));
}

/* ─── Layout: bin herbs into clusters and lay them out ─── */
function computeLayout(view, viewport, herbs) {
  const cfg = VIEWS[view];
  const groups = cfg.groups;
  const buckets = groups.map(g => ({ ...g, herbs: [] }));

  herbs.forEach(h => {
    let idx = -1;
    if (cfg.custom) {
      idx = groups.findIndex(g => g.test(h[cfg.field]));
    } else if (cfg.valueType === 'string') {
      idx = groups.findIndex(g => g.id === String(h[cfg.field]));
    } else {
      idx = groups.findIndex(g => g.id === h[cfg.field]);
    }
    if (idx >= 0) buckets[idx].herbs.push(h);
  });

  // drop empty groups (esp. for taste, part, etc.)
  const live = buckets.filter(b => b.herbs.length > 0);

  // grid of cluster cards
  const W = viewport.width;
  // choose columns to fit clusters compactly
  const totalHerbs = live.reduce((s, b) => s + b.herbs.length, 0);
  const tileW = 76; // herb tile spacing (horizontal)
  const tileH = 92;
  const padding = { top: 76, right: 16, bottom: 16, left: 16 };

  // column count based on viewport and number of clusters
  let cols;
  if (W < 720) cols = Math.min(2, live.length);
  else if (W < 1100) cols = Math.min(3, live.length);
  else if (live.length <= 4) cols = live.length;
  else if (live.length <= 9) cols = 3;
  else if (live.length <= 12) cols = 4;
  else cols = Math.min(5, live.length);

  const gutter = 28;
  const colWidth = (W - gutter * (cols - 1)) / cols;
  const tilesPerRowInCluster = Math.max(3, Math.floor((colWidth - padding.left - padding.right) / tileW));

  const positions = {};
  const clusters = [];
  let rowY = 0;
  let rowTallest = 0;

  live.forEach((b, i) => {
    const col = i % cols;
    const rowIdx = Math.floor(i / cols);
    if (col === 0 && rowIdx > 0) { rowY += rowTallest + 56; rowTallest = 0; }

    const x0 = col * (colWidth + gutter);
    const y0 = rowY;

    const tilesPerRow = tilesPerRowInCluster;
    const rowsNeeded = Math.ceil(b.herbs.length / tilesPerRow);
    const clusterH = padding.top + rowsNeeded * tileH + padding.bottom;

    clusters.push({ ...b, x: x0, y: y0, w: colWidth, h: clusterH });

    // sort herbs within cluster by rarity then price (rarer + dearer goes later)
    const sorted = [...b.herbs].sort((a, c) => (a.rarity - c.rarity) || (a.price - c.price));
    sorted.forEach((h, idx) => {
      const r = Math.floor(idx / tilesPerRow);
      const c2 = idx % tilesPerRow;
      const tilesInThisRow = Math.min(tilesPerRow, b.herbs.length - r * tilesPerRow);
      const innerW = colWidth - padding.left - padding.right;
      const offsetX = (innerW - tilesInThisRow * tileW) / 2; // center each row
      positions[h.id] = {
        x: x0 + padding.left + offsetX + c2 * tileW + tileW / 2 - 28,
        y: y0 + padding.top + r * tileH
      };
    });

    rowTallest = Math.max(rowTallest, clusterH);
  });

  const totalH = rowY + rowTallest;
  return { positions, clusters, totalH };
}

/* ─── Hover detail card ─── */
function DetailCard({ herb, x, y, onClose }) {
  if (!herb) return null;
  const W = 340, H = 360;
  const vw = window.innerWidth, vh = window.innerHeight;
  let left = x + 20, top = y + 20;
  if (left + W > vw - 16) left = x - W - 20;
  if (top + H > vh - 16) top = vh - H - 16;
  if (top < 16) top = 16;

  const rarityLabel = ['', 'Common', 'Frequent', 'Uncommon', 'Rare', 'Endangered'][herb.rarity];
  const priceFmt = herb.price >= 1000 ? `$${herb.price.toLocaleString()}/kg` : `$${herb.price}/kg`;

  return (
    <div className={`detail-card ${onClose ? 'pinned' : ''}`} style={{ left, top }}>
      {onClose && <button className="detail-close" onClick={onClose} aria-label="Close">×</button>}
      <div className="zh">{herb.zh}</div>
      <div className="pinyin">{herb.pinyin}</div>
      <div className="latin">{herb.latin}</div>
      <div className="en">{herb.en}</div>
      <div className="uses">{herb.uses}</div>
      <dl className="props">
        <dt>Nature</dt><dd>{herb.nature}</dd>
        <dt>Taste</dt><dd>{herb.taste}</dd>
        <dt>Meridian</dt><dd>{herb.meridian}</dd>
        <dt>Part Used</dt><dd>{herb.part}</dd>
        <dt>Origin</dt><dd>{herb.origin}</dd>
        <dt>Rarity</dt>
        <dd>
          {rarityLabel}{' '}
          <span className="rarity-pip">
            {[1,2,3,4,5].map(i => <span key={i} className={i <= herb.rarity ? 'on' : ''} />)}
          </span>
        </dd>
        <dt>Value</dt><dd>{priceFmt}</dd>
      </dl>
    </div>
  );
}

/* ─── Compare tray ─── */
function CompareTray({ items, onRemove, onClose, open }) {
  const slots = [0, 1, 2].map(i => items[i] || null);
  return (
    <div className={`compare-tray ${open ? 'open' : ''}`}>
      <div className="compare-inner">
        <div className="compare-title">
          Side by side
          <strong>Compare</strong>
        </div>
        <div className="compare-cards">
          {slots.map((h, i) => h ? (
            <div className="compare-card" key={h.id}>
              <button className="x" onClick={() => onRemove(h.id)}>×</button>
              <div className="zh">{h.zh}</div>
              <div className="pinyin">{h.pinyin} — {h.en}</div>
              <dl>
                <dt>Nature</dt><dd>{h.nature} · {h.taste}</dd>
                <dt>Origin</dt><dd>{h.origin}</dd>
                <dt>Rarity</dt><dd>{['','Common','Frequent','Uncommon','Rare','Endangered'][h.rarity]}</dd>
                <dt>Value</dt><dd>${h.price.toLocaleString()}/kg</dd>
                <dt>Uses</dt><dd>{h.uses.split('.')[0]}.</dd>
              </dl>
            </div>
          ) : (
            <div className="compare-card empty" key={'e'+i}>Click a herb to add</div>
          ))}
        </div>
        <button className="compare-close" onClick={onClose}>×</button>
      </div>
    </div>
  );
}

/* ─── China map (real GeoJSON admin boundaries, fetched at runtime) ─── */

// Pinyin → Chinese province name mapping (for lookup in GeoJSON properties)
const PROV_CN = {
  'Heilongjiang': '黑龙江省', 'Jilin': '吉林省', 'Liaoning': '辽宁省',
  'Inner Mongolia': '内蒙古自治区', 'Xinjiang': '新疆维吾尔自治区',
  'Tibet': '西藏自治区', 'Qinghai': '青海省', 'Gansu': '甘肃省',
  'Ningxia': '宁夏回族自治区', 'Shaanxi': '陕西省', 'Shanxi': '山西省',
  'Hebei': '河北省', 'Shandong': '山东省', 'Henan': '河南省',
  'Sichuan': '四川省', 'Chongqing': '重庆市', 'Hubei': '湖北省',
  'Anhui': '安徽省', 'Jiangsu': '江苏省', 'Hunan': '湖南省',
  'Jiangxi': '江西省', 'Zhejiang': '浙江省', 'Fujian': '福建省',
  'Guizhou': '贵州省', 'Yunnan': '云南省', 'Guangxi': '广西壮族自治区',
  'Guangdong': '广东省', 'Hainan': '海南省', 'Beijing': '北京市',
  'Tianjin': '天津市', 'Shanghai': '上海市', 'Hong Kong': '香港特别行政区',
  'Macau': '澳门特别行政区', 'Taiwan': '台湾省'
};

// Single-character license-plate abbreviation per province
const PROV_PLATE = {
  'Heilongjiang': '黑', 'Jilin': '吉', 'Liaoning': '辽',
  'Inner Mongolia': '蒙', 'Xinjiang': '新', 'Tibet': '藏',
  'Qinghai': '青', 'Gansu': '甘', 'Ningxia': '宁', 'Shaanxi': '陕',
  'Shanxi': '晋', 'Hebei': '冀', 'Shandong': '鲁', 'Henan': '豫',
  'Jiangsu': '苏', 'Anhui': '皖', 'Zhejiang': '浙', 'Fujian': '闽',
  'Jiangxi': '赣', 'Hubei': '鄂', 'Hunan': '湘', 'Sichuan': '川',
  'Chongqing': '渝', 'Guizhou': '贵', 'Yunnan': '云', 'Guangxi': '桂',
  'Guangdong': '粤', 'Hainan': '琼', 'Beijing': '京', 'Tianjin': '津',
  'Shanghai': '沪', 'Hong Kong': '港', 'Macau': '澳', 'Taiwan': '台'
};

/* Albers-like equal-area projection for China, returns [x,y] in viewBox units */
/* Equirectangular-style projection tuned for China.
   Matches the flatter look of the chinaPlates reference (standard ~35°N parallel).
   Returns [x,y] in viewBox units. */
function projectChina(lon, lat, vbW, vbH) {
  // Bounds of mainland China
  const LON_MIN = 73, LON_MAX = 135;   // ~62° span
  const LAT_MIN = 18, LAT_MAX = 54;    // ~36° span
  const phi0 = 35 * Math.PI / 180;     // standard parallel
  const k = Math.cos(phi0);            // x-compression at center latitude

  // Project lon/lat to a flat plane (degrees -> pseudo-meters)
  const xp = (lon - (LON_MIN + LON_MAX) / 2) * k;
  const yp = -(lat - (LAT_MIN + LAT_MAX) / 2);

  // Fit-to-viewBox scaling
  const xSpan = (LON_MAX - LON_MIN) * k;
  const ySpan = (LAT_MAX - LAT_MIN);
  const scale = Math.min(vbW * 0.95 / xSpan, vbH * 0.95 / ySpan);

  const x = xp * scale + vbW / 2;
  const y = yp * scale + vbH * 0.52;
  return [x, y];
}

function geoToPath(geometry, vbW, vbH) {
  const draw = (rings) => rings.map(ring => {
    return ring.map((pt, i) => {
      const [x, y] = projectChina(pt[0], pt[1], vbW, vbH);
      return (i === 0 ? 'M' : 'L') + x.toFixed(1) + ',' + y.toFixed(1);
    }).join('') + 'Z';
  }).join('');

  if (geometry.type === 'Polygon') return draw(geometry.coordinates);
  if (geometry.type === 'MultiPolygon') {
    return geometry.coordinates.map(poly => draw(poly)).join('');
  }
  return '';
}

function centroidOfGeom(geometry, vbW, vbH) {
  // Use polygon area-weighted centroid of the LARGEST ring only.
  let ring = [];
  if (geometry.type === 'Polygon') {
    ring = geometry.coordinates[0] || [];
  } else if (geometry.type === 'MultiPolygon') {
    // pick the polygon with largest absolute area (in projected coords)
    let bestRing = null, bestArea = 0;
    geometry.coordinates.forEach(poly => {
      const r = poly[0];
      // shoelace area in projected coords
      let a = 0;
      for (let i = 0; i < r.length - 1; i++) {
        const [x1, y1] = projectChina(r[i][0], r[i][1], vbW, vbH);
        const [x2, y2] = projectChina(r[i+1][0], r[i+1][1], vbW, vbH);
        a += x1 * y2 - x2 * y1;
      }
      a = Math.abs(a / 2);
      if (a > bestArea) { bestArea = a; bestRing = r; }
    });
    ring = bestRing || [];
  }
  if (ring.length < 3) return [vbW / 2, vbH / 2];
  // area-weighted centroid (proper polygon centroid)
  let cx = 0, cy = 0, A = 0;
  for (let i = 0; i < ring.length - 1; i++) {
    const [x1, y1] = projectChina(ring[i][0], ring[i][1], vbW, vbH);
    const [x2, y2] = projectChina(ring[i+1][0], ring[i+1][1], vbW, vbH);
    const f = x1 * y2 - x2 * y1;
    cx += (x1 + x2) * f;
    cy += (y1 + y2) * f;
    A += f;
  }
  A = A / 2;
  if (Math.abs(A) < 1e-6) return [vbW / 2, vbH / 2];
  return [cx / (6 * A), cy / (6 * A)];
}

function ChinaMap({ herbs, hovered, setHovered, onProvinceClick }) {
  const [geo, setGeo] = useState(null);
  const VB_W = 820, VB_H = 600;

  useEffect(() => {
    // Try a couple of CDN-hosted GeoJSON sources for China provinces
    const sources = ['china-provinces.json'];
    (async () => {
      for (const url of sources) {
        try {
          const r = await fetch(url);
          if (!r.ok) continue;
          const data = await r.json();
          setGeo(data);
          return;
        } catch (e) { /* try next */ }
      }
    })();
  }, []);

  const counts = useMemo(() => {
    const m = {};
    herbs.forEach(h => { m[h.origin] = (m[h.origin] || 0) + 1; });
    return m;
  }, [herbs]);

  /* Extract feature → pinyin label mapping */
  const features = useMemo(() => {
    if (!geo || !geo.features) return [];
    return geo.features.filter(f => {
      // Drop the 9-dash-line feature (adcode 100000_JD, no name)
      const props = f.properties || {};
      const ad = String(props.adcode || '');
      if (ad.includes('JD') || ad === '100000_JD') return false;
      if (!props.name) return false;
      return true;
    }).map(f => {
      const props = f.properties || {};
      const cn = props.name || props.NAME_1 || props.fullname || '';
      // Match against PROV_CN values, allowing partial matches
      let pinyin = null;
      for (const [py, zh] of Object.entries(PROV_CN)) {
        if (cn === zh || cn.startsWith(zh.slice(0, 2)) || zh.startsWith(cn)) {
          pinyin = py;
          break;
        }
      }
      const path = geoToPath(f.geometry, VB_W, VB_H);
      const [cx, cy] = centroidOfGeom(f.geometry, VB_W, VB_H);
      return { id: pinyin || cn, cn, pinyin, path, cx, cy, count: pinyin ? (counts[pinyin] || 0) : 0 };
    });
  }, [geo, counts]);

  return (
    <div className="map-section reveal">
      <h3>
        <span className="cn">道地药材</span>
        By Region
      </h3>
      <p className="sub">Hover a region to see how many herbs in this collection trace their finest grade to that province.</p>
      <svg className="map-svg" viewBox={`0 0 ${VB_W} ${VB_H + 55}`} xmlns="http://www.w3.org/2000/svg">
        {!geo && (
          <text x={VB_W/2} y={VB_H/2} textAnchor="middle" className="map-label" style={{ fontSize: 12 }}>
            Loading map…
          </text>
        )}
        {features.sort((a,b) => {
          const small = ['Beijing','Tianjin','Shanghai','Hong Kong','Macau','Ningxia'];
          const aS = small.includes(a.pinyin), bS = small.includes(b.pinyin);
          return (aS?1:0) - (bS?1:0);
        }).map((f, i) => (
          <path
            key={i}
            d={f.path}
            className={`map-region ${hovered === f.pinyin ? 'active' : ''} ${f.count > 0 ? 'has-herbs' : ''}`}
            onMouseEnter={() => f.pinyin && setHovered(f.pinyin)}
            onMouseLeave={() => setHovered(null)}
            onClick={() => f.pinyin && f.count > 0 && onProvinceClick && onProvinceClick(f.pinyin)}
            style={{ cursor: f.pinyin && f.count > 0 ? 'pointer' : 'default' }}
          />
        ))}
        {(() => {
          // Nudge offsets calibrated to match reference (xuanx1 chinaPlates):
          // labels sit visually centered in each province's main body.
          const NUDGE = {
            'Inner Mongolia': { dx: -50, dy: 50 },  // pulled further west and down
            'Gansu':           { dx: 50, dy: 50 },   // 50 right, 50 down
            'Heilongjiang':   { dx: 5, dy: 10 },
            'Jilin':          { dx: 0, dy: 10 },
            'Xinjiang':       { dx: 0, dy: 20 },
            'Tibet':          { dx: 0, dy: 5 },
            'Qinghai':        { dx: 0, dy: 0 },
            'Sichuan':        { dx: -10, dy: 15 },
            'Yunnan':         { dx: 0, dy: 10 },
            'Guangxi':        { dx: 0, dy: 10 },
            'Fujian':         { dx: 0, dy: 10 },
            'Guangdong':      { dx: -8, dy: 0 },
            'Hainan':         { dx: 0, dy: 4 },     // below island with leader
            'Beijing':        { dx: -10, dy: -6 },
            'Tianjin':        { dx: 12, dy: 2 },
            'Hebei':          { dx: -10, dy: 32 },  // pulled down more, slight left
            'Shanghai':       { dx: 14, dy: 4 },
            'Hong Kong':      { dx: 10, dy: 8 },
            'Macau':          { dx: -10, dy: 12 },
            'Ningxia':        { dx: -4, dy: 0 },
            'Shaanxi':        { dx: 0, dy: 4 },
            'Shanxi':         { dx: 0, dy: 0 },
            'Taiwan':         { dx: 4, dy: 0 },
          };
          const FS = 10;
          const nudge = (f) => {
            const n = NUDGE[f.pinyin] || { dx: 0, dy: 0 };
            return { cx: f.cx + n.dx, cy: f.cy + n.dy, fs: FS };
          };
          return (
            <>
              {features.filter(f => f.count > 0).map((f, i) => {
                const { cx, cy, fs } = nudge(f);
                const r = 2 + Math.min(f.count * 1.4, 9);
                return <circle key={'p'+i} cx={cx} cy={cy - fs - 4 - r} r={r} className="map-pip" opacity="0.6"
                  style={{ cursor: 'pointer', pointerEvents: 'all' }}
                  onClick={() => onProvinceClick && onProvinceClick(f.pinyin)}
                  onMouseEnter={() => setHovered(f.pinyin)}
                  onMouseLeave={() => setHovered(null)}
                />;
              })}
              {features.filter(f => f.pinyin).map((f, i) => {
                const { cx, cy, fs } = nudge(f);
                // Strip 省 / 市 / 自治区 / 特别行政区 suffixes for cleaner display
                const zh = (f.cn || '')
                  .replace(/省$/,'').replace(/市$/,'')
                  .replace(/自治区$/,'').replace(/特别行政区$/,'')
                  .replace(/回族$/,'').replace(/壮族$/,'').replace(/维吾尔$/,'');
                return (
                  <g key={'t'+i} pointerEvents="none">
                    <text x={cx} y={cy - 1} className="map-label-cn" textAnchor="middle" style={{ fontSize: fs + 1, fontWeight: 500 }}>{zh}</text>
                    <text x={cx} y={cy + fs + 2} className="map-label" textAnchor="middle" style={{ fontSize: fs - 2, fontStyle: 'italic' }}>{f.pinyin}</text>
                  </g>
                );
              })}
            </>
          );
        })()}
        {hovered && counts[hovered] > 0 && (
          <text x={VB_W/2} y={VB_H + 29} textAnchor="middle" style={{ fontSize: 14, fill: 'var(--ink)', fontFamily: 'var(--serif)', fontStyle: 'italic' }}>
            {hovered} · <tspan style={{ fill: 'var(--accent)' }}>{PROV_CN[hovered] || ''}</tspan> — {counts[hovered]} herb{counts[hovered] > 1 ? 's' : ''}
          </text>
        )}
      </svg>
    </div>
  );
}

/* ─── Story mode controller ─── */
const STORY_STEPS = [
  { view: 'rarity', title: 'From staple to treasure', note: 'Reorder by rarity. Notice the long tail.' },
  { view: 'category', title: 'How they work', note: 'Tonifying, heat-clearing, blood-moving — the classical functions.' },
  { view: 'nature', title: 'Hot or cold?', note: 'Each herb has a thermal nature that guides its use.' },
  { view: 'taste', title: 'Five tastes', note: 'Bitter drains, sweet tonifies, sour astringes…' },
  { view: 'part', title: 'Roots, flowers, fungi', note: 'The medicine can be any part — even an animal bezoar.' },
  { view: 'origin', title: 'Terroir', note: '\u9053\u5730 — the right province makes the herb.' },
  { view: 'price', title: 'A market of extremes', note: 'From three dollars a kilo to sixty thousand.' }
];

/* ─── Main app ─── */
function App() {
  const [view, setView] = useState('category');
  const [search, setSearch] = useState('');
  const [hover, setHover] = useState(null); // {herb, x, y}
  const [pinned, setPinned] = useState(null); // pinned detail card on click
  const [openCluster, setOpenCluster] = useState(null); // cluster id whose herb-list panel is open
  const [openProvince, setOpenProvince] = useState(null); // province pinyin whose herb-list panel is open
  const [compare, setCompare] = useState([]); // herb ids
  const [compareMode, setCompareMode] = useState(false);
  const [story, setStory] = useState(null); // index into STORY_STEPS
  const [mapHover, setMapHover] = useState(null);
  const [viewport, setViewport] = useState({ width: 1200 });
  const chartRef = useRef(null);

  /* viewport tracking */
  useEffect(() => {
    const onResize = () => {
      if (chartRef.current) setViewport({ width: chartRef.current.clientWidth });
    };
    onResize();
    window.addEventListener('resize', onResize);
    return () => window.removeEventListener('resize', onResize);
  }, []);

  /* dismiss pinned card on Esc */
  useEffect(() => {
    if (!pinned) return;
    const onKey = e => { if (e.key === 'Escape') setPinned(null); };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [pinned]);

  /* scroll-reveal on .reveal sections */
  useEffect(() => {
    const els = document.querySelectorAll('.reveal');
    if (!('IntersectionObserver' in window)) {
      els.forEach(el => el.classList.add('in'));
      return;
    }
    const io = new IntersectionObserver((entries) => {
      entries.forEach(en => {
        if (en.isIntersecting) {
          en.target.classList.add('in');
          io.unobserve(en.target);
        }
      });
    }, { rootMargin: '0px 0px -10% 0px', threshold: 0.08 });
    els.forEach(el => io.observe(el));
    return () => io.disconnect();
  }, []);

  /* filter by search + map hover */
  const filteredHerbs = useMemo(() => {
    const q = search.trim().toLowerCase();
    return HERBS.filter(h => {
      if (mapHover && h.origin !== mapHover) return false;
      if (!q) return true;
      return (
        h.zh.includes(q) ||
        h.pinyin.toLowerCase().includes(q) ||
        h.en.toLowerCase().includes(q) ||
        h.latin.toLowerCase().includes(q) ||
        (h.uses || '').toLowerCase().includes(q)
      );
    });
  }, [search, mapHover]);

  const dimSet = useMemo(() => {
    if (!search.trim() && !mapHover) return null;
    return new Set(filteredHerbs.map(h => h.id));
  }, [filteredHerbs, search, mapHover]);

  /* layout */
  const layout = useMemo(() => {
    return computeLayout(view, viewport, HERBS);
  }, [view, viewport]);

  /* story mode auto-advance */
  useEffect(() => {
    if (story === null) return;
    const step = STORY_STEPS[story];
    setView(step.view);
    const t = setTimeout(() => {
      setStory(s => (s === null ? null : (s + 1) % STORY_STEPS.length));
    }, 5200);
    return () => clearTimeout(t);
  }, [story]);

  /* compare handlers */
  const onHerbClick = (h) => {
    if (compareMode) {
      setCompare(prev => {
        if (prev.includes(h.id)) return prev.filter(id => id !== h.id);
        if (prev.length >= 3) return [...prev.slice(1), h.id];
        return [...prev, h.id];
      });
      return;
    }
    setPinned(p => p === h.id ? null : h.id);
  };

  const compareItems = compare.map(id => HERBS.find(h => h.id === id)).filter(Boolean);

  const currentView = VIEWS[view];

  return (
    <>
      {/* Editorial intro */}
      <header className="intro reveal">
        <div className="kicker">A Visual Catalogue · 2026</div>
        <h1>
          <span className="cn">中药本草</span>
          The Herbs of<br/><em>Chinese Medicine</em>
        </h1>
        <p className="deck">
          One hundred and ninety-four plants, fungi, minerals and animal parts — from the ginger in your kitchen to a fungus that grows from a Himalayan caterpillar — laid out by what they do, where they come from, and what they cost.
        </p>
        <p className="byline">
          By <strong>The Studio</strong> · Drawings rendered in the manner of classical <em>běncǎo</em> woodblocks
        </p>
        <div className="intro-divider">
          <span className="line" /><span className="ornament">本草</span><span className="line" />
        </div>
      </header>

      <section className="intro-lede reveal">
        <p className="dropcap">
          For two thousand years Chinese physicians have catalogued the world by what it can do for the body. The earliest classic, the <em>Shénnóng Běncǎojīng</em>, lists 365 substances graded into three classes: superior herbs that nourish life, middle herbs that supplement nature, and lower herbs that attack disease. The categories below are descendants of that scheme.
        </p>
        <p>
          Each circle is a single herb. Hover for its name in 中文 and pīnyīn, the meridians it enters, and the diseases it treats. Use the controls at top to recompose the catalogue — by category, by rarity, by what part of the plant or animal becomes the medicine, by where in China the finest material grows, or by what it sells for at market.
        </p>
      </section>

      {/* Sticky controls */}
      <div className="controls-wrap reveal">
        <div className="controls">
          <span className="controls-label">Classify by</span>
          <div className="classify-group">
            {Object.entries(VIEWS).map(([k, v]) => (
              <button
                key={k}
                className={`classify-btn ${view === k ? 'active' : ''}`}
                onClick={() => { setView(k); setStory(null); setHover(null); setPinned(null); setOpenCluster(null); }}
              >
                {v.label}
              </button>
            ))}
          </div>
          <button
            className={`story-btn ${story !== null ? 'active' : ''}`}
            onClick={() => setStory(s => s === null ? 0 : null)}
          >
            {story === null ? '▶ Story Mode' : '■ Stop Story'}
          </button>
          <button
            className={`compare-btn ${compareMode ? 'active' : ''}`}
            onClick={() => setCompareMode(c => !c)}
          >
            {compareMode ? `Comparing (${compare.length}/3)` : 'Compare Herbs'}
          </button>
          <div className="search">
            <svg className="search-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.4">
              <circle cx="7" cy="7" r="4.5" />
              <path d="M10.5 10.5 L14 14" />
            </svg>
            <input
              placeholder="Search 五味子, schisandra, cough…"
              value={search}
              onChange={e => setSearch(e.target.value)}
            />
          </div>
        </div>
      </div>

      {/* Chart */}
      <main className="chart-wrap reveal">
        <div className="chart-title">
          <span className="cn">{currentView.cn}</span>
          <h2>By {currentView.label.toLowerCase()}</h2>
          <div className="sub">{currentView.desc}</div>
        </div>

        <div className="chart" ref={chartRef} style={{ height: layout.totalH + 40 }}>
          {layout.clusters.map(c => (
            <div className="cluster" key={c.id} style={{ left: c.x, top: c.y, width: c.w, height: c.h }}>
              <button
                className="cluster-label cluster-label-btn"
                onClick={() => setOpenCluster(c.id)}
                title="View all herbs in this group"
              >
                {c.cn && <span className="cn">{c.cn}</span>}
                {c.label}
                <span className="cluster-arrow" aria-hidden="true">→</span>
              </button>
              <div className="cluster-rule" />
              <div className="cluster-meta">
                {c.herbs.length} herb{c.herbs.length === 1 ? '' : 's'}
                {c.detail ? ` · ${c.detail}` : ''}
              </div>
            </div>
          ))}

          {HERBS.map(h => {
            const pos = layout.positions[h.id];
            if (!pos) return null;
            const dimmed = dimSet && !dimSet.has(h.id);
            const selected = compareMode && compare.includes(h.id);
            return (
              <div
                key={h.id}
                className={`herb ${dimmed ? 'dimmed' : ''} ${selected ? 'selected' : ''}`}
                data-rarity={h.rarity}
                style={{ transform: `translate(${pos.x}px, ${pos.y}px)` }}
                onMouseEnter={e => setHover({ herb: h, x: e.clientX, y: e.clientY })}
                onMouseMove={e => setHover(prev => prev && prev.herb.id === h.id ? { herb: h, x: e.clientX, y: e.clientY } : prev)}
                onMouseLeave={() => setHover(null)}
                onClick={() => { if (compareMode) onHerbClick(h); }}
              >
                <div className="herb-illus">
                  <HerbIcon herb={h} size={38} />
                </div>
                <div className="herb-label">{h.zh}</div>
              </div>
            );
          })}
        </div>
      </main>

      {/* China map */}
      <ChinaMap herbs={HERBS} hovered={mapHover} setHovered={setMapHover} onProvinceClick={(prov) => setOpenProvince(prov)} />

      {/* Cluster detail panel */}
      {openCluster && (() => {
        const c = layout.clusters.find(x => x.id === openCluster);
        if (!c) return null;
        const sorted = [...c.herbs].sort((a, b) => a.rarity - b.rarity || a.price - b.price);
        return (
          <div className="cluster-panel-backdrop" onClick={() => setOpenCluster(null)}>
            <div className="cluster-panel" onClick={e => e.stopPropagation()}>
              <button className="cluster-panel-close" onClick={() => setOpenCluster(null)}>×</button>
              <div className="cluster-panel-head">
                <div className="kicker">{currentView.label} · {currentView.cn}</div>
                <h3>
                  {c.cn && <span className="cn">{c.cn}</span>}
                  {c.label}
                </h3>
                <div className="cluster-panel-meta">{c.herbs.length} herb{c.herbs.length === 1 ? '' : 's'}{c.detail ? ` · ${c.detail}` : ''}</div>
              </div>
              <div className="cluster-panel-list">
                {sorted.map(h => (
                  <div className="cluster-panel-row" key={h.id}>
                    <div className="cluster-panel-illus">
                      <HerbIcon herb={h} size={42} />
                    </div>
                    <div className="cluster-panel-name">
                      <div className="zh">{h.zh} <span className="py">{h.pinyin}</span></div>
                      <div className="en">{h.en} · <em>{h.latin}</em></div>
                    </div>
                    <div className="cluster-panel-uses">{h.uses}</div>
                    <div className="cluster-panel-stats">
                      <div><span>Origin</span><span>{h.origin}</span></div>
                      <div><span>Nature</span><span>{h.nature}</span></div>
                      <div><span>Taste</span><span>{h.taste}</span></div>
                      <div><span>Rarity</span>
                        <span className="rarity-pip">{[1,2,3,4,5].map(i => <span key={i} className={i <= h.rarity ? 'on' : ''} />)}</span>
                      </div>
                      <div><span>Value</span><span>${h.price.toLocaleString()}/kg</span></div>
                    </div>
                  </div>
                ))}
              </div>
            </div>
          </div>
        );
      })()}

      {/* Footer */}
      <footer className="footer reveal">
        <span className="cn">本草纲目</span>
        Sources: Bensky &amp; Gamble, <em>Chinese Herbal Medicine: Materia Medica</em> · Approximate market values reflect 2024 wholesale ranges and vary widely by grade and provenance.
      </footer>

      {/* Hover detail — suppress while a cluster/province panel is open */}
      {hover && !pinned && !openCluster && !openProvince && <DetailCard herb={hover.herb} x={hover.x} y={hover.y} />}

      {/* Province detail panel */}
      {openProvince && (() => {
        const list = HERBS.filter(h => h.origin === openProvince).sort((a,b) => a.rarity - b.rarity || a.price - b.price);
        const zh = (PROV_CN[openProvince] || '');
        return (
          <div className="cluster-panel-backdrop" onClick={() => setOpenProvince(null)}>
            <div className="cluster-panel" onClick={e => e.stopPropagation()}>
              <button className="cluster-panel-close" onClick={() => setOpenProvince(null)}>×</button>
              <div className="cluster-panel-head">
                <div className="kicker">Origin · 产地</div>
                <h3>
                  <span className="cn">{zh}</span>
                  {openProvince}
                </h3>
                <div className="cluster-panel-meta">{list.length} herb{list.length === 1 ? '' : 's'} traced to this region</div>
              </div>
              <div className="cluster-panel-list">
                {list.map(h => (
                  <div className="cluster-panel-row" key={h.id}>
                    <div className="cluster-panel-illus">
                      <HerbIcon herb={h} size={42} />
                    </div>
                    <div className="cluster-panel-name">
                      <div className="zh">{h.zh} <span className="py">{h.pinyin}</span></div>
                      <div className="en">{h.en} · <em>{h.latin}</em></div>
                    </div>
                    <div className="cluster-panel-uses">{h.uses}</div>
                    <div className="cluster-panel-stats">
                      <div><span>Category</span><span>{h.category}</span></div>
                      <div><span>Nature</span><span>{h.nature}</span></div>
                      <div><span>Taste</span><span>{h.taste}</span></div>
                      <div><span>Rarity</span>
                        <span className="rarity-pip">{[1,2,3,4,5].map(i => <span key={i} className={i <= h.rarity ? 'on' : ''} />)}</span>
                      </div>
                      <div><span>Value</span><span>${h.price.toLocaleString()}/kg</span></div>
                    </div>
                  </div>
                ))}
              </div>
            </div>
          </div>
        );
      })()}
      {pinned && (() => {
        const h = HERBS.find(x => x.id === pinned);
        const pos = layout.positions[pinned];
        if (!h) return null;
        const rect = chartRef.current?.getBoundingClientRect();
        const x = (rect?.left || 0) + (pos?.x || 0);
        const y = (rect?.top || 0) + (pos?.y || 0);
        return <DetailCard herb={h} x={x} y={y} onClose={() => setPinned(null)} />;
      })()}

      {/* Compare tray */}
      <CompareTray
        items={compareItems}
        open={compareMode && compare.length > 0}
        onRemove={id => setCompare(c => c.filter(x => x !== id))}
        onClose={() => { setCompareMode(false); setCompare([]); }}
      />

      {/* Story mode overlay */}
      {story !== null && (
        <div className="story-overlay">
          <button onClick={() => setStory(s => (s + STORY_STEPS.length - 1) % STORY_STEPS.length)}>‹</button>
          <div className="step">
            <strong>{String(story + 1).padStart(2, '0')} / {String(STORY_STEPS.length).padStart(2, '0')}</strong>
            {STORY_STEPS[story].title} — <span style={{ opacity: 0.7 }}>{STORY_STEPS[story].note}</span>
          </div>
          <div className="story-progress" key={story}>
            <span style={{ width: '100%' }} />
          </div>
          <button onClick={() => setStory(s => (s + 1) % STORY_STEPS.length)}>›</button>
          <button onClick={() => setStory(null)} style={{ marginLeft: 4 }}>×</button>
        </div>
      )}
    </>
  );
}

ReactDOM.createRoot(document.getElementById('root')).render(<App />);
