/* ============================================================
   Shared components, exported to window for cross-file use.
   ============================================================ */
const { useState, useEffect, useRef } = React;

/* ---- tiny icon set (functional UI glyphs only) ---- */
/* All icons here are decorative (they sit beside a text label), so they are
   hidden from assistive tech. Any icon-only control must carry its own
   aria-label on the interactive element. */
function Icon({ name, style }) {
  const p = { width: "1em", height: "1em", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.8, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", focusable: "false", style };
  switch (name) {
    case "arrow": return <svg {...p}><path d="M5 12h14M13 6l6 6-6 6" /></svg>;
    case "arrow-left": return <svg {...p}><path d="M19 12H5M11 6l-6 6 6 6" /></svg>;
    case "check": return <svg {...p}><path d="M5 12.5l4.5 4.5L19 7" /></svg>;
    case "no-ads": return <svg {...p}><rect x="3" y="5" width="18" height="14" rx="2.5" /><path d="M4 5l16 14" /></svg>;
    case "lock": return <svg {...p}><rect x="5" y="11" width="14" height="9" rx="2" /><path d="M8 11V8a4 4 0 0 1 8 0v3" /></svg>;
    case "no-account": return <svg {...p}><circle cx="12" cy="9" r="3.2" /><path d="M5.5 19a6.5 6.5 0 0 1 13 0" /><path d="M3 3l18 18" /></svg>;
    case "infinity": return <svg {...p}><path d="M7 9.5a3 3 0 1 0 0 5c2 0 3-2.5 5-2.5s3 2.5 5 2.5a3 3 0 1 0 0-5c-2 0-3 2.5-5 2.5S9 9.5 7 9.5z" /></svg>;
    case "wifi-off": return <svg {...p}><path d="M3 3l18 18M8.5 16.5a5 5 0 0 1 7 0M5 12.5a10 10 0 0 1 4-2.6M12 8c2.5 0 5 .9 7 2.6" /><circle cx="12" cy="20" r="0.6" fill="currentColor" /></svg>;
    case "sparkle": return <svg {...p}><path d="M12 4l1.6 4.4L18 10l-4.4 1.6L12 16l-1.6-4.4L6 10l4.4-1.6z" /></svg>;
    case "heart": return <svg {...p}><path d="M12 20s-7-4.4-9.2-9.2C1.3 7.6 3 5 6 5c1.9 0 3.2 1.1 4 2.3C10.8 6.1 12.1 5 14 5c3 0 4.7 2.6 3.2 5.8C19 15.6 12 20 12 20z" /></svg>;
    case "mail": return <svg {...p}><rect x="3" y="5" width="18" height="14" rx="2.5" /><path d="M4 7l8 6 8-6" /></svg>;
    default: return null;
  }
}

function Apple() {
  return (
    <svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
      <path d="M17.05 12.55c-.03-2.62 2.14-3.88 2.24-3.94-1.22-1.79-3.12-2.03-3.79-2.06-1.61-.16-3.15.95-3.97.95-.82 0-2.08-.93-3.42-.9-1.76.03-3.38 1.02-4.29 2.6-1.83 3.17-.47 7.86 1.31 10.43.87 1.26 1.9 2.67 3.26 2.62 1.31-.05 1.8-.85 3.39-.85 1.58 0 2.03.85 3.41.82 1.41-.02 2.3-1.28 3.16-2.55.99-1.46 1.4-2.88 1.42-2.95-.03-.01-2.72-1.04-2.75-4.13zM14.62 4.7c.72-.88 1.21-2.09 1.07-3.3-1.04.04-2.3.69-3.05 1.56-.67.78-1.26 2.02-1.1 3.2 1.16.09 2.35-.59 3.08-1.46z" />
    </svg>
  );
}

/* ---- star rating ---- */
function Stars({ value = 5, gold }) {
  return (
    <span className={"stars" + (gold ? " gold" : "")} role="img" aria-label={value + " out of 5 stars"}>
      {[0, 1, 2, 3, 4].map((i) => (
        <svg key={i} aria-hidden="true" focusable="false" viewBox="0 0 24 24" fill={i < Math.round(value) ? "currentColor" : "none"} stroke="currentColor" strokeWidth="1.6">
          <path d="M12 3.5l2.6 5.3 5.8.85-4.2 4.1 1 5.8L12 17.8 6.8 19.5l1-5.8L3.6 9.65l5.8-.85z" strokeLinejoin="round" />
        </svg>
      ))}
    </span>
  );
}

/* ---- App Store artwork lookup (real icons + screenshots, hotlinked) ---- */
function appOf(game) {
  const m = ((game && game.url) || "").match(/id(\d+)/);
  return (m && window.APPSTORE) ? window.APPSTORE[m[1]] : null;
}
function shotsOf(game) { const a = appOf(game); return (a && a.shots) || []; }

/* ---- app icon (real artwork, falls back to styled squircle) ---- */
function AppIcon({ game, size = 64 }) {
  const a = appOf(game);
  if (a && a.icon) {
    return <img className="appicon-img" src={a.icon} alt={game.name + " app icon"} loading="lazy" style={{ width: size, height: size }} />;
  }
  const initials = game.name.split(" ").map((w) => w[0]).join("").slice(0, 2).toUpperCase();
  return (
    <div className="appicon" style={{ "--s": size + "px", "--game": "oklch(0.6 0.16 " + game.hue + ")" }} aria-hidden="true">
      {initials}
    </div>
  );
}

/* ---- phone frame: real screenshot if available, else branded icon splash ---- */
function PhoneFrame({ game, width = 280, label, variant = "hero", shot }) {
  const a = appOf(game);
  const initials = game.name.split(" ").map((w) => w[0]).join("").slice(0, 2).toUpperCase();
  return (
    <div className="phone" style={{ "--pw": width + "px", "--game": "oklch(0.6 0.16 " + game.hue + ")" }}>
      <div className="phone-island"></div>
      <div className="phone-screen">
        {shot ? (
          <img className="scr-img" src={shot} alt={game.name + " screenshot"} loading="lazy" />
        ) : (
          <React.Fragment>
            <div className="scr-fill"></div>
            <div className="scr-top">
              {a && a.icon
                ? <img className="scr-iconimg" src={a.icon} alt="" loading="lazy" />
                : <div className="scr-icon">{initials}</div>}
              <div className="scr-name">{game.name}</div>
            </div>
            <div className="scr-label">{(label || (game.name + " · gameplay")).toUpperCase()}</div>
          </React.Fragment>
        )}
      </div>
    </div>
  );
}

/* ---- App Store badge ---- */
function AppStore({ url, label = "Download on the", store = "App Store" }) {
  return (
    <a className="appstore" href={url} target="_blank" rel="noopener noreferrer">
      <span className="apple"><Apple /></span>
      <span className="as-txt"><small>{label}</small><strong>{store}</strong></span>
    </a>
  );
}

/* ---- honor the user's reduced-motion preference for programmatic scrolls ---- */
function reduceMotion() {
  return !!(window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches);
}
function scrollBehavior() { return reduceMotion() ? "auto" : "smooth"; }

/* ---- scroll reveal hook ---- */
function useReveal() {
  useEffect(() => {
    const els = Array.from(document.querySelectorAll(".reveal:not(.in)"));
    if (!("IntersectionObserver" in window) || window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
      els.forEach((e) => e.classList.add("in"));
      return;
    }
    const io = new IntersectionObserver((ents) => {
      ents.forEach((en) => { if (en.isIntersecting) { en.target.classList.add("in"); io.unobserve(en.target); } });
    }, { threshold: 0.08, rootMargin: "0px 0px -6% 0px" });
    els.forEach((e) => io.observe(e));
    // Safety net: anything still hidden after a beat (off-screen, slow capture,
    // or observer never firing) gets revealed so content is never stuck invisible.
    const t = setTimeout(() => {
      document.querySelectorAll(".reveal:not(.in)").forEach((e) => {
        const r = e.getBoundingClientRect();
        if (r.top < window.innerHeight * 1.2) e.classList.add("in");
      });
    }, 900);
    return () => { io.disconnect(); clearTimeout(t); };
  });
}

Object.assign(window, { Icon, Apple, Stars, AppIcon, PhoneFrame, AppStore, useReveal, reduceMotion, scrollBehavior, appOf, shotsOf });
