const { Button, IconButton, Icon, Badge, Tag } = window.SuperiorPrintingHouseDesignSystem_81d178;

/* The LINE deep link, built from the account id rather than stored as a URL: a
   copy.js value beginning http: is typed as an image by the admin schema, which
   would hand the user a file picker for it. */
function lineHref(t) {
  return "https://line.me/R/ti/p/" + encodeURIComponent(t.nav.lineId);
}

/* The three accounts, as links. Like the LINE deep link, each is built from the
   handle stored in copy.js rather than a full URL: a copy.js value beginning http:
   is typed as an image by the admin schema and would hand the user a file picker.
   Instagram's link came with an fbclid tracking parameter attached; it is dropped —
   the profile is the destination, and the tracking id belongs to whoever shared it. */
function socialLinks(t) {
  return [
    { label: "Facebook", href: "https://www.facebook.com/" + t.nav.fbId },
    { label: "LINE", href: lineHref(t) },
    { label: "Instagram", href: "https://www.instagram.com/" + t.nav.igId },
  ];
}

/* tel: wants digits, the page wants the spaced form people read. Strip on the way
   out rather than storing a second copy of the number that can disagree. */
function telHref(t) {
  return "tel:" + String(t.nav.phone).replace(/[^+0-9]/g, "");
}

/* Every image may carry a focal point. Absent means centre, so every existing
   image is unaffected. Exported for the self-check. */
function focalOrDefault(focal) {
  return focal ? focal : "50% 50%";
}

/* One list from two props, so every existing `src` call site keeps working while
   new call sites can pass `images`. Empty entries are dropped because a deleted
   gallery slot leaves an empty string behind. */
function galleryList(images, src) {
  const list = (Array.isArray(images) ? images : []).filter(Boolean);
  if (list.length) return list;
  return src ? [src] : [];
}

/* The name a rail goes by, so a work page scoped to one rail can be headed with it
   rather than with the whole portfolio's title. */
function railTitle(t, rail) {
  if (rail === "work.projects") return t.nav.slogan;
  const m = /^showcases\.(\d+)\.projects$/.exec(rail || "");
  return m && t.showcases[m[1]] ? t.showcases[m[1]].title : t.work.title;
}

/* What a card shows. Until it has a photograph it shows as Coming Soon throughout
   — no category, no title, no spec line — because a card that names a job it cannot
   picture reads as a broken card rather than as work still to come. The words stay
   in copy.js untouched and come back the moment an image is added. */
function cardView(t, p) {
  if (galleryList(p.images, p.src).length) {
    return { c: p.c, t: p.t, d: p.d, label: t.work.mediaFmt(p.t) };
  }
  return { c: "", t: t.work.soon, d: t.work.soon, label: t.work.soon };
}

/* Categories are one comma-separated string per card rather than a list, so the
   admin edits them in the text field it already draws and a card can carry more
   than one — "สโลแกน, ป้ายคาดหัว". Nothing anywhere holds a master list: the
   filter row on the work page is the union of what the cards actually say, so a
   category cannot go stale the way the old hard-coded `filters` array had. */
function workCats(p) {
  return String(p.c || "").split(",").map(function (c) { return c.trim(); }).filter(Boolean);
}

/* Every card on the site, in page order, each with the copy.js path its images
   live at — that path is what lets the admin edit the same card from either the
   home rails or the work grid. The work page renders this whole list; the home
   page renders it one rail at a time. */
function allWorkItems(t) {
  const out = t.work.projects.map(function (p, n) { return { p: p, path: "work.projects." + n }; });
  (t.showcases || []).forEach(function (s, si) {
    s.projects.forEach(function (p, n) { out.push({ p: p, path: "showcases." + si + ".projects." + n }); });
  });
  return out;
}

/* One clock for every gallery on the page. Per-component intervals each start at
   their own mount time, so the cards drift apart within a second or two and the
   marquee's duplicated copies turn over at visibly different moments. Subscribers
   share a single interval instead, so every card changes on the same beat. */
const galleryTick = (() => {
  const subs = new Set();
  let timer = null;
  return (fn) => {
    subs.add(fn);
    if (!timer) timer = setInterval(() => Array.from(subs).forEach((f) => f()), 3000);
    return () => {
      subs.delete(fn);
      if (!subs.size) { clearInterval(timer); timer = null; }
    };
  };
})();

