/* Portfolio UI kit — primitives (use kit.css classes; tokens from styles.css) */

function Button({ children, variant = "primary", size = "md", ...rest }) {
  return <button className={`hu-btn hu-btn--${size} hu-btn--${variant}`} {...rest}>{children}</button>;
}

function IconButton({ children, label, onink, ...rest }) {
  return <button aria-label={label} title={label} className={`hu-iconbtn${onink ? " hu-iconbtn--onink" : ""}`} {...rest}>{children}</button>;
}

function Tag({ children }) {
  return <span className={`hu-tag hu-tag--${children}`}>{children}</span>;
}

function Eyebrow({ children, rule, variant }) {
  return (
    <span className={`hu-eyebrow${variant ? " hu-eyebrow--" + variant : ""}`}>
      {rule ? <span className="rule" /> : null}{children}
    </span>
  );
}

function LangSwitch({ value, onChange }) {
  const items = [["tw", "中文"], ["en", "English"]];
  return (
    <span className="hu-lang">
      {items.map(([code, label], i) => (
        <React.Fragment key={code}>
          {i > 0 ? <span className="sep">/</span> : null}
          <button className={value === code ? "on" : ""} onClick={() => onChange(code)}>{label}</button>
        </React.Fragment>
      ))}
    </span>
  );
}

