/* =================================================================
   App — rich loader, route-aware nav, hash router + page transitions
   ================================================================= */
const { useState: uS, useEffect: uE, useRef: uR } = React;

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "topDirection": "index",
  "language": "jp",
  "accent": "#f3f2ee",
  "grain": true,
  "cursor": true,
  "transitions": true
}/*EDITMODE-END*/;

/* ---------- custom cursor ---------- */
function Cursor({ enabled }) {
  const dot = uR(null), ring = uR(null);
  uE(() => {
    if (!enabled) return;
    if (window.matchMedia('(hover: none)').matches) return;
    document.body.classList.add('cur-custom');
    let mx = innerWidth / 2, my = innerHeight / 2, rx = mx, ry = my, raf;
    const move = (e) => {
      mx = e.clientX; my = e.clientY;
      if (dot.current) dot.current.style.transform = `translate(${mx}px,${my}px) translate(-50%,-50%)`;
    };
    const loop = () => {
      rx += (mx - rx) * 0.18; ry += (my - ry) * 0.18;
      if (ring.current) ring.current.style.transform = `translate(${rx}px,${ry}px) translate(-50%,-50%)`;
      raf = requestAnimationFrame(loop);
    };
    const over = (e) => { if (e.target.closest('[data-hot], a, button')) document.body.classList.add('cur-hot'); };
    const out = (e) => { if (e.target.closest('[data-hot], a, button')) document.body.classList.remove('cur-hot'); };
    window.addEventListener('mousemove', move);
    document.addEventListener('mouseover', over);
    document.addEventListener('mouseout', out);
    loop();
    return () => { window.removeEventListener('mousemove', move);
      document.removeEventListener('mouseover', over); document.removeEventListener('mouseout', out);
      cancelAnimationFrame(raf); document.body.classList.remove('cur-hot'); document.body.classList.remove('cur-custom'); };
  }, [enabled]);
  if (!enabled) return null;
  return (<><div className="cursor-dot" ref={dot} /><div className="cursor-ring" ref={ring} /></>);
}

/* ---------- rich loader: counts EVERY integer 0 → 100 (imperative, cheap) ---------- */
function Loader({ data, onDone }) {
  const [exit, setExit] = uS(false);
  const numRef = uR(null), fillRef = uR(null);
  const charRefs = uR([]), mfRefs = uR([]);
  const reduce = typeof window !== 'undefined'
    && window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  const name = data.meta.nameEn;
  const chars = name.split('');
  const manifest = [
    { label: 'FILM & MOTION', count: data.works.film.length, at: 28 },
    { label: 'APPLICATIONS', count: data.works.app.length, at: 56 },
    { label: 'WEB TOOLS', count: data.works.web.length, at: 82 },
  ];

  uE(() => {
    const paint = (v) => {
      if (numRef.current) numRef.current.textContent = String(v).padStart(3, '0');
      if (fillRef.current) fillRef.current.style.transform = `scaleX(${v / 100})`;
      const revealed = Math.round((v / 100) * chars.length);
      charRefs.current.forEach((el, i) => { if (el) el.classList.toggle('on', i < revealed); });
      mfRefs.current.forEach((el, i) => {
        if (el) el.classList.toggle('on', v >= manifest[i].at);
        if (el) { const m = el.querySelector('.ld-mf-mark'); if (m) m.textContent = v >= manifest[i].at ? '●' : '○'; }
      });
    };

    if (reduce) { paint(100); setExit(true); const d = setTimeout(onDone, 500); return () => clearTimeout(d); }

    // wall-clock driven: completes in real time even if timers are throttled,
    // and steps through every integer at full speed when visible.
    const DUR = 2400;
    const start = Date.now();
    let last = -1, done = false;
    const id = setInterval(() => {
      const p = Math.min(1, (Date.now() - start) / DUR);
      const v = Math.min(100, Math.floor(Math.pow(p, 0.92) * 100));
      if (v !== last) { paint(v); last = v; }
      if (p >= 1 && !done) {
        done = true; clearInterval(id); paint(100);
        setTimeout(() => setExit(true), 360);
        setTimeout(onDone, 1280);
      }
    }, 16);
    paint(0);
    return () => { clearInterval(id); };
  }, []);

  return (
    <div className={`loader ${exit ? 'exit' : ''}`}>
      <div className="ld-panels" aria-hidden="true">
        {Array.from({ length: 7 }).map((_, i) => <span key={i} style={{ '--ci': i }} />)}
      </div>

      <div className="ld-content">
        <div className="ld-top">
          <span className="mono-label">SAKUYA MIZUNO — PORTFOLIO</span>
          <span className="mono-label faint">{data.meta.nameJp}</span>
        </div>

        <div className="ld-center">
          <div className="ld-name" aria-label={name}>
            {chars.map((ch, i) => (
              <span key={i} ref={(el) => (charRefs.current[i] = el)} className="ld-ch">
                {ch === ' ' ? '\u00A0' : ch}
              </span>
            ))}
          </div>

          <ul className="ld-manifest">
            {manifest.map((m, i) => (
              <li key={m.label} ref={(el) => (mfRefs.current[i] = el)} className="ld-mf">
                <span className="ld-mf-mark">○</span>
                <span className="ld-mf-label">{m.label}</span>
                <span className="ld-mf-dots" />
                <span className="ld-mf-count mono-label tnum">{String(m.count).padStart(2, '0')}</span>
              </li>
            ))}
          </ul>
        </div>

        <div className="ld-bottom">
          <div className="ld-track"><span className="ld-fill" ref={fillRef} /></div>
          <div className="ld-pct">
            <span className="ld-pct-n tnum" ref={numRef}>000</span>
            <span className="ld-pct-sym mono-label">%</span>
          </div>
        </div>
      </div>
    </div>
  );
}

