/* app.jsx — orchestration: hash routing, view transitions, nav, tweaks. */
const { useState: useS, useEffect: useE, useCallback: useC, useMemo } = React;
const flushSync = (ReactDOM && ReactDOM.flushSync) || ((fn) => fn());

// Swallow the benign "Transition was aborted" rejection the View Transitions API
// emits when a new transition starts before the previous finishes (fast nav).
window.addEventListener("unhandledrejection", (e) => {
  const m = e && e.reason && (e.reason.message || e.reason.name);
  if (m && /Transition was aborted|AbortError/i.test(String(m))) e.preventDefault();
});

const ACCENTS = {
  // muted, low-chroma, drawn from nature — never a fill, only a trace
  indigo: { label: "藍 indigo", c: "oklch(0.46 0.058 256)" },
  clay:   { label: "土 clay",   c: "oklch(0.52 0.066 52)" },
  moss:   { label: "苔 moss",   c: "oklch(0.52 0.05 150)" },
  rust:   { label: "錆 rust",   c: "oklch(0.52 0.085 44)" },
};
const MEASURE = { narrow: "33rem", comfortable: "37rem", wide: "41rem" };

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "accent": "oklch(0.46 0.058 256)",
  "measure": "comfortable",
  "typeBase": 20,
  "texture": true
}/*EDITMODE-END*/;