/* Scroll reveal. Returns a ref; the element fades and rises once, the first time it
   comes into view, then the observer disconnects — scrolling back up never re-hides
   it. Put the ref and className="reveal" on a section that already exists rather
   than wrapping it: Home's wrapper grid owns the vertical rhythm, and the sections
   it holds sit inside `display:contents`, which cannot be transformed at all.

   Under reduced motion, and on anything without IntersectionObserver, the class is
   simply never added and the CSS leaves the section visible. */
let ioProven = false;

/* If no observer anywhere has been called within a few seconds, IntersectionObserver
   is present but not working, and every revealed section would stay invisible — the
   whole page below the hero, blank, with nothing in the console. Seen for real in an
   embedded browser view. So: show everything and stop pretending. */
function armRevealBackstop() {
  setTimeout(function () {
    if (ioProven) return;
    document.querySelectorAll("[data-reveal]").forEach(function (n) {
      if (window.gsap) gsap.set(n, { clearProps: "opacity,transform,filter" });
    });
  }, 3000);
}
armRevealBackstop();

/* Scroll reveal, same feel as the page intro: a 30px rise out of a soft blur. The
   element is hidden from a layout effect rather than from CSS, so a browser without
   JS — or without GSAP — simply shows the page rather than hiding it forever.
   useLayoutEffect, not useEffect, or the section paints once at full opacity before
   it is hidden and the reveal starts with a flash. */
function useReveal() {
  const ref = React.useRef(null);
  React.useLayoutEffect(() => {
    const el = ref.current;
    if (!el || !window.gsap) return;
    if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
    el.setAttribute("data-reveal", "");
    gsap.set(el, { opacity: 0, y: 30, filter: "blur(10px)" });

    const show = () => gsap.to(el, { duration: 1.2, opacity: 1, y: 0, filter: "blur(0px)",
                                     ease: "power3.out", clearProps: "filter" });
    if (!window.IntersectionObserver) { show(); return; }

    /* Already on screen when the page mounts? Then there is nothing to wait for.
       Arriving on the work page from a card put its grid at the top of the viewport
       and left it blank until the visitor scrolled — the first observation lands
       after the page-entry animation and reports no *change*, so the callback that
       would have shown it never came. Measuring once at mount does not depend on
       that timing at all. */
    const onScreen = () => {
      const r = el.getBoundingClientRect();
      return r.top < window.innerHeight * 0.92 && r.bottom > 0;
    };
    if (onScreen()) { show(); return; }

    const io = new IntersectionObserver(function (entries) {
      /* Being called at all is what proves the API works — not being called with an
         intersection. Setting the flag only on intersect meant a page whose sections
         all start below the fold never proved anything, and the backstop then wiped
         every section in at once, three seconds in, before the user had scrolled. */
      ioProven = true;
      if (!entries.some((e) => e.isIntersecting)) return;
      show();
      io.disconnect();
    }, { threshold: 0.12, rootMargin: "0px 0px -8% 0px" });
    io.observe(el);
    return () => io.disconnect();
  }, []);
  return ref;
}

/* Media slot. Pass `src` for a supplied photograph; with no src it renders a
   labelled warm-neutral plate rather than inventing imagery. */