function SiteHeader({ active, onNavigate, lang, onLang, variant, navItems, hideBrand }) {
  const cls = variant === "ink" ? "hu-hdr--ondark" : "hu-hdr--bordered";
  const [menu, setMenu] = React.useState(false);
  // scrolls off with the page like any other block; once it's gone it can float back in
  // on an upward scroll or when the pointer reaches the top edge
  const hdrRef = React.useRef(null);
  const [floatH, setFloatH] = React.useState(0);
  const [shown, setShown] = React.useState(false);
  const [armed, setArmed] = React.useState(false);
  const floatRef = React.useRef(0);
  React.useEffect(() => { floatRef.current = floatH; }, [floatH]);
  // the frame it turns fixed it must already be hidden — arm the slide only afterwards,
  // otherwise the first scroll animates it up from the top edge and looks like a bounce
  React.useEffect(() => {
    if (!floatH) { setArmed(false); return; }
    const r = requestAnimationFrame(() => setArmed(true));
    return () => cancelAnimationFrame(r);
  }, [floatH]);
  React.useEffect(() => {
    // the profile reads top to bottom — the bar simply stays put there
    if (active === "profile") { setFloatH(0); setShown(false); return; }
    const phone = () => window.matchMedia("(max-width: 680px) and (orientation: portrait)").matches;
    let last = window.scrollY, acc = 0, lastDown = 0;
    const height = () => (hdrRef.current ? hdrRef.current.offsetHeight : 0);
    const onScroll = () => {
      if (phone()) return;
      const y = window.scrollY;
      const m = gateRef.current;
      // the homepage hand-off owns the bar until the name is well clear
      if (m && y < m.end + 80) { last = y; acc = 0; return; }
      const dy = y - last;
      last = y;
      if (y <= 2) { setFloatH(0); setShown(false); acc = 0; return; }
      // near the top the floating bar sits where the real one would — hold it visible so the
      // hand-off back into the flow happens with nothing moving
      if (y <= height() + 8) { if (floatRef.current) setShown(true); acc = 0; return; }
      setFloatH((v) => v || height());
      // accumulate in one direction so momentum jitter can't flip the bar back and forth
      acc = dy * acc > 0 ? acc + dy : dy;
      if (acc > 12) { lastDown = performance.now(); setShown(false); }
      else if (acc < -140) setShown(true);
    };
    const onMove = (e) => {
      if (phone()) return;
      // the lightbox owns the screen — never summon the bar behind it
      if (document.querySelector(".hu-lb")) return;
      // anywhere within the bar's own band at the top of the window brings it back
      if (e.clientY > Math.max(90, height() + 16)) return;
      const m = gateRef.current;
      if (m && window.scrollY < m.end) return;
      // a downward scroll in the last moment wins — the pointer only reveals when the page is still
      if (performance.now() - lastDown < 500) return;
      if (window.scrollY > height() + 8) { setFloatH((v) => v || height()); setShown(true); }
    };
    // a page change starts at the top again: hand back to the bar in the flow, ready to be seen
    const onNav = () => { last = 0; acc = 0; lastDown = 0; setFloatH(0); setShown(false); };
    // filtering jumps the page: keep the bar on show so the landing point is predictable
    const onReveal = () => {
      if (phone()) return;
      lastDown = 0; acc = 0;
      if (window.scrollY > height() + 8) { setFloatH((v) => v || height()); setShown(true); }
    };
    window.addEventListener("hu:reveal-bar", onReveal);
    window.addEventListener("hu:nav", onNav);
    window.addEventListener("scroll", onScroll, { passive: true });
    window.addEventListener("pointermove", onMove);
    return () => { window.removeEventListener("scroll", onScroll); window.removeEventListener("pointermove", onMove); window.removeEventListener("hu:nav", onNav); window.removeEventListener("hu:reveal-bar", onReveal); };
  }, [active]);
  React.useEffect(() => { if (menu) setShown(true); }, [menu]);
  // on the homepage the links wait until the opening has finished arranging the page
  const [quiet, setQuiet] = React.useState(() => !!hideBrand && !window.__huIntroDone);
  const [line] = React.useState(() => !!hideBrand && !window.__huIntroDone);
  const [drawing, setDrawing] = React.useState(false);
  React.useEffect(() => {
    if (!quiet) return;
    const on = () => setQuiet(false);
    const draw = () => setDrawing(true);
    window.addEventListener("hu:intro-done", on);
    window.addEventListener("hu:intro-unveil", draw);
    const t = setTimeout(on, 5000);
    return () => { window.removeEventListener("hu:intro-done", on);
      window.removeEventListener("hu:intro-unveil", draw); clearTimeout(t); };
  }, [quiet]);
  // ---- home: the big hero name carries the identity, so the bar keeps out of its way and only
  // appears once the name has scrolled off
  const gateRef = React.useRef(null);
  React.useEffect(() => {
    if (!hideBrand) { gateRef.current = null; return; }
    const off = window.matchMedia("(max-width: 680px) and (orientation: portrait)").matches;
    if (off) return;
    let gate = 0, raf = 0;
    const measure = () => {
      const name = document.querySelector(".hu-hero-full .hu-namestack");
      gate = name ? name.getBoundingClientRect().bottom + window.scrollY + 24 : 0;
      gateRef.current = gate ? { end: gate } : null;
    };
    const tick = () => {
      raf = 0;
      if (!gate) return;
      const h = hdrRef.current ? hdrRef.current.offsetHeight : 0;
      const y = window.scrollY;
      // at the very top the floating bar hands back to the one in the page, with nothing moving
      if (y <= 2) { setShown(false); setFloatH(0); }
      // over the hero: hold whatever is on screen — never retract mid-scroll
      else if (y <= gate) { if (floatRef.current) setShown(true); }
      else if (!h) return;
    };
    const onScroll = () => { if (!raf) raf = requestAnimationFrame(tick); };
    const onResize = () => { measure(); onScroll(); };
    measure(); tick();
    window.addEventListener("scroll", onScroll, { passive: true });
    window.addEventListener("resize", onResize);
    // the hero lockup is transformed during the opening, so take the real reading once it settles
    window.addEventListener("hu:intro-done", onResize);
    const t = setTimeout(onResize, 500);
    return () => { cancelAnimationFrame(raf); clearTimeout(t);
      window.removeEventListener("scroll", onScroll); window.removeEventListener("resize", onResize);
      window.removeEventListener("hu:intro-done", onResize); };
  }, [hideBrand]);
  React.useEffect(() => { if (window.lucide) window.lucide.createIcons(); }, [menu]);
  const nav = (id) => { setMenu(false); onNavigate(id); };
  return (
    <React.Fragment>
    {floatH ? <div className="hu-hdr__spacer" style={{ height: floatH }}></div> : null}
    <header ref={hdrRef} className={`hu-hdr ${cls}${active === "profile" ? " is-pinned" : ""}${floatH ? " is-float" : ""}${armed ? " is-armed" : ""}${floatH && shown && armed ? " is-in" : ""}${line ? " is-line" : ""}${menu ? " is-menu" : ""}${quiet ? " is-quiet" : ""}${drawing ? " is-drawing" : ""}`}>
      <div className={"hu-brand" + (hideBrand ? " is-hidden" : "")} onClick={() => nav("home")}>
        <span className="cn">胡朝景</span>
        <span className="en"><span className="hu-rom-a">Chau<span className="hu-rom-sep">-</span>Jin</span><span className="hu-rom-b">Hu</span></span>
      </div>
      <nav className="hu-nav">
        {navItems.map(([id, label]) => (
          <a key={id} tabIndex={0} className={active === id ? "active" : ""}
            onClick={() => onNavigate(id)}
            onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onNavigate(id); } }}>{label}</a>
        ))}
      </nav>
      <div className="hu-right">
        <span className="hu-right__lang"><LangSwitch value={lang} onChange={onLang} /></span>
        <button className="hu-burger" aria-label="Menu" onClick={() => setMenu((v) => !v)}>
          <i data-lucide={menu ? "x" : "menu"}></i>
        </button>
      </div>
      {menu ? (
        <div className="hu-mobnav">
          {navItems.map(([id, label]) => (
            <a key={id} tabIndex={0} className={active === id ? "active" : ""}
              onClick={() => nav(id)}
              onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); nav(id); } }}>{label}</a>
          ))}
          <div className="hu-mobnav__lang">
            <button className={lang === "tw" ? "on" : ""} onClick={() => { setMenu(false); onLang("tw"); }}>中文</button>
            <button className={lang === "en" ? "on" : ""} onClick={() => { setMenu(false); onLang("en"); }}>English</button>
          </div>
        </div>
      ) : null}
    </header>
    </React.Fragment>
  );
}

