/* ui.jsx — shared helpers, logo, small components */
(function () {
  const { useState, useEffect, useRef } = React;

  const cx = (...a) => a.filter(Boolean).join(" ");

  // ---- number / quantity formatting --------------------------------
  const FRACTIONS = [
    [0.125, "⅛"], [0.25, "¼"], [0.333, "⅓"], [0.375, "⅜"],
    [0.5, "½"], [0.625, "⅝"], [0.667, "⅔"], [0.75, "¾"], [0.875, "⅞"],
  ];
  function fmtNumber(n) {
    if (n == null) return "";
    const rounded = Math.round(n * 100) / 100;
    const whole = Math.floor(rounded);
    const frac = rounded - whole;
    // snap to nearest nice fraction within tolerance
    let best = null, bestD = 0.04;
    for (const [v, g] of FRACTIONS) {
      const d = Math.abs(frac - v);
      if (d < bestD) { bestD = d; best = g; }
    }
    if (best) return (whole ? whole + " " : "") + best;
    if (Number.isInteger(rounded)) return String(rounded);
    // otherwise show up to 1 decimal, trimming
    const s = (Math.round(rounded * 10) / 10).toFixed(1).replace(/\.0$/, "");
    return s;
  }
  function scaleAmount(ing, factor) {
    if (ing.qty == null) return ing.note || "to taste";
    const scaled = ing.qty * factor;
    return (fmtNumber(scaled) + (ing.unit ? " " + ing.unit : "")).trim();
  }

  // ---- time formatting ---------------------------------------------
  function fmtTime(mins) {
    if (mins == null) return "—";
    if (mins >= 1440) { const d = Math.round(mins / 1440 * 10) / 10; return d + " day" + (d > 1 ? "s" : ""); }
    if (mins >= 60) {
      const hPart = Math.floor(mins / 60), m = mins % 60;
      return m ? `${hPart} h ${m} min` : `${hPart} h`;
    }
    return mins + " min";
  }
  function fmtDate(iso) {
    try { return new Date(iso).toLocaleDateString("en-GB", { day: "numeric", month: "short", year: "numeric" }); }
    catch (e) { return iso; }
  }

  // ---- Logo (recreated from logo.svg, theme + brand aware) ---------
  function dodecagon(cx_, cy_, r) {
    const pts = [];
    for (let k = 0; k < 12; k++) {
      const a = (Math.PI / 6) * k - Math.PI / 12;
      pts.push(`${(cx_ + r * Math.cos(a)).toFixed(2)},${(cy_ + r * Math.sin(a)).toFixed(2)}`);
    }
    return pts.join(" ");
  }
  function Logo({ size = 30 }) {
    return (
      <svg width={size} height={size} viewBox="0 0 100 100" aria-hidden="true" style={{ flex: "none" }}>
        <circle cx="50" cy="50" r="49" fill="hsl(var(--muted))" />
        <polygon points={dodecagon(50, 50, 40)} fill="none" stroke="hsl(var(--foreground))" strokeWidth="9" strokeLinejoin="round" />
        <circle cx="50" cy="50" r="31" fill="hsl(var(--background))" />
        <circle cx="50" cy="50" r="20" fill="var(--brand)" />
      </svg>
    );
  }

  function Wordmark({ size = 30, onClick }) {
    return (
      <button className="row" onClick={onClick} style={{ gap: ".6rem" }} aria-label="AmosCooks home">
        <Logo size={size} />
        <span style={{ fontWeight: 600, fontSize: "1.06rem", letterSpacing: "-.02em" }}>
          Amos<span style={{ color: "color-mix(in srgb, var(--brand) 70%, hsl(var(--foreground)))" }}>Cooks</span>
        </span>
      </button>
    );
  }

  // ---- difficulty pip ----------------------------------------------
  function Difficulty({ level }) {
    const map = { Easy: 1, Medium: 2, Hard: 3 };
    const n = map[level] || 1;
    return (
      <span className="row" style={{ gap: ".3rem" }} title={level + " difficulty"}>
        <span className="row" style={{ gap: "2px" }}>
          {[1, 2, 3].map((d) => (
            <span key={d} style={{
              width: 6, height: 6, borderRadius: 99,
              background: d <= n ? "var(--brand)" : "hsl(var(--border))",
            }} />
          ))}
        </span>
        <span>{level}</span>
      </span>
    );
  }

  // ---- meta stat (icon + label) ------------------------------------
  function Stat({ icon: Ico, children, title }) {
    return (
      <span className="row muted" style={{ gap: ".4rem", fontSize: ".86rem" }} title={title}>
        <Ico size={15} /> <span>{children}</span>
      </span>
    );
  }

  // ---- scroll reveal hook ------------------------------------------
  function useReveal() {
    const ref = useRef(null);
    useEffect(() => {
      const root = ref.current;
      if (!root) return;
      const els = root.querySelectorAll(".reveal");
      if (!els.length) return;
      const show = (el) => el.classList.add("in");
      if (!("IntersectionObserver" in window)) { els.forEach(show); return; }
      const vh = window.innerHeight || 800;
      const io = new IntersectionObserver((entries) => {
        entries.forEach((e) => { if (e.isIntersecting) { show(e.target); io.unobserve(e.target); } });
      }, { threshold: 0.08, rootMargin: "0px 0px -30px 0px" });
      els.forEach((el) => {
        // above / near the viewport: show now (capture-safe, no flash). Below: hide then reveal on scroll.
        if (el.getBoundingClientRect().top < vh + 160) el.classList.add("in");
        else { el.classList.add("pre"); io.observe(el); }
      });
      // safety net: never leave content hidden
      const t = setTimeout(() => els.forEach(show), 1400);
      return () => { io.disconnect(); clearTimeout(t); };
    });
    return ref;
  }

  // ---- image with striped placeholder fallback ---------------------
  function SmartImg({ src, alt, label, style, className }) {
    const [err, setErr] = useState(false);
    if (err || !src) {
      return (
        <div className={cx("ph", className)} style={style}>
          <span className="mono">{label || "photo"}</span>
        </div>
      );
    }
    return <img src={src} alt={alt || ""} className={className} style={style} loading="lazy" onError={() => setErr(true)} />;
  }

  Object.assign(window, { cx, fmtNumber, scaleAmount, fmtTime, fmtDate, Logo, Wordmark, Difficulty, Stat, useReveal, SmartImg });
})();