function Ph({ label, src, images, focal, fit = "contain", tone = "mid", radius = "var(--radius-media)", labelAlign = "center", style }) {
  const list = galleryList(images, src);
  const [i, setI] = React.useState(0);
  const [hover, setHover] = React.useState(false);
  const shown = Math.min(i, Math.max(0, list.length - 1));
  const many = list.length > 1;
  /* Hover zooms the image it is over; it no longer swaps to the next one. Swapping
     meant the picture you moved the pointer towards was never the picture you got. */

  /* Auto-advance off the shared clock. Unsubscribing while hovered both pauses
     this card and leaves the beat running for the rest, so a hovered card rejoins
     in time rather than restarting a timer of its own. Off entirely under
     prefers-reduced-motion, where nothing should move unprompted. */
  React.useEffect(() => {
    if (!many || hover) return;
    if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
    return galleryTick(() => setI((n) => (n + 1) % list.length));
  }, [many, hover, list.length]);

  const bg = tone === "dark" ? "var(--ink-800)" : tone === "light" ? "var(--ink-100)" : "var(--ink-200)";
  const fg = tone === "dark" ? "rgba(255,255,255,.45)" : "var(--ink-500)";
  const step = (d) => (e) => { e.stopPropagation(); setI((shown + d + list.length) % list.length); };
  const imgStyle = { position: "absolute", inset: 0, width: "100%", height: "100%",
                     objectFit: fit, objectPosition: focalOrDefault(focal) };
  return (
    <div className="ph" style={{ background: bg, borderRadius: radius, position: "relative", overflow: "hidden", ...style }}
         onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}>
      {list.length
        ? /* Every image is mounted once and never re-`src`d. Swapping src on a
             shared element blanks it while the new file decodes, and that blank
             frame — with the incoming layer still at opacity 0 — was the flicker.
             Stacked layers cross-blur with a plain CSS transition instead, so
             both directions animate and nothing ever reloads. */
          list.map((s, n) => (
            <img key={n} className={"ph-img" + (n === shown ? " is-on" : "")} src={s}
                 alt={n === shown ? label : ""} aria-hidden={n === shown ? undefined : "true"}
                 style={imgStyle} />
          ))
        : <span style={{ position: "absolute", inset: 0, display: "flex", justifyContent: "center",
          alignItems: labelAlign === "top" ? "flex-start" : labelAlign === "bottom" ? "flex-end" : "center",
          font: "var(--type-eyebrow)", letterSpacing: "var(--tr-label)", textTransform: "uppercase", color: fg, textAlign: "center",
          padding: labelAlign === "center" ? 12 : 24 }}>{label}</span>}
      {list.length > 1 && (
        <React.Fragment>
          {/* stopPropagation so browsing the gallery never triggers the card's own
             click-to-navigate, and never counts as an admin selection. */}
          <IconButton icon="arrow-left" label="ก่อนหน้า" variant="outline" size="sm"
            className="ph-arrow ph-arrow--prev" onClick={step(-1)} />
          <IconButton icon="arrow-right" label="ถัดไป" variant="outline" size="sm"
            className="ph-arrow ph-arrow--next" onClick={step(1)} />
          <span className="ph-dots" aria-hidden="true">
            {list.map((_, n) => <i key={n} className={n === shown ? "is-on" : ""} />)}
          </span>
        </React.Fragment>
      )}
    </div>
  );
}

/* Display weights are deliberately light for a modern, quiet page: 500 for display
   type, 400 for sub-heads, tuned to Noto Sans Thai's optical weight. */
/* The supplied lockup (mark + wordmark) is a single PNG — used as-is, never rebuilt. */
function Logo({ height = 34 }) {
  return <img src="../../assets/logo-lockup.png" alt="Superior Printing House" style={{ height, width: "auto" }} />;
}

/* Nav links are plain type — no pills, no boxes. The active item is simply the
   only one set in full brand black. */
function NavLink({ children, active, onClick }) {
  return (
    <button type="button" onClick={onClick} style={{
      border: 0, background: "none", padding: 0, cursor: "pointer",
      /* 18, not 15: the labels are English now, so they render in Cormorant, whose
         lowercase sits small for its point size beside the Thai elsewhere. */
      fontFamily: "var(--font-core)", fontSize: 18, fontWeight: "var(--fw-medium)",
      letterSpacing: "-.004em", color: active ? "var(--text-strong)" : "var(--text-muted)",
      whiteSpace: "nowrap", flexShrink: 0,
      transition: "var(--transition-control)" }}>
      {children}
    </button>
  );
}

/* Two-tier header: logo above nav in the centre column. The phone and quote CTA sit
   in their own grid column, centred against the FULL header height rather than the
   logo row alone, and the empty first column keeps the centre optically centred. */
/* The menu, defined once and rendered twice — header and footer. Two hand-kept
   copies drift the first time an entry is added.
   "work.projects" is a copy.js path, not a category: it scopes the work page to that
   one rail. Books and Packaging have no page of their own yet and deliberately do
   nothing rather than lead somewhere empty. */
/* The rails, as addresses. Each one is a real URL a visitor can bookmark, a search
   engine can index and an assistant can cite — /slogan rather than a filter applied
   to /. The rail is still a copy.js path underneath; this is only the mapping.
   Derived from copy.js rather than hard-coded, so a fourth showcase gets a URL by
   existing. */
function railRoutes(t) {
  const out = [{ path: "/slogan", rail: "work.projects", label: t.work.showcaseTitle }];
  (t.showcases || []).forEach(function (s, i) {
    out.push({ path: "/" + String(s.title).toLowerCase().replace(/[^a-z0-9]+/g, "-"),
               rail: "showcases." + i + ".projects", label: s.title });
  });
  return out;
}