/* artwork title shown in the page language (CJK title for tw/jp, English for en) */
function workTitle(work, lang) { return lang === "en" ? work.en : work.cjk; }
function workMedium(work, lang) {
  const m = window.HU_DATA.medium[work.tag][lang];
  return `${m}, ${work.dim}`;
}
function collNote(work, lang) {
  if (!work.coll) return null;
  const t = window.HU_I18N[lang];
  return `${t.collTag} · ${work.coll[lang]} ${work.coll.y}`;
}

function ArtworkCard({ work, lang, onClick }) {
  const isCjk = lang !== "en";
  // true pixel ratio of the actual image file (measured); fall back to onLoad if missing
  const ar = (window.HU_RATIOS && window.HU_RATIOS[work.img]) || null;
  const onImgLoad = (e) => {
    const img = e.target;
    if (!ar && img.naturalWidth) {
      const f = img.closest(".hu-art__frame");
      if (f) f.style.aspectRatio = `${img.naturalWidth} / ${img.naturalHeight}`;
    }
    // never upscale past native pixels (avoids blur): cap the mat to the image's real width
    const mat = img.closest(".hu-art__mat");
    if (mat && img.naturalWidth) mat.style.maxWidth = img.naturalWidth + "px";
  };
  return (
    <button id={`work-${work.id}`} className="hu-art" onClick={onClick}
      onPointerEnter={() => window.HU_PREFETCH && window.HU_PREFETCH(work.img)}
      onPointerDown={() => window.HU_PREFETCH && window.HU_PREFETCH(work.img)}>
      <span className="hu-art__mat">
        <span className="hu-art__frame" style={ar ? { aspectRatio: ar.replace("/", " / ") } : undefined}>
          <img className="hu-art__img" src={`${window.HU_ASSET}/works-sm/${work.img.replace(/\.jpg$/i, ".webp")}`} alt={work.en} loading="lazy" decoding="async" onLoad={onImgLoad}
            onError={(e) => { const im = e.target; if (im.dataset.fb) return; im.dataset.fb = "1"; im.src = `${window.HU_ASSET}/works/${work.img}`; }} />
        </span>
      </span>
      <span className="hu-art__label">
        <span className="hu-art__top">
          <span className={`hu-art__title${isCjk ? " is-cjk" : ""}`}>{workTitle(work, lang)}</span>
          <span className="hu-art__year">{work.yearLabel || work.year}</span>
        </span>
        <span className="hu-art__meta">{workMedium(work, lang)}</span>
        {work.coll ? <span className="hu-art__coll">{collNote(work, lang)}</span> : null}
      </span>
    </button>
  );
}

function MuseumLabel({ work, lang, onink }) {
  const isCjk = lang !== "en";
  return (
    <span className={`hu-label${onink ? " hu-label--onink" : ""}`}>
      <span className="hu-label__top">
        <span className={`t${isCjk ? " is-cjk" : ""}`}>{workTitle(work, lang)}</span>
        <span className="hu-label__year">{work.yearLabel || work.year}</span>
      </span>
      <span className="m">{workMedium(work, lang)}</span>
      {work.coll ? <span className="hu-label__coll">{collNote(work, lang)}</span> : null}
    </span>
  );
}

function SectionHeading({ eyebrow, title, sub, cjk, center, accent }) {
  return (
    <div className={`hu-sh${center ? " hu-sh--center" : ""}`}>
      {eyebrow ? <Eyebrow rule variant={accent ? "accent" : undefined}>{eyebrow}</Eyebrow> : null}
      <h2 className={`hu-sh__title${cjk ? " is-cjk" : ""}`}>{title}</h2>
      {sub ? <p className="hu-sh__sub">{sub}</p> : null}
    </div>
  );
}