function parseHash() {
  const h = (location.hash || "").replace(/^#\/?/, "");
  if (h.startsWith("post/")) return { view: "post", slug: decodeURIComponent(h.slice(5)), topic: null };
  if (h.startsWith("topic/")) return { view: "topic", slug: null, topic: decodeURIComponent(h.slice(6)) };
  if (h === "updates") return { view: "updates", slug: null, topic: null };
  if (h === "work") return { view: "work", slug: null, topic: null };
  if (h === "about") return { view: "about", slug: null, topic: null };
  // default landing is the Writing (blog) feed
  return { view: "blog", slug: null, topic: null };
}

function NavLink({ active, onClick, children }) {
  return (
    <button className={`nav-link ${active ? "is-active" : ""}`} onClick={onClick}>
      {children}
    </button>
  );
}

function App() {
  const [tw, setTweak] = useTweaks(TWEAK_DEFAULTS);
  const data = window.DIARY;
  const lang = useSiteLang();          // current site language (re-renders on change)
  const L = (k) => window.t(k, lang);  // localized UI string

  const [route, setRoute] = useS(() => parseHash());
  const [shell, setShell] = useS(() => parseHash()); // what is actually painted
  const [fading, setFading] = useS(false);

  // hash <-> state
  useE(() => {
    const onHash = () => setRoute(parseHash());
    window.addEventListener("hashchange", onHash);
    return () => window.removeEventListener("hashchange", onHash);
  }, []);

  // transition when the route changes.
  // Preferred: the native View Transitions API (GPU-accelerated, feels app-like on
  // mobile). Fallback: the JS opacity/translate crossfade. Honors reduced-motion.
  useE(() => {
    if (route.view === shell.view && route.slug === shell.slug && route.topic === shell.topic) return;
    const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
    if (reduce) { setShell(route); window.scrollTo({ top: 0 }); return; }

    // Only ANIMATE when the reading context genuinely changes — drilling into an
    // article/topic, or backing out. Switching between top-level sections (nav /
    // bottom tab bar) SWAPS INSTANTLY, like native app tabs: a cross-fade there
    // just flashes the page through blank/ghosted content ("flicker"). Instant is
    // correct and flicker-free.
    const drillIn = (route.view === "post" || route.view === "topic")
      && (shell.view !== "post" && shell.view !== "topic");
    const drillOut = (shell.view === "post" || shell.view === "topic")
      && (route.view !== "post" && route.view !== "topic");
    const animate = drillIn || drillOut;

    if (!animate) {
      // section switch → instant, no transition
      setShell(route);
      window.scrollTo({ top: 0 });
      return;
    }

    document.documentElement.setAttribute("data-vt", drillIn ? "forward" : "back");

    if (document.startViewTransition) {
      window.scrollTo({ top: 0 });
      const swap = () => flushSync(() => { setShell(route); });
      if (window.__vtBusy) { swap(); return; }
      window.__vtBusy = true;
      try {
        const t = document.startViewTransition(swap);
        if (t.ready) t.ready.catch(() => {});
        if (t.updateCallbackDone) t.updateCallbackDone.catch(() => {});
        t.finished.catch(() => {}).finally(() => { window.__vtBusy = false; });
      } catch (e) {
        window.__vtBusy = false;
        swap();
      }
      return;
    }

    // fallback crossfade
    setFading(true);
    const id = setTimeout(() => {
      setShell(route);
      window.scrollTo({ top: 0 });
      requestAnimationFrame(() => requestAnimationFrame(() => setFading(false)));
    }, 300);
    return () => clearTimeout(id);
  }, [route]);

  const go = useC((view, arg = null) => {
    const hash = view === "post" ? `#post/${encodeURIComponent(arg)}`
      : view === "topic" ? `#topic/${encodeURIComponent(arg)}`
      : view === "blog" ? "#" : `#${view}`;
    if (location.hash !== hash) location.hash = hash;
    else setRoute(parseHash());
  }, []);

  // apply tweaks to :root
  useE(() => {
    const r = document.documentElement;
    r.style.setProperty("--accent", tw.accent);
    r.style.setProperty("--measure", MEASURE[tw.measure] || MEASURE.comfortable);
    r.style.setProperty("--type-base", `${tw.typeBase}px`);
    r.classList.toggle("no-texture", !tw.texture);
  }, [tw.accent, tw.measure, tw.typeBase, tw.texture]);

  // Esc backs out of an article or a topic page
  useE(() => {
    const onKey = (e) => {
      if (e.key === "Escape" && (shell.view === "post" || shell.view === "topic")) go("blog");
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [shell.view, go]);

  // blog: imported articles (window.BLOG), newest-first
  const posts = (window.BLOG && window.BLOG.posts) || [];
  const activePost = shell.view === "post"
    ? posts.find((p) => p.slug === shell.slug) : null;

  const postNeighbors = useMemo(() => {
    if (!activePost) return { prev: null, next: null };
    const i = posts.findIndex((p) => p.slug === activePost.slug);
    return {
      next: i > 0 ? posts[i - 1] : null,          // newer
      prev: i < posts.length - 1 ? posts[i + 1] : null, // older
    };
  }, [activePost]);

  return (
    <div className="page">
      <div className="grain" aria-hidden="true"></div>

      <nav className="topnav">
        <button className="brand" onClick={() => go("blog")}>福</button>
        <div className="nav-right">
          <div className="nav-links">
            <NavLink active={shell.view === "blog" || shell.view === "post" || shell.view === "topic"} onClick={() => go("blog")}>{L("nav_writing")}</NavLink>
            <NavLink active={shell.view === "updates"} onClick={() => go("updates")}>{L("nav_updates")}</NavLink>
            <NavLink active={shell.view === "work"} onClick={() => go("work")}>{L("nav_work")}</NavLink>
            <NavLink active={shell.view === "about"} onClick={() => go("about")}>{L("nav_about")}</NavLink>
          </div>
          <div className="lang-switch" role="group" aria-label="Site language">
            {[["en", "EN"], ["ms", "BM"], ["ja", "日本語"]].map(([lc, label]) => (
              <button key={lc}
                className={`lang-btn ${lang === lc ? "is-active" : ""}`}
                onClick={() => window.setSiteLang(lc)}>
                {label}
              </button>
            ))}
          </div>
        </div>
      </nav>

      {/* mobile bottom tab bar — thumb-reachable, app-like; hidden on desktop via CSS */}
      <nav className="tabbar" aria-label="Sections">
        {[
          ["blog", L("nav_writing"), shell.view === "blog" || shell.view === "post" || shell.view === "topic", "✎"],
          ["updates", L("nav_updates"), shell.view === "updates", "◴"],
          ["work", L("nav_work"), shell.view === "work", "▤"],
          ["about", L("nav_about"), shell.view === "about", "◔"],
        ].map(([v, label, active, glyph]) => (
          <button key={v} className={`tab ${active ? "is-active" : ""}`} onClick={() => go(v)}>
            <span className="tab-glyph" aria-hidden="true">{glyph}</span>
            <span className="tab-label">{label}</span>
          </button>
        ))}
      </nav>

      <main className="frame">
        <div className="view-wrap" style={{
          opacity: fading ? 0 : 1,
          transform: fading ? "translateY(-6px)" : "translateY(0)",
          transition: "opacity .42s cubic-bezier(.22,.61,.36,1), transform .42s cubic-bezier(.22,.61,.36,1)",
        }}>
          {shell.view === "blog" && (
            <BlogView onOpen={(slug) => go("post", slug)} onTopic={(name) => go("topic", name)} />
          )}
          {shell.view === "topic" && (
            <TopicView topic={shell.topic} onOpen={(slug) => go("post", slug)}
              onTopic={(name) => go("topic", name)} onBack={() => go("blog")} />
          )}
          {shell.view === "post" && activePost && (
            <PostView post={activePost} onBack={() => go("blog")}
              neighbors={postNeighbors} onOpen={(slug) => go("post", slug)} />
          )}
          {shell.view === "post" && !activePost && (
            <div className="view"><p className="blog-sub" style={{padding:"2rem 0"}}>
              That article could not be found. <button className="back-link" onClick={() => go("blog")}>← back to writing</button>
            </p></div>
          )}
          {shell.view === "updates" && <UpdatesView />}
          {shell.view === "work" && <WorkView work={data.work} profile={data.profile} />}
          {shell.view === "about" && <AboutView profile={data.profile} />}
        </div>
      </main>

      <TweaksPanel title="Tweaks">
        <TweakSection label="Layout" />
        <TweakRadio label="Measure" value={tw.measure}
          options={["narrow", "comfortable", "wide"]}
          onChange={(v) => setTweak("measure", v)} />

        <TweakSection label="Type" />
        <TweakSlider label="Body size" value={tw.typeBase} min={18} max={23} step={1} unit="px"
          onChange={(v) => setTweak("typeBase", v)} />

        <TweakSection label="Material" />
        <TweakColor label="Accent" value={tw.accent}
          options={Object.values(ACCENTS).map((a) => a.c)}
          onChange={(v) => setTweak("accent", v)} />
        <TweakToggle label="Paper grain" value={tw.texture}
          onChange={(v) => setTweak("texture", v)} />
      </TweaksPanel>
    </div>
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(<App />);