function pathFor(t, page, focus) {
  if (page !== "work") return "/";
  const hit = railRoutes(t).filter(function (r) { return r.rail === (focus && focus.rail); })[0];
  return hit ? hit.path : "/slogan";
}

/* The reverse, for a cold load and for the back button. Anything unrecognised is
   home — a URL nobody minted should land somewhere real, not on an error. */
function routeFor(t, pathname) {
  const p = String(pathname || "/").replace(/\/+$/, "") || "/";
  const hit = railRoutes(t).filter(function (r) { return r.path === p; })[0];
  return hit ? { page: "work", focus: { rail: hit.rail } } : { page: "home", focus: null };
}

/* Which menu entry is the page you are on. Every rail entry is page "work" now, so
   the page name alone would set all three in black at once. */
function isHere(item, page, focus) {
  if (item.page !== page) return false;
  return item.rail ? item.rail === (focus && focus.rail) : true;
}

function navItems(t, go) {
  const labels = [t.nav.slogan, t.nav.books, t.nav.packaging];
  return [{ label: t.nav.home, page: "home", path: "/", onClick: () => go("home") }].concat(
    railRoutes(t).map(function (r, i) {
      return { label: labels[i] || r.label, page: "work", rail: r.rail, path: r.path,
               onClick: () => go("work", { rail: r.rail }) };
    }));
}

function Nav({ page, focus, go, t }) {
  return (
    <header style={{ position: "sticky", top: 0, zIndex: 40, background: "color-mix(in srgb, var(--white) 90%, transparent)", backdropFilter: "saturate(180%) blur(20px)", WebkitBackdropFilter: "saturate(180%) blur(20px)", borderBottom: "1px solid var(--line-subtle)", display: "grid", gridTemplateColumns: "1fr auto 1fr", alignItems: "center", gap: 24, padding: "22px 40px" }} className="site-header">
      <span />
      <div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 18 }}>
        <button onClick={() => go("home")} style={{ border: 0, background: "none", cursor: "pointer", padding: 0 }}><Logo height={34} /></button>
        <nav className="site-nav" style={{ display: "flex", gap: 32, justifyContent: "center", flexWrap: "wrap" }}>
          {navItems(t, go).map(it => (
            <NavLink key={it.label} active={isHere(it, page, focus)} onClick={it.onClick}>{it.label}</NavLink>
          ))}
        </nav>
      </div>
      <div className="header-actions" style={{ justifySelf: "end", display: "flex", gap: 24, alignItems: "center", lineHeight: 1 }}>
        <a className="tel-link" href={telHref(t)} style={{ fontFamily: "var(--font-mono)", fontSize: 13, color: "var(--text-muted)", whiteSpace: "nowrap", display: "flex", alignItems: "center", textDecoration: "none" }}>
          {t.nav.phone}<span className="tel-cta" aria-hidden="true">{t.nav.callNow}</span>
        </a>
        <Button className="line-btn" size="sm" variant="primary" href={lineHref(t)} target="_blank" rel="noopener" iconRight="arrow-up-right">{t.nav.line}</Button>
      </div>
    </header>
  );
}

function SectionHead({ eyebrow, title, aside , style }) {
  return (
    <div style={{ display: "flex", alignItems: "flex-end", justifyContent: "space-between", gap: 40, ...style }}>
      <div style={{ display: "flex", flexDirection: "column", gap: 14, maxWidth: 620 }}>
        {eyebrow && <span style={{ font: "var(--type-eyebrow)", letterSpacing: "var(--tr-label)", textTransform: "uppercase", color: "var(--text-subtle)" }}>{eyebrow}</span>}
        <h2 style={{ fontFamily: "var(--font-display)", fontWeight: 500, fontSize: 44, lineHeight: 1.2,
          letterSpacing: "-.01em", color: "var(--text-strong)", margin: 0 }}>{title}</h2>
      </div>
      {aside && <div style={{ fontSize: 14, lineHeight: 1.75, color: "var(--text-muted)", maxWidth: 280 }}>{aside}</div>}
    </div>
  );
}

/* Columns of links under headings, the way a printer's site with a lot of pages
   does it, then a thin bar with the legal line and the mark. The three columns are
   the only ones there is real content for — invented headings pointing at pages
   that do not exist would look complete and lead nowhere. */
