/* The page wrapper: which screen is showing, and the chrome around it.
   Lives in its own file (not inline in index.html) so admin.html can load it too. */
const WIPE = ".transition-wipe";
const COVER = 0.8;   /* seconds for the wipe to travel one screen height */
const HOLD = 0.1;    /* the beat where the screen is fully covered */

function Site() {
  const t = window.COPY.th;
  /* The URL is the state: /slogan, /books and /packaging are real addresses, so a
     cold load, a bookmark and a shared link all land where they say they do. */
  const first = routeFor(t, location.pathname);
  const [page, setPage] = React.useState(first.page);
  /* Where the visitor arrived from: { rail } scopes the work grid to one rail and
     names the URL, { cat } preselects a filter chip after clicking a card. */
  const [focus, setFocus] = React.useState(first.focus);
  const busy = React.useRef(false);
  const pending = React.useRef(null);

  /* GSAP is a CDN global and reduced motion opts out of the whole sequence. Either
     way the swap still happens — the transition is decoration, never the mechanism. */
  const animated = () => !!window.gsap && !window.matchMedia("(prefers-reduced-motion: reduce)").matches;
  /* `push` is false when the browser moved us — a popstate that pushed again would
     bury the entry the visitor just went back to. */
  const swap = (p, f, push) => {
    setPage(p);
    setFocus(f || null);
    document.documentElement.scrollTop = 0;
    const path = pathFor(t, p, f);
    if (push !== false && path !== location.pathname) history.pushState({}, "", path);
    document.title = p === "work" ? t.seo.pageFmt(railTitle(t, f && f.rail)) : t.seo.title;
  };

  /* Back and forward are navigation like any other, minus the push. */
  React.useEffect(() => {
    const onPop = () => { const r = routeFor(t, location.pathname); swap(r.page, r.focus, false); };
    window.addEventListener("popstate", onPop);
    return () => window.removeEventListener("popstate", onPop);
  }, []);

  /* Covering the screen and trusting an animation to uncover it is a bet on frames
     actually ticking. When they do not — an embedded browser view with no
     compositor, a tab throttled to death — the whole site sits behind an opaque
     panel with nothing in the console. So every timeline that closes the wipe arms
     a watchdog that forces it back open, and clears it on completion. Normal
     browsers finish in 1.7s and never see it. */
  const guard = (tl) => {
    const bail = setTimeout(() => {
      /* The swap lives in the middle of the timeline, so a timeline that never
         advances does not just stall the curtain — it swallows the navigation and
         leaves the visitor on the page they tried to leave. Land them anyway. */
      if (pending.current) { swap(pending.current[0], pending.current[1]); pending.current = null; }
      gsap.set(WIPE, { scaleY: 0 });
      busy.current = false;
    }, 3000);
    tl.eventCallback("onComplete", () => { clearTimeout(bail); busy.current = false; });
    return tl;
  };

  /* Intro. The wipe is parked open in CSS so a script that never loads can not leave
     the screen covered — it is closed here, on the frame the app mounts, and then
     drawn back up to reveal the page underneath. */
  React.useEffect(() => {
    if (!animated()) return;
    gsap.set(WIPE, { scaleY: 1, transformOrigin: "top" });
    guard(gsap.timeline()
      .to(WIPE, { duration: COVER, scaleY: 0, transformOrigin: "top", ease: "power4.inOut" })
      .from(".premium-reveal", { duration: 1.2, y: 30, opacity: 0, filter: "blur(10px)",
                                 ease: "power3.out", stagger: 0.1 }, "-=0.4"));
  }, []);

  const go = (p, f) => {
    /* Already there? Then this is the visitor clicking the page they are reading,
       and replaying the curtain for it would look like a fault. */
    if ((p === page && (f && f.rail) === (focus && focus.rail)) || busy.current) return;
    if (!animated()) { swap(p, f); return; }
    busy.current = true;
    pending.current = [p, f];
    guard(gsap.timeline())
      /* leave: the wipe grows up from the floor while the old page sinks away */
      .to(WIPE, { duration: COVER, scaleY: 1, transformOrigin: "bottom", ease: "power4.inOut" })
      .to(".page-body", { duration: COVER, y: 50, opacity: 0, ease: "power3.in" }, "<")
      .add(() => { swap(p, f); pending.current = null; })
      /* enter: the wipe retreats through the ceiling, uncovering the new page */
      .to(WIPE, { duration: COVER, scaleY: 0, transformOrigin: "top", ease: "power4.inOut", delay: HOLD })
      /* .call, not .from — a tween built now would bind to the OUTGOING page's
         elements, because GSAP resolves a selector string when the tween is created
         and at that point the swap above has not run yet. */
      .call(() => {
        gsap.from(".premium-reveal", { duration: 1.2, y: 30, opacity: 0, filter: "blur(10px)",
                                       ease: "power3.out", stagger: 0.1 });
      }, null, "-=0.4");
  };

  const Screen = { home: Home, work: Work }[page];
  /* The wipe sits outside .shell: .shell is overflow:clip with a radius, which would
     crop a fixed-position child. The page body stays keyed on the page name, so React
     hands each page a clean node and the inline styles GSAP wrote on the outgoing one
     leave with it — nothing to reset. The Nav stays outside it: the chrome should not
     sink and fade on every navigation. */
  return (
    <React.Fragment>
      <div className="transition-wipe" aria-hidden="true" />
      <ScrollTop t={t} />
      <div className="shell">
        <Nav page={page} focus={focus} go={go} t={t}/>
        <div key={page} className="page-body"><Screen go={go} t={t} focus={focus}/></div>
        <Footer t={t} go={go} page={page}/>
      </div>
    </React.Fragment>
  );
}

Object.assign(window, { Site });
