/* global React, ReactDOM, renderMarkdown,
          wordCount, readMinutes, slugify, toFullMarkdown,
          useTweaks, TweaksPanel, TweakSection, TweakRadio, TweakToggle */
const { useState, useEffect, useRef, useMemo } = React;

// =========================================================================
// App — three states: idle | converting | reading.
// The TopBar is always there. The URL input is always there.
// =========================================================================
function App() {
  const [tweaks, setTweak] = useTweaks(/*EDITMODE-BEGIN*/{
    "theme":      "light",
    "paperGrain": true,
    "density":    "comfortable",
    "measure":    "88",
    "view":       "rendered"
  }/*EDITMODE-END*/);

  const [state, setState] = useState("idle"); // idle | converting | reading
  const [url, setUrl] = useState("");
  const [doc, setDoc] = useState(null);
  const [pendingUrl, setPendingUrl] = useState("");
  const cacheRef = useRef(new Map());          // url → converted doc

  // Central navigation: either pushes a new history entry (push=true) or
  // restores from one (push=false, used by popstate). Cache hits skip the
  // converting animation so the browser's native back/forward feels instant.
  function goTo(rawUrl, opts) {
    const push = !opts || opts.push !== false;
    const u = (rawUrl || "").trim();
    if (!u) return;
    if (push) {
      try {
        history.pushState({ mwk: true, url: u }, "", `?url=${encodeURIComponent(u)}`);
      } catch (e) { /* ignored */ }
    }
    setUrl(u);
    setPendingUrl(u);
    const cached = cacheRef.current.get(u);
    if (cached) {
      setDoc(cached);
      setState("reading");
    } else {
      setState("converting");
    }
  }

  // Theme + grain on root. We add a `.theming` class for one frame so the
  // attribute-driven color swap paints instantly without any element's
  // transition animating between old and new (which is what made the
  // toggle feel choppy — top bar bg fading over 220ms while body bg
  // snapped). After paint, transitions resume for normal hover/focus.
  useEffect(() => {
    const html = document.documentElement;
    html.classList.add("theming");
    html.dataset.theme = tweaks.theme;
    html.dataset.density = tweaks.density;
    html.dataset.grain = tweaks.paperGrain ? "on" : "off";
    const id = requestAnimationFrame(() => html.classList.remove("theming"));
    return () => cancelAnimationFrame(id);
  }, [tweaks.theme, tweaks.density, tweaks.paperGrain]);

  // Cmd/Ctrl+K to focus URL input from anywhere.
  const urlRef = useRef(null);
  useEffect(() => {
    function onKey(e) {
      if ((e.metaKey || e.ctrlKey) && e.key === "k") {
        e.preventDefault();
        urlRef.current && urlRef.current.focus();
        urlRef.current && urlRef.current.select();
      }
      if (e.key === "Escape" && state === "reading") {
        // Just blur input if focused.
        document.activeElement === urlRef.current && urlRef.current.blur();
      }
    }
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [state]);

  // Browser back/forward — restore from cache if possible.
  useEffect(() => {
    function onPop(e) {
      const s = e.state;
      if (s && s.mwk && s.url) {
        goTo(s.url, { push: false });
      } else {
        // Stepped outside our managed range — show the home.
        setDoc(null);
        setUrl("");
        setPendingUrl("");
        setState("idle");
      }
    }
    window.addEventListener("popstate", onPop);
    return () => window.removeEventListener("popstate", onPop);
  }, []);

  // Deep-link on first mount: ?url=… converts immediately.
  useEffect(() => {
    try {
      const params = new URLSearchParams(window.location.search);
      const initial = (params.get("url") || "").trim();
      if (initial) {
        history.replaceState(
          { mwk: true, url: initial },
          "",
          `?url=${encodeURIComponent(initial)}`
        );
        setUrl(initial);
        setPendingUrl(initial);
        setState("converting");
      }
    } catch (e) { /* ignored */ }
  }, []);

  function onConvert(e) {
    e && e.preventDefault();
    goTo(url);
  }

  function onConvertDone(result) {
    if (result) {
      // Cache under both keys — the URL we pushed to history (what popstate
      // will look up by) AND the server-normalized url (what link clicks
      // resolve to). Same doc either way, no reconvert on back/forward.
      if (pendingUrl) cacheRef.current.set(pendingUrl, result);
      if (result.url && result.url !== pendingUrl) {
        cacheRef.current.set(result.url, result);
      }
    }
    setDoc(result);
    setState("reading");
  }

  function onCancel() {
    if (doc) {
      setState("reading");
      setPendingUrl("");
    } else {
      setState("idle");
      setPendingUrl("");
    }
  }

  function newConversion() {
    setDoc(null);
    setUrl("");
    setPendingUrl("");
    setState("idle");
    try { history.pushState(null, "", window.location.pathname); } catch (e) { /* ignored */ }
    setTimeout(() => urlRef.current && urlRef.current.focus(), 50);
  }

  return (
    <div className="app" data-state={state}>
      <TopBar
        urlRef={urlRef}
        url={url}
        setUrl={setUrl}
        onSubmit={onConvert}
        state={state}
        doc={doc}
        tweaks={tweaks}
        setTweak={setTweak}
        onLogo={newConversion}
      />

      {state === "idle" && (
        <IdleHero
          url={url}
          setUrl={setUrl}
          onSubmit={onConvert}
          urlRef={urlRef}
        />
      )}

      {state === "converting" && (
        <Converting
          url={pendingUrl}
          onDone={onConvertDone}
          onCancel={onCancel}
        />
      )}

      {state === "reading" && doc && (
        <DocumentView
          doc={doc}
          tweaks={tweaks}
          view={tweaks.view}
          onNavigate={(href) => goTo(href)}
        />
      )}

      <TweaksPanel title="Tweaks">
        <TweakSection label="Theme">
          <TweakRadio
            label="Mode"
            value={tweaks.theme}
            onChange={v => setTweak("theme", v)}
            options={[
              { value: "light", label: "Light" },
              { value: "dark",  label: "Dark"  },
            ]}
          />
          <TweakToggle
            label="Paper grain"
            value={tweaks.paperGrain}
            onChange={v => setTweak("paperGrain", v)}
          />
        </TweakSection>
        <TweakSection label="Reading">
          <TweakRadio
            label="Density"
            value={tweaks.density}
            onChange={v => setTweak("density", v)}
            options={[
              { value: "comfortable", label: "Cozy" },
              { value: "compact",     label: "Compact" },
            ]}
          />
          <TweakRadio
            label="Measure"
            value={tweaks.measure}
            onChange={v => setTweak("measure", v)}
            options={[
              { value: "60", label: "Narrow" },
              { value: "88", label: "Default" },
              { value: "100", label: "Wide" },
            ]}
          />
        </TweakSection>
      </TweaksPanel>
    </div>
  );
}

// =========================================================================
// TopBar — wordmark · slim URL input (reading only) · doc actions · theme.
// On idle the URL input lives in the hero; the top bar stays minimal.
// =========================================================================
function TopBar({ urlRef, url, setUrl, onSubmit, state, doc, tweaks, setTweak, onLogo }) {
  const [focused, setFocused] = useState(false);
  const [copied, setCopied] = useState(false);
  const [downloaded, setDownloaded] = useState(false);
  const isReading = state === "reading" && doc;

  function onCopy() {
    if (!doc) return;
    const text = toFullMarkdown(doc);
    navigator.clipboard && navigator.clipboard.writeText(text).catch(() => {});
    setCopied(true);
    setTimeout(() => setCopied(false), 1600);
  }
  function onDownload() {
    if (!doc) return;
    setDownloaded(true);
    setTimeout(() => setDownloaded(false), 1600);
    try {
      const blob = new Blob([toFullMarkdown(doc)], { type: "text/markdown" });
      const a = document.createElement("a");
      a.href = URL.createObjectURL(blob);
      a.download = slugify(doc.title) + ".md";
      a.click();
    } catch (e) { /* ignored */ }
  }

  return (
    <header className="tb" data-reading={isReading ? "1" : "0"}>
      {state === "idle" ? (
        // On the home page the big centered ¶ markdwn cluster in the hero
        // carries the brand — no need to repeat it as a tiny top-left
        // wordmark. The slot stays in the grid so the right cluster's
        // alignment doesn't shift between states.
        <span className="tb-spacer" />
      ) : (
        <button className="tb-mark" onClick={onLogo} title={isReading ? "Start over" : "markdwn"}>
          <span className="tb-pilcrow">¶</span>
          <span className="tb-name">markdwn</span>
        </button>
      )}

      {isReading ? (
        <>
          <form className={`tb-paste ${focused ? "is-focused" : ""}`} onSubmit={onSubmit}>
            <img src="design-system/icons/link.svg" className="tb-paste-ico" alt="" />
            <input
              ref={urlRef}
              value={url}
              onChange={e => setUrl(e.target.value)}
              onFocus={() => setFocused(true)}
              onBlur={() => setFocused(false)}
              placeholder="Convert another URL…"
              spellCheck={false}
              autoCorrect="off"
              autoCapitalize="off"
              autoComplete="off"
              className="tb-paste-input"
            />
            {url.trim() && (
              <button
                type="button"
                className="tb-clear"
                onMouseDown={(e) => { e.preventDefault(); setUrl(""); urlRef.current && urlRef.current.focus(); }}
                aria-label="Clear"
              >
                <img src="design-system/icons/x.svg" alt="" />
              </button>
            )}
            <button
              type="submit"
              className="btn btn-primary tb-paste-btn"
              disabled={!url.trim()}
            >
              Convert
              <span className="tb-paste-kbd">↵</span>
            </button>
          </form>

          <div className="tb-right">
            <ViewToggle
              value={tweaks.view}
              onChange={v => setTweak("view", v)}
            />
            <span className="tb-sep" />
            <button className={`tb-icon-btn ${copied ? "is-flash" : ""}`} onClick={onCopy} title="Copy markdown (⌘C)">
              <img src={`design-system/icons/${copied ? "clipboard-check" : "clipboard"}.svg`} alt="" />
              <span className="tb-icon-label">{copied ? "Copied" : "Copy"}</span>
            </button>
            <button className={`tb-icon-btn ${downloaded ? "is-flash" : ""}`} onClick={onDownload} title="Download .md">
              <img src={`design-system/icons/${downloaded ? "check" : "download"}.svg`} alt="" />
              <span className="tb-icon-label">{downloaded ? "Saved" : "Download"}</span>
            </button>
            <span className="tb-sep" />
            <button
              className="tb-icon"
              onClick={() => setTweak("theme", tweaks.theme === "light" ? "dark" : "light")}
              title="Toggle theme"
            >
              <img src={`design-system/icons/${tweaks.theme === "light" ? "moon" : "sun"}.svg`} alt="" />
            </button>
          </div>
        </>
      ) : (
        // Home / converting state — wordmark anchors the bar; theme toggle on the right.
        <>
          <span className="tb-spacer" />
          <div className="tb-right">
            <button
              className="tb-icon"
              onClick={() => setTweak("theme", tweaks.theme === "light" ? "dark" : "light")}
              title="Toggle theme"
            >
              <img src={`design-system/icons/${tweaks.theme === "light" ? "moon" : "sun"}.svg`} alt="" />
            </button>
          </div>
        </>
      )}
    </header>
  );
}

// =========================================================================
// ViewToggle — small segmented control for Rendered ↔ Raw.
// =========================================================================
function ViewToggle({ value, onChange }) {
  return (
    <div className="seg" role="tablist" aria-label="View">
      <button
        role="tab"
        aria-selected={value === "rendered"}
        className={`seg-btn ${value === "rendered" ? "is-active" : ""}`}
        onClick={() => onChange("rendered")}
        title="Rendered view"
      >Rendered</button>
      <button
        role="tab"
        aria-selected={value === "raw"}
        className={`seg-btn ${value === "raw" ? "is-active" : ""}`}
        onClick={() => onChange("raw")}
        title="Raw markdown source"
      >Raw</button>
    </div>
  );
}

// =========================================================================
// IdleHero — search-engine style. Big brand mark centered, big URL bar
// below it with a typewriter-cycled placeholder, and a tiny footer.
// =========================================================================
function IdleHero({ url, setUrl, onSubmit, urlRef }) {
  const [focused, setFocused] = useState(false);
  // Placeholder URLs cycled through by the typewriter effect. Real,
  // working pages so a user who actually pastes one of them gets a real
  // article instead of a 404.
  const examples = useMemo(() => ([
    { url: "en.wikipedia.org/wiki/Markdown" },
    { url: "arxiv.org/abs/1706.03762" },
    { url: "paulgraham.com/disagree.html" },
  ]), []);

  // Typewriter placeholder. Cycles through example URLs, typing each one
  // character by character, holding, then erasing, then moving on.
  // Pauses entirely the moment the user types anything.
  const [phIdx, setPhIdx] = useState(0);
  const [phText, setPhText] = useState("");
  useEffect(() => {
    if (url) return;
    let cancelled = false;
    const target = examples[phIdx].url;
    let chars = 0;
    let phase = "typing";  // typing | holding | erasing
    let timer;

    function tick() {
      if (cancelled) return;
      if (phase === "typing") {
        chars++;
        setPhText(target.slice(0, chars));
        if (chars >= target.length) {
          phase = "holding";
          timer = setTimeout(tick, 1800);
        } else {
          // Slight humanizing jitter, faster on common letters.
          timer = setTimeout(tick, 38 + Math.random() * 32);
        }
      } else if (phase === "holding") {
        phase = "erasing";
        timer = setTimeout(tick, 60);
      } else {
        // Erasing
        chars--;
        setPhText(target.slice(0, Math.max(0, chars)));
        if (chars <= 0) {
          // Move to next example after a brief beat.
          timer = setTimeout(() => {
            if (!cancelled) setPhIdx(i => (i + 1) % examples.length);
          }, 240);
        } else {
          timer = setTimeout(tick, 22);
        }
      }
    }
    timer = setTimeout(tick, 360);
    return () => { cancelled = true; clearTimeout(timer); };
  }, [phIdx, url, examples]);

  const showPh = !url;

  return (
    <section className="ih">
      <div className="ih-inner">
        <div className="ih-mark" aria-label="markdwn">
          <span className="ih-mark-pilcrow">¶</span>
          <span className="ih-mark-name">markdwn</span>
        </div>
        <p className="ih-tagline">Read the web.</p>
        <form className={`ih-paste ${focused ? "is-focused" : ""} ${url ? "has-text" : ""}`} onSubmit={onSubmit}>
          <img src="design-system/icons/link.svg" className="ih-paste-ico" alt="" />
          <div className="ih-paste-field">
            <input
              ref={urlRef}
              value={url}
              onChange={e => setUrl(e.target.value)}
              onFocus={() => setFocused(true)}
              onBlur={() => setFocused(false)}
              spellCheck={false}
              autoCorrect="off"
              autoCapitalize="off"
              autoComplete="off"
              className="ih-paste-input"
              autoFocus
              aria-label="URL to convert"
            />
            {showPh && (
              <span className="ih-paste-ph" aria-hidden="true">
                <span className="ih-paste-ph-proto">https://</span>
                <span className="ih-paste-ph-rest">{phText}</span>
                <span className="ih-paste-ph-caret" />
              </span>
            )}
          </div>
          {url.trim() && (
            <button
              type="button"
              className="ih-paste-clear"
              onMouseDown={(e) => { e.preventDefault(); setUrl(""); urlRef.current && urlRef.current.focus(); }}
              aria-label="Clear"
            >
              <img src="design-system/icons/x.svg" alt="" />
            </button>
          )}
          <button
            type="submit"
            className="btn btn-primary ih-paste-btn"
            disabled={!url.trim()}
          >
            Convert
            <span className="ih-paste-kbd">↵</span>
          </button>
        </form>

        <p className="ih-instruction">
          Paste any public URL — an article, paper, post, or doc —
          and we'll strip the chrome and set the words like a book.
        </p>
      </div>

      <footer className="ih-footer" aria-hidden="true" />
    </section>
  );
}

// =========================================================================
// Converting — kicks off the real fetch in parallel with a 4-step
// animation so the conversion never feels jarringly instant. Stays in
// the same card to show an error if the API rejects.
// =========================================================================
function Converting({ url, onDone, onCancel }) {
  const steps = useMemo(() => ([
    { label: "Fetching the page",        ms: 520 },
    { label: "Stripping ads and chrome", ms: 460 },
    { label: "Finding the article",      ms: 420 },
    { label: "Setting the type",         ms: 380 },
  ]), []);
  const [step, setStep] = useState(0);
  const [error, setError] = useState(null);

  useEffect(() => {
    let cancelled = false;
    const abort = new AbortController();
    const timers = [];

    // Animation — purely visual; the real work happens in parallel.
    let total = 100;
    steps.forEach((s, i) => {
      total += s.ms;
      timers.push(setTimeout(() => { if (!cancelled) setStep(i + 1); }, total - s.ms + 100));
    });
    const animDone = new Promise(resolve => {
      timers.push(setTimeout(resolve, total + 320));
    });

    // Real fetch.
    const apiDone = fetch(`/api/convert?url=${encodeURIComponent(url)}`, { signal: abort.signal })
      .then(async (r) => {
        const data = await r.json().catch(() => ({}));
        if (!r.ok) throw new Error(data.error || `Conversion failed (${r.status})`);
        return data;
      });

    Promise.all([animDone, apiDone.catch((e) => ({ __err: e.message || "Conversion failed" }))])
      .then(([, result]) => {
        if (cancelled) return;
        if (result && result.__err) setError(result.__err);
        else onDone(result);
      });

    return () => {
      cancelled = true;
      abort.abort();
      timers.forEach(clearTimeout);
    };
  }, [url]);

  return (
    <section className="cv">
      <div className={`cv-card ${error ? "is-error" : ""}`}>
        <header className="cv-head">
          <span className="cv-eyebrow">{error ? "Couldn't convert" : "Converting"}</span>
          <button className="cv-cancel" onClick={onCancel} aria-label="Cancel">
            <img src="design-system/icons/x.svg" alt="" />
          </button>
        </header>
        <p className="cv-url" title={url}>
          <img src="design-system/icons/link.svg" alt="" />
          <span>{prettifyUrl(url)}</span>
        </p>

        {error ? (
          <div className="cv-error">
            <p className="cv-error-msg">{error}</p>
            <button type="button" className="btn btn-primary cv-error-btn" onClick={onCancel}>
              Try another URL
            </button>
          </div>
        ) : (
          <ol className="cv-steps">
            {steps.map((s, i) => {
              const done = step > i;
              const active = step === i;
              return (
                <li key={i} className={`cv-step ${done ? "is-done" : ""} ${active ? "is-active" : ""}`}>
                  <span className="cv-step-mark">
                    {done
                      ? <img src="design-system/icons/check.svg" alt="" />
                      : <span className="cv-dot" />}
                  </span>
                  <span className="cv-step-label">{s.label}</span>
                </li>
              );
            })}
          </ol>
        )}
      </div>
    </section>
  );
}

function prettifyUrl(u) { return u.replace(/^https?:\/\//, ""); }

// =========================================================================
// DocumentView — the converted markdown rendered as an e-reader page.
// Structured header (kicker · title · byline) → body (rendered or raw)
// → end ornament → small footer.
// =========================================================================
function DocumentView({ doc, tweaks, view, onNavigate }) {
  const rendered = useMemo(() => renderMarkdown(doc.body), [doc.body]);
  const wc = useMemo(() => wordCount(doc.body), [doc.body]);
  const rm = useMemo(() => readMinutes(doc.body), [doc.body]);
  const fullMd = useMemo(() => toFullMarkdown(doc), [doc]);

  // Intercept clicks on absolute http(s) links inside the article so they
  // convert in place instead of leaving the site. Links flagged
  // data-external="true" (kicker site, "View original") bypass the intercept.
  function onArticleClick(e) {
    if (!onNavigate) return;
    if (e.defaultPrevented || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
    if (e.button !== undefined && e.button !== 0) return;
    const a = e.target.closest && e.target.closest("a");
    if (!a) return;
    if (a.dataset && a.dataset.external === "true") return;
    const href = a.getAttribute("href");
    if (!href || !/^https?:\/\//i.test(href)) return;
    e.preventDefault();
    onNavigate(href);
  }

  return (
    <main className="dv" data-density={tweaks.density} data-measure={tweaks.measure} data-view={view}>
      <article className="dv-article" onClick={onArticleClick}>
        <header className="dh">
          <div className="dh-kicker">
            <span className="dh-kicker-site">
              <img src="design-system/icons/globe.svg" alt="" />
              <a href={doc.url} target="_blank" rel="noreferrer" data-external="true">{doc.site}</a>
            </span>
            <span className="dh-kicker-dot">·</span>
            <span>{doc.kind || "Article"}</span>
            <span className="dh-kicker-dot">·</span>
            <span>{rm} min read</span>
          </div>
          <h1 className="dh-title">{doc.title}</h1>
          {(doc.byline || doc.date) && (
            <p className="dh-byline">
              {doc.byline && <span>{doc.byline}</span>}
              {doc.byline && doc.date && <span className="dh-byline-dot">·</span>}
              {doc.date && <span>{doc.date}</span>}
            </p>
          )}
          <div className="dh-rule" aria-hidden="true">
            <span>§</span>
          </div>
        </header>

        {view === "raw" ? (
          <pre className="dv-raw">{fullMd}</pre>
        ) : (
          <div className="dv-body">
            {rendered}
          </div>
        )}

        <footer className="dv-foot">
          <div className="dv-foot-ornament" aria-hidden="true">
            <span>·</span><span>·</span><span>·</span>
          </div>
          <p className="dv-foot-end">End.</p>
          <p className="dv-foot-meta">
            <span>{wc.toLocaleString()} words</span>
            <span className="dh-kicker-dot">·</span>
            <a href={doc.url} target="_blank" rel="noreferrer" data-external="true">View original</a>
          </p>
        </footer>
      </article>
    </main>
  );
}

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

Object.assign(window, { App, TopBar, IdleHero, Converting, DocumentView });