/* ---------- theme (light / dark) ---------- */
const readStoredTheme = () => { try { return localStorage.getItem('sm-theme'); } catch (e) { return null; } };
const systemTheme = () => (window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark');

/* ---------- top nav (route-aware) ---------- */
function Nav({ data, lang, setLang, route, theme, toggleTheme }) {
  const [solid, setSolid] = uS(false);
  uE(() => {
    const onScroll = () => setSolid(window.scrollY > 80);
    onScroll();
    window.addEventListener('scroll', onScroll, { passive: true });
    return () => window.removeEventListener('scroll', onScroll);
  }, []);
  // home keeps the nav transparent until you scroll; inner pages are always solid
  const home = route.view === 'home';
  const isWorks = route.view === 'category' || route.view === 'work';
  const onCat = (id) => (route.cat === id);

  return (
    <nav className={`nav ${(solid || !home) ? 'solid' : ''}`}>
      <a className="nav-brand" href="/" data-hot>
        <span className="nav-brand-en">{data.meta.nameEn}</span>
        <span className="nav-brand-jp">{data.meta.nameJp}</span>
      </a>
      <div className="nav-right">
        <a href="/videos" className={`nav-link ${isWorks ? 'on' : ''}`} data-hot><T t={data.ui.work} lang={lang} /></a>
        <a href="/about" className={`nav-link ${route.view === 'about' ? 'on' : ''}`} data-hot><T t={data.ui.about} lang={lang} /></a>
        <a href="/contact" className={`nav-link ${route.view === 'contact' ? 'on' : ''}`} data-hot><T t={data.ui.contact} lang={lang} /></a>
        <button className="lang" data-hot aria-label="Switch language"
          title={lang === 'jp' ? 'Switch to English' : '日本語に切り替え'}
          onClick={() => setLang(lang === 'jp' ? 'en' : 'jp')}>
          <span className={`lang-b ${lang === 'jp' ? 'on' : ''}`}>JP</span>
          <span className="lang-sep">/</span>
          <span className={`lang-b ${lang === 'en' ? 'on' : ''}`}>EN</span>
        </button>
        <button className="theme-toggle" data-hot onClick={toggleTheme}
          aria-label={theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'}
          title={theme === 'dark' ? 'Light mode' : 'Dark mode'}>
          <span className="tt-orb" />
        </button>
      </div>
    </nav>
  );
}

/* ---------- transition curtain ---------- */
function Curtain({ phase, label }) {
  return (
    <div className={`curtain ph-${phase}`} aria-hidden="true">
      {Array.from({ length: 7 }).map((_, i) => <span className="cpanel" key={i} style={{ '--ci': i }} />)}
      <span className="curtain-mark mono-label">{label}</span>
    </div>
  );
}

/* ---------- root ---------- */
function App() {
  const data = window.PORTFOLIO;
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
  const [intro, setIntro] = uS(true);
  const lang = t.language === 'en' ? 'en' : 'jp';
  const setLang = (l) => setTweak('language', l);

  const route = useRoute();
  const [display, setDisplay] = uS(route);
  const [phase, setPhase] = uS('idle');      // idle | cover | reveal
  const [curtainLabel, setCurtainLabel] = uS('');

  /* ---- theme: device default, manual choice persisted ---- */
  const [theme, setTheme] = uS(() => readStoredTheme() || systemTheme());
  uE(() => { document.documentElement.setAttribute('data-theme', theme); }, [theme]);
  uE(() => {
    const mq = window.matchMedia('(prefers-color-scheme: light)');
    const onSys = () => { if (!readStoredTheme()) setTheme(systemTheme()); };
    mq.addEventListener('change', onSys);
    return () => mq.removeEventListener('change', onSys);
  }, []);
  const toggleTheme = () => setTheme((p) => {
    const n = p === 'dark' ? 'light' : 'dark';
    try { localStorage.setItem('sm-theme', n); } catch (e) {}
    return n;
  });

  /* accent var */
  uE(() => {
    document.documentElement.style.setProperty('--accent', t.accent);
    document.documentElement.style.setProperty('--accent-dim',
      t.accent === '#f3f2ee' ? 'rgba(243,242,238,0.5)' : t.accent);
  }, [t.accent]);

  /* SPA link handler — intercepts internal <a href="/..."> clicks */
  uE(() => {
    const handle = (e) => {
      const a = e.target.closest('a[href]');
      if (!a || a.target === '_blank') return;
      const href = a.getAttribute('href');
      if (!href || href.startsWith('http') || href.startsWith('//') || href.startsWith('mailto:') || href.startsWith('tel:')) return;
      e.preventDefault();
      if (href !== location.pathname) {
        history.pushState(null, '', href);
        window.dispatchEvent(new PopStateEvent('popstate', { state: null }));
      }
    };
    document.addEventListener('click', handle);
    return () => document.removeEventListener('click', handle);
  }, []);

  /* route change → curtain wipe → swap content */
  uE(() => {
    if (route.key === display.key) return;
    if (!t.transitions) { setDisplay(route); window.scrollTo(0, 0); return; }
    setCurtainLabel(routeLabel(route, lang));
    setPhase('cover');
    const t1 = setTimeout(() => { setDisplay(route); window.scrollTo(0, 0); setPhase('reveal'); }, 560);
    const t2 = setTimeout(() => setPhase('idle'), 1180);
    return () => { clearTimeout(t1); clearTimeout(t2); };
  }, [route.key]);

  /* page view tracking */
  uE(() => { if (!intro) window.smTrack?.(display.key); }, [display.key, intro]);

  /* global reveal observer — re-arms on each page swap + lang change */
  uE(() => {
    if (intro) return;
    const io = new IntersectionObserver((entries) => {
      entries.forEach((e) => { if (e.isIntersecting) { e.target.classList.add('in'); io.unobserve(e.target); } });
    }, { threshold: 0.12, rootMargin: '0px 0px -6% 0px' });
    const id = setTimeout(() => {
      document.querySelectorAll('.reveal:not(.in)').forEach((el) => io.observe(el));
    }, 80);
    return () => { clearTimeout(id); io.disconnect(); };
  }, [intro, display.key, lang, t.topDirection]);

  return (
    <div className="app">
      {t.grain && <div className="grain" />}
      <div className="vignette" /><div className="scan" />
      <Cursor enabled={t.cursor} />

      {intro && <Loader data={data} onDone={() => setIntro(false)} />}

      <div className={`stage ${intro ? 'lock' : 'go'}`}>
        <Nav data={data} lang={lang} setLang={setLang} route={display} theme={theme} toggleTheme={toggleTheme} />
        <main key={display.key} className="route-main">
          <Page route={display} data={data} lang={lang} topDirection={t.topDirection} />
        </main>
      </div>

      <Curtain phase={phase} label={curtainLabel} />

      <TweaksPanel>
        <TweakSection label="トップの方向性 / Top" />
        <TweakRadio label="Direction" value={t.topDirection}
          options={['index', 'reel', 'vault']}
          onChange={(v) => setTweak('topDirection', v)} />
        <p className="tw-note">index=静かな索引 · reel=動くマーキー · vault=ターミナル目録</p>

        <TweakSection label="言語 / Language" />
        <TweakRadio label="Language" value={t.language}
          options={['jp', 'en']} onChange={(v) => setTweak('language', v)} />

        <TweakSection label="質感 / Texture" />
        <TweakColor label="Accent" value={t.accent}
          options={['#f3f2ee', '#c9a227', '#7db0ff', '#ff5a3c']}
          onChange={(v) => setTweak('accent', v)} />
        <p className="tw-note">既定は無彩色（モノクロ）。色は最小限のハイライトにのみ反映。</p>
        <TweakToggle label="Page transitions" value={t.transitions} onChange={(v) => setTweak('transitions', v)} />
        <TweakToggle label="Film grain" value={t.grain} onChange={(v) => setTweak('grain', v)} />
        <TweakToggle label="Custom cursor" value={t.cursor} onChange={(v) => setTweak('cursor', v)} />
      </TweaksPanel>
    </div>
  );
}

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