// app.jsx — main site component.
// Picks copy from window.SITE_CONTENT (loaded from content.js).

const { useState, useEffect, useRef, useMemo } = React;

// ─────────────────────────────────────────────────────────────────────────────
// Cursor warmth — soft radial gradient that follows the pointer.
// Pointer-events: none so it never steals input. Hidden on touch & reduced-motion.
function CursorWarmth() {
  const ref = useRef(null);
  useEffect(() => {
    const el = ref.current;
    if (!el) return;
    let raf = 0;
    let tx = window.innerWidth / 2;
    let ty = window.innerHeight / 3;
    let cx = tx, cy = ty;
    const onMove = (e) => {
      tx = e.clientX;
      ty = e.clientY;
      el.classList.add("on");
    };
    const tick = () => {
      cx += (tx - cx) * 0.12;
      cy += (ty - cy) * 0.12;
      el.style.setProperty("--mx", cx + "px");
      el.style.setProperty("--my", cy + "px");
      raf = requestAnimationFrame(tick);
    };
    window.addEventListener("pointermove", onMove);
    raf = requestAnimationFrame(tick);
    return () => {
      window.removeEventListener("pointermove", onMove);
      cancelAnimationFrame(raf);
    };
  }, []);
  return <div ref={ref} className="cursor-warmth" />;
}

// ─────────────────────────────────────────────────────────────────────────────
// Reveal-on-scroll wrapper — fades children in as they enter the viewport.
function Reveal({ children, delay = 0, as: Tag = "div", ...rest }) {
  const ref = useRef(null);
  const [visible, setVisible] = useState(false);
  useEffect(() => {
    const el = ref.current;
    if (!el) return;
    if (typeof IntersectionObserver === "undefined") {
      setVisible(true);
      return;
    }
    const io = new IntersectionObserver(
      (entries) => {
        entries.forEach((e) => {
          if (e.isIntersecting) {
            setVisible(true);
            io.disconnect();
          }
        });
      },
      { threshold: 0.12, rootMargin: "0px 0px -40px 0px" }
    );
    io.observe(el);
    return () => io.disconnect();
  }, []);
  return (
    <Tag
      ref={ref}
      {...rest}
      className={(rest.className ? rest.className + " " : "") + "reveal" + (visible ? " visible" : "")}
      style={{ "--reveal-delay": delay + "ms", ...(rest.style || {}) }}
    >
      {children}
    </Tag>
  );
}

// ─────────────────────────────────────────────────────────────────────────────
// Rotating word in the headline.
// Cycles through words with a small blur/lift transition.
function Rotator({ words }) {
  const [idx, setIdx] = useState(0);
  const [phase, setPhase] = useState("steady"); // 'steady' | 'out' | 'in'
  const measureRef = useRef(null);
  const [width, setWidth] = useState("auto");

  // Measure the widest word so the headline doesn't reflow on each swap.
  useEffect(() => {
    const node = measureRef.current;
    if (!node) return;
    let max = 0;
    Array.from(node.children).forEach((c) => {
      if (c.offsetWidth > max) max = c.offsetWidth;
    });
    if (max) setWidth(max + "px");
  }, [words]);

  useEffect(() => {
    if (words.length <= 1) return;
    let cancel = false;
    const cycle = () => {
      setPhase("out");
      setTimeout(() => {
        if (cancel) return;
        setIdx((i) => (i + 1) % words.length);
        setPhase("in");
        // Force reflow before going steady so the transition kicks in.
        requestAnimationFrame(() => {
          requestAnimationFrame(() => {
            if (cancel) return;
            setPhase("steady");
          });
        });
      }, 480);
    };
    const id = setInterval(cycle, 2800);
    return () => {
      cancel = true;
      clearInterval(id);
    };
  }, [words]);

  return (
    <span className="rotator" style={{ width, minWidth: width }}>
      <span className={"rotator-word " + (phase === "steady" ? "" : phase)}>{words[idx]}</span>
      {/* Hidden measurer */}
      <span
        ref={measureRef}
        aria-hidden="true"
        style={{
          position: "absolute",
          visibility: "hidden",
          whiteSpace: "nowrap",
          left: 0,
          top: 0,
          pointerEvents: "none",
        }}
      >
        {words.map((w, i) => (
          <span key={i} style={{ display: "inline-block", paddingRight: 4 }}>{w}</span>
        ))}
      </span>
    </span>
  );
}

// ─────────────────────────────────────────────────────────────────────────────
// Tweaks defaults — wrapped in EDITMODE markers so the host can persist edits
// straight back into this HTML file. (Block must be valid JSON.)
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "theme": "ink",
  "typography": "mix",
  "accent": "#e9b384",
  "lang": "en",
  "warmth": true
}/*EDITMODE-END*/;