function FootLink({ href, onClick, children }) {
  const style = { border: 0, background: "none", padding: 0, textAlign: "left", cursor: "pointer",
    fontFamily: "var(--font-core)", fontSize: 14, lineHeight: 1.9, color: "var(--text-muted)",
    textDecoration: "none", transition: "var(--transition-control)" };
  if (href) return <a className="foot-link" href={href} target="_blank" rel="noopener" style={style}>{children}</a>;
  return <button type="button" className="foot-link" onClick={onClick} style={style}>{children}</button>;
}

/* Back to top. It belongs to the window rather than to any one screen, so it lives
   beside the page body and stays mounted across navigations — the pages are long
   enough that scrolling back by hand is a chore on every one of them.
   It only appears once there is something to scroll back from; before that it is a
   button that does nothing sitting over the hero. */
function ScrollTop({ t }) {
  const [on, setOn] = React.useState(false);
  React.useEffect(() => {
    const check = () => setOn(window.scrollY > 600);
    check();
    window.addEventListener("scroll", check, { passive: true });
    return () => window.removeEventListener("scroll", check);
  }, []);
  const up = () => window.scrollTo({
    top: 0,
    behavior: window.matchMedia("(prefers-reduced-motion: reduce)").matches ? "auto" : "smooth",
  });
  return (
    <div className={"scroll-top" + (on ? " is-on" : "")}>
      <IconButton icon="arrow-up" label={t.nav.top} variant="outline" size="lg" onClick={up}
        style={{ background: "var(--white)", boxShadow: "var(--shadow-md)" }} />
    </div>
  );
}

function Footer({ t, go, page }) {
  const reveal = useReveal();
  return (
    <footer ref={reveal} className="site-footer reveal" style={{ background: "var(--surface-sunken)", padding: "56px 56px 28px", display: "flex", flexDirection: "column", gap: 40 }}>
      {/* One column of links, centred. Home is left out — a link to the page you are
          reading the bottom of is not a link anyone follows. */}
      <div className="foot-cols" style={{ display: "flex", justifyContent: "center", gap: 8 }}>
        {socialLinks(t).map(s => (
          <a key={s.label} className="social-link" href={s.href} target="_blank" rel="noopener">
            <Tag>{s.label}</Tag>
          </a>
        ))}
      </div>

      <div style={{ height: 1, background: "var(--line-subtle)" }} />
      {/* Grid, not space-between: the social row has to sit on the footer's centre
          line, and with flex it only lands there when the blocks either side happen
          to be the same width. */}
      <div className="foot-bottom" style={{ display: "grid", gridTemplateColumns: "minmax(0,1fr) auto minmax(0,1fr)", alignItems: "center", gap: 24 }}>
        <div style={{ display: "flex", flexDirection: "column", gap: 4, maxWidth: 460 }}>
          <span style={{ fontSize: 13, lineHeight: 1.7, color: "var(--text-strong)" }}>{t.footer.company}</span>
          <span style={{ fontSize: 13, lineHeight: 1.7, color: "var(--text-muted)" }}>{t.footer.address}</span>
          <a className="tel-link" href={telHref(t)} style={{ fontFamily: "var(--font-mono)", fontSize: 13, color: "var(--text-muted)", textDecoration: "none" }}>
            {t.nav.phone}<span className="tel-cta" aria-hidden="true">{t.nav.callNow}</span>
          </a>
          <span style={{ fontFamily: "var(--font-mono)", fontSize: 12, color: "var(--text-subtle)" }}>{t.footer.copyright}</span>
        </div>
        {/* Top-aligned, not centred: its heading should sit on the same line as the
            company name rather than floating in the middle of the taller block. */}
        <div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 12, justifySelf: "center", alignSelf: "start" }}>
          <span style={{ fontFamily: "var(--font-display)", fontWeight: 500, fontSize: 17, color: "var(--text-strong)" }}>{t.footer.colWork}</span>
          <nav style={{ display: "flex", gap: 32, flexWrap: "wrap", justifyContent: "center" }}>
            {navItems(t, go).filter(it => it.page !== "home").map(it => (
              <FootLink key={it.label} onClick={it.onClick}>{it.label}</FootLink>
            ))}
          </nav>
        </div>
        <span style={{ justifySelf: "end" }}><Logo height={44} /></span>
      </div>
    </footer>
  );
}

Object.assign(window, { Ph, galleryList, focalOrDefault, lineHref, telHref, socialLinks, cardView, railTitle, railRoutes, pathFor, routeFor, workCats, allWorkItems, useReveal, Logo, NavLink, Nav, SectionHead, ScrollTop, Footer });