function Input({ label, multiline, ...rest }) {
  const C = multiline ? "textarea" : "input";
  return (
    <label style={{ display: "block" }}>
      {label ? <span className="hu-fieldlabel">{label}</span> : null}
      <C className="hu-input" {...rest} />
    </label>
  );
}

// a listbox rather than a native <select>: the menu always opens upward, so the
// floating filters at the foot of the works page never drop their options off-screen
function Select({ label, value, onChange, options, align }) {
  const [open, setOpen] = React.useState(false);
  const [room, setRoom] = React.useState(0);
  const [width, setWidth] = React.useState(null);
  const [down, setDown] = React.useState(false);
  const wrapRef = React.useRef(null);
  const btnRef = React.useRef(null);
  const current = options.find((o) => o.value === value) || options[0];
  const measure = () => {
    const b = btnRef.current;
    if (!b) return;
    const r = b.getBoundingClientRect();
    const above = r.top - 18, below = window.innerHeight - r.bottom - 18;
    const wanted = Math.min(options.length * 38 + 16, 420);
    // open downward when there is room there (the row at the top of the page), upward otherwise
    const goDown = below >= Math.min(wanted, 180) && below >= above;
    setDown(goDown);
    setRoom(Math.max(120, Math.round(goDown ? below : above)));
    // "wide": run from this trigger to the right edge of the whole filter row
    if (align === "wide") {
      const row = b.closest(".hu-filters, .hu-filters--float");
      setWidth(row ? Math.round(row.getBoundingClientRect().right - r.left) : null);
    }
  };
  React.useEffect(() => {
    if (!open) return;
    const away = (e) => { if (!wrapRef.current || !wrapRef.current.contains(e.target)) setOpen(false); };
    const key = (e) => { if (e.key === "Escape") { e.stopPropagation(); setOpen(false); } };
    document.addEventListener("pointerdown", away);
    document.addEventListener("keydown", key, true);
    window.addEventListener("resize", measure);
    // scrolling the page moves the trigger, so simply put the menu away
    const shut = (e) => { if (!wrapRef.current || !wrapRef.current.contains(e.target)) setOpen(false); };
    window.addEventListener("scroll", shut, { passive: true, capture: true });
    return () => {
      document.removeEventListener("pointerdown", away); document.removeEventListener("keydown", key, true);
      window.removeEventListener("resize", measure); window.removeEventListener("scroll", shut, true);
    };
  }, [open]);
  const pick = (v) => { onChange(v); setOpen(false); };
  const onKey = (e) => {
    const i = options.findIndex((o) => o.value === value);
    if (e.key === "ArrowDown" && i < options.length - 1) { e.preventDefault(); onChange(options[i + 1].value); }
    else if (e.key === "ArrowUp" && i > 0) { e.preventDefault(); onChange(options[i - 1].value); }
  };
  return (
    <span className={"hu-selectwrap" + (open ? " is-open" : "") + (align ? " al-" + align : "")} ref={wrapRef}>
      <button type="button" className="hu-select" aria-label={label} aria-haspopup="listbox" aria-expanded={open}
        ref={btnRef} onClick={() => { measure(); setOpen((v) => !v); }} onKeyDown={onKey}>
        <span className="hu-select__val">{value === "all" ? label : current ? current.label : ""}</span>      </button>
      <span className="chev">▾</span>
      {open ? (
        <ul className={"hu-selectmenu" + (down ? " is-down" : "") + (align ? " al-" + align : "")} role="listbox" aria-label={label}
          style={{ maxHeight: room + "px", width: align === "wide" && width ? width + "px" : undefined }}>
          {options.map((o) => (
            <li key={o.value} role="option" aria-selected={o.value === value}>
              <button type="button" className={o.value === value ? "on" : ""} onClick={() => pick(o.value)}>{o.label}</button>
            </li>
          ))}
        </ul>
      ) : null}
    </span>
  );
}

Object.assign(window, {
  HUButton: Button, HUIconButton: IconButton, HUTag: Tag, HUEyebrow: Eyebrow,
  HULangSwitch: LangSwitch, HUSiteHeader: SiteHeader, HUArtworkCard: ArtworkCard,
  HUMuseumLabel: MuseumLabel, HUSectionHeading: SectionHeading, HUInput: Input, HUSelect: Select,
});