// ─────────────────────────────────────────────────────────────────────────────
function App() {
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);

  // Apply theme + typography to <html> so CSS vars cascade.
  useEffect(() => {
    document.documentElement.dataset.theme = t.theme || "ink";
    document.documentElement.dataset.typography = t.typography || "sans";
    if (t.accent) {
      document.documentElement.style.setProperty("--accent", t.accent);
      // Build a soft alpha version. Simple mix via rgba string.
      const rgb = hexToRgb(t.accent);
      if (rgb) {
        document.documentElement.style.setProperty(
          "--accent-soft",
          `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.18)`
        );
      }
    }
  }, [t.theme, t.typography, t.accent]);

  // Lang state lives in tweak too, but we let the user toggle via the topbar.
  const lang = t.lang === "fr" ? "fr" : "en";
  const setLang = (v) => setTweak("lang", v);

  const C = window.SITE_CONTENT;
  const L = C[lang];
  const M = C.meta;

  // Update <html lang>
  useEffect(() => {
    document.documentElement.lang = lang;
    document.title = `${M.name} — ${lang === "fr" ? M.role_fr : M.role_en}`;
  }, [lang, M]);

  return (
    <>
      {t.warmth && <CursorWarmth />}

      <main className="page">
        <header className="topbar">
          <div className="mark">
            <span className="dot" aria-hidden="true"></span>
            {M.name}
          </div>
          <div className="lang" role="group" aria-label="Language">
            <button className={lang === "en" ? "on" : ""} onClick={() => setLang("en")}>EN</button>
            <button className={lang === "fr" ? "on" : ""} onClick={() => setLang("fr")}>FR</button>
          </div>
        </header>

        <section className="hero">
          <Reveal>
            <div className="eyebrow">{L.hero.eyebrow}</div>
          </Reveal>
          <Reveal delay={80}>
            <h1 className="headline">
              {L.hero.lineA}{" "}
              <Rotator words={L.hero.rotating} />
              <br />
              {L.hero.lineB}
            </h1>
          </Reveal>
          <Reveal delay={180}>
            <p className="sub">{L.hero.sub}</p>
          </Reveal>
        </section>

        <section className="section" aria-labelledby="now-label">
          <Reveal>
            <h2 id="now-label" className="section-label">{L.now.label}</h2>
          </Reveal>
          <ul className="now-list">
            {L.now.items.map((line, i) => (
              <Reveal key={i} as="li" delay={i * 80}>{line}</Reveal>
            ))}
          </ul>
        </section>

        <section className="section" aria-labelledby="about-label">
          <Reveal>
            <h2 id="about-label" className="section-label">{L.about.label}</h2>
          </Reveal>
          <div className="prose">
            {L.about.paragraphs.map((p, i) => (
              <Reveal key={i} as="p" delay={i * 80}>{p}</Reveal>
            ))}
          </div>
        </section>

        <section className="section" aria-labelledby="work-label">
          <Reveal>
            <h2 id="work-label" className="section-label">{L.work.label}</h2>
          </Reveal>
          <Reveal delay={60}>
            <div className="work-sub">{L.work.sub}</div>
          </Reveal>
          <div>
            {L.work.roles.map((r, i) => (
              <Reveal key={i} className="role" delay={i * 60}>
                <div className="role-period">{r.period}</div>
                <div>
                  <h3 className="role-title">{r.title}</h3>
                  <p className="role-body">{r.body}</p>
                </div>
              </Reveal>
            ))}
          </div>
        </section>

        <section className="section" aria-labelledby="extras-label">
          <Reveal>
            <h2 id="extras-label" className="section-label">{L.extras.label}</h2>
          </Reveal>
          <div className="extras">
            {L.extras.items.map((line, i) => (
              <Reveal key={i} as="span" delay={i * 60}>{line}</Reveal>
            ))}
          </div>
        </section>

        <section className="section" aria-labelledby="contact-label">
          <Reveal>
            <h2 id="contact-label" className="section-label">{L.contact.label}</h2>
          </Reveal>
          <Reveal delay={60}>
            <p className="contact-lines">{L.contact.lines[0]}</p>
          </Reveal>
          <Reveal delay={140}>
            <div className="contact-actions">
              <a className="btn primary" href={M.linkedin} target="_blank" rel="noopener noreferrer">
                {L.contact.cta_linkedin} <span className="arrow">→</span>
              </a>
            </div>
          </Reveal>
        </section>

        <footer className="footer">
          <span>{L.footer.year} · {M.name}</span>
          <span>{L.footer.built}</span>
        </footer>
      </main>

      <TweaksPanel>
        <TweakSection label={lang === "fr" ? "Langue" : "Language"} />
        <TweakRadio
          label={lang === "fr" ? "Langue" : "Language"}
          value={lang}
          options={[{ value: "en", label: "EN" }, { value: "fr", label: "FR" }]}
          onChange={(v) => setTweak("lang", v)}
        />

        <TweakSection label={lang === "fr" ? "Thème" : "Theme"} />
        <TweakRadio
          label={lang === "fr" ? "Palette" : "Palette"}
          value={t.theme}
          options={[
            { value: "ink", label: "Ink" },
            { value: "cream", label: "Cream" },
            { value: "sand", label: "Sand" },
          ]}
          onChange={(v) => setTweak("theme", v)}
        />
        <TweakColor
          label={lang === "fr" ? "Accent" : "Accent"}
          value={t.accent}
          onChange={(v) => setTweak("accent", v)}
        />

        <TweakSection label={lang === "fr" ? "Typographie" : "Typography"} />
        <TweakRadio
          label={lang === "fr" ? "Style" : "Style"}
          value={t.typography}
          options={[
            { value: "sans", label: "Sans" },
            { value: "serif", label: "Serif" },
            { value: "mix", label: "Mix" },
          ]}
          onChange={(v) => setTweak("typography", v)}
        />

        <TweakSection label={lang === "fr" ? "Effets" : "Effects"} />
        <TweakToggle
          label={lang === "fr" ? "Halo curseur" : "Cursor warmth"}
          value={!!t.warmth}
          onChange={(v) => setTweak("warmth", v)}
        />
      </TweaksPanel>
    </>
  );
}

// ─────────────────────────────────────────────────────────────────────────────
function hexToRgb(hex) {
  if (!hex) return null;
  const m = hex.replace("#", "").match(/^([0-9a-f]{6}|[0-9a-f]{3})$/i);
  if (!m) return null;
  let h = m[1];
  if (h.length === 3) h = h.split("").map((c) => c + c).join("");
  return {
    r: parseInt(h.slice(0, 2), 16),
    g: parseInt(h.slice(2, 4), 16),
    b: parseInt(h.slice(4, 6), 16),
  };
}

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