// reader.jsx — Ashfall German reader, multi-chapter, candles + audio

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

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "theme": "ashfall",
  "font": "cormorant",
  "fontSize": 19,
  "lineHeight": 1.55,
  "pageWidth": 580,
  "justify": true,
  "audioEnabled": true
}/*EDITMODE-END*/;

function buildPages() {
  const out = [];
  out.push({ kind: "cover" });
  out.push({ kind: "halftitle" });
  out.push({ kind: "warning" });
  out.push({ kind: "toc" });
  for (const ch of CHAPTERS) {
    out.push({ kind: "chapter-title", ch });
    const pages = window[`CH${ch.n}_PAGES`];
    if (pages && pages.length) {
      pages.forEach((p, i) => out.push({ ...p, ch, isLastInCh: i === pages.length - 1 }));
    } else {
      out.push({ kind: "synopsis", ch });
    }
  }
  out.push({ kind: "thefin" });
  return out;
}

const LS_KEY = "ashfall.v1";
function loadState() {
  try { return JSON.parse(localStorage.getItem(LS_KEY)) || {}; } catch (_) { return {}; }
}
function saveState(s) {
  try { localStorage.setItem(LS_KEY, JSON.stringify(s)); } catch (_) {}
}

let __turnAudio = new Audio("audio_cues/page_turn.mp3");
__turnAudio.volume = 0.07;
__turnAudio.preload = "auto";
let __turnFallback = null;
(function initTurn() {
  try {
    const C = window.AudioContext || window.webkitAudioContext;
    if (C) __turnFallback = new C();
  } catch (_) {}
  __turnAudio.addEventListener("error", () => {}, { once: true });
})();
function pageTurnSound(forward) {
  if (__turnAudio.readyState >= 2) {
    __turnAudio.currentTime = 0;
    __turnAudio.play().catch(() => {});
    return;
  }
  if (__turnFallback) {
    const ctx = __turnFallback;
    if (ctx.state === "suspended") { try { ctx.resume(); } catch (_) {} }
    const now = ctx.currentTime;
    const dur = 0.15;
    const buf = ctx.createBuffer(1, Math.floor(ctx.sampleRate * dur), ctx.sampleRate);
    const data = buf.getChannelData(0);
    for (let i = 0; i < data.length; i++) {
      data[i] = (Math.random() * 2 - 1) * Math.exp(-i / (ctx.sampleRate * 0.02)) * 0.15;
    }
    const src = ctx.createBufferSource();
    src.buffer = buf;
    const bp = ctx.createBiquadFilter();
    bp.type = "bandpass"; bp.frequency.value = forward ? 2000 : 1500; bp.Q.value = 2;
    const gain = ctx.createGain();
    gain.gain.value = 0.12;
    src.connect(bp).connect(gain).connect(ctx.destination);
    src.start(now);
  }
}

function Couverture() {
  return (
    <div className="cover">
      <div className="cover-bg"></div>
      <div className="cover-top">
        <div className="cover-eyebrow">{BOOK.publisher}</div>
        <div className="cover-pub">22 Wachen · 1 Käfig</div>
      </div>
      <div className="cover-mid">
        <div className="cover-rule"></div>
        <h1 className="cover-title">Ashfall</h1>
        <div className="cover-tag">»Leere ist keine Abwesenheit. Es ist eine Methode.«</div>
        <div className="cover-rule pulse"></div>
      </div>
      <div className="cover-bot">
        <div className="cover-author">{BOOK.author}</div>
        <div className="cover-pub" style={{ opacity: 0.55, marginTop: 6 }}>
          Marseille&nbsp;·&nbsp;MMXXVI
        </div>
      </div>
    </div>
  );
}

function HalfTitle() {
  return (
    <div className="halftitle">
      <div className="ht-name">Ashfall</div>
      <div className="ht-tag">»Man inszeniert ein solches Dekor nur für jemanden, den man bereits hat.«</div>
      <div className="ht-meta">{BOOK.author}</div>
      <div className="ht-meta" style={{ opacity: 0.6 }}>{BOOK.publisher}</div>
      <div className="ht-warning">{BOOK.warning}</div>
    </div>
  );
}

function WarningPage() {
  return (
    <div className="warning-page">
      <div className="warning-head">
        <div className="stamp">Content Warnings</div>
        <h3>Dieses Buch enthält.</h3>
        <div className="by">»Wenn du einen Ausgang brauchst, ist er noch da.«</div>
      </div>
      <div className="warning-grid">
        {CONTENT_WARNINGS.map((w, i) => (
          <div key={i} className="item">{w}</div>
        ))}
      </div>
      <div className="warning-foot">
        Keine explizite anatomische Sexualität. Der ganze »Spice« läuft über das Nervensystem
        von Sasha: Tachykardie, Hypoxie, kalter Schweiß, Pupillenerweiterung.
        <br />Dies ist, per Konstruktion, ein psychologisches Buch.
      </div>
    </div>
  );
}

function TOC({ litUpTo, current, onJump }) {
  return (
    <div className="toc">
      <div className="toc-head">
        <div className="label">Die fünf Düfte</div>
        <div className="title">Beeren · Rosen · Feigenbaum · Tuberose · Lilie</div>
        <div className="sub">Jede Kerze durchzieht mehrere Kapitel. Jeder Duft, ein Versprechen.</div>
      </div>
      <div className="toc-grid">
        {SCENTS.map(sc => {
          const chs = chaptersForScent(sc.id);
          return (
            <ScentCandle key={sc.id} scent={sc} chapters={chs}
                         lit={chs.some(n => n <= litUpTo)}
                         current={chs.includes(current)}
                         onClick={() => chs.length > 0 && onJump(chs[0])} />
          );
        })}
      </div>
      <div className="toc-list">
        {CHAPTERS.map((ch) => (
          <div key={ch.n}
               className={`toc-item ${ch.n === current ? "current" : ""}`}
               onClick={() => onJump(ch.n)}>
            <span className="num">{ROMAN[ch.n]}</span>
            <span className="name">{ch.name}</span>
            <span className="act">{["I","II","III"][actOf(ch.n)-1]}</span>
          </div>
        ))}
      </div>
    </div>
  );
}

function ChapterTitle({ ch }) {
  const sc = scentForChapter(ch.n);
  return (
    <div className="chapter-title">
      <div className="ch-marker">{actLabel(ch.n)}</div>
      <div className="ch-rule"></div>
      <div className="ch-number">Kapitel {ROMAN[ch.n]}</div>
      <h2 className="ch-name">{ch.name}</h2>
      <div className="ch-sub">»{ch.epigraph}«</div>
      {sc && (
        <div className="scent" style={{ "--scent-glow": sc.glow }}>
          <div className="scent-row">
            <div className="scent-disk"></div>
            <div className="scent-name">Kerze&nbsp;·&nbsp;{sc.name}</div>
          </div>
          <div className="scent-notes">{sc.notes}</div>
          <div className="scent-moment">{sc.moment}</div>
        </div>
      )}
      <div className="ch-act-line">
        <div className="h"></div>
        <div className="lbl">Wache Nr.{String(ch.n).padStart(2,"0")}</div>
        <div className="h"></div>
      </div>
    </div>
  );
}

function Body({ html }) {
  return <div className="prose" dangerouslySetInnerHTML={{ __html: html }} />;
}

function Synopsis({ ch }) {
  return (
    <div className="synopsis">
      <div className="synopsis-head">
        <div className="ttl">{ch.name}</div>
        <div className="meta">{actLabel(ch.n)}</div>
      </div>
      <div className="synopsis-body">
        {ch.synopsis.map((p, i) => (
          <p key={i} dangerouslySetInnerHTML={{ __html: p }} />
        ))}
      </div>
      <div className="synopsis-tags">
        <div className="synopsis-tag">
          <div className="k">Sound Design</div>
          <div className="v">{ch.soundDesign}</div>
        </div>
        <div className="synopsis-tag">
          <div className="k">Spice-Notiz</div>
          <div className="v">{ch.spice}</div>
        </div>
        <div className="synopsis-tag">
          <div className="k">Audio-Wache</div>
          <div className="v">{ch.audioTitle}</div>
        </div>
        <div className="synopsis-tag">
          <div className="k">Epigraph</div>
          <div className="v">»{ch.epigraph}«</div>
        </div>
      </div>
      <div className="synopsis-status">Vorschau · In Kürze</div>
    </div>
  );
}

function EndCard({ ch }) {
  const next = CHAPTERS[ch.n] && CHAPTERS[ch.n];
  return (
    <div className="endcard">
      <div className="endcard-mark">⁂</div>
      <div className="endcard-line"></div>
      <div className="endcard-next">Ende von Kapitel {ROMAN[ch.n]}</div>
      <div className="endcard-ch" style={{ fontStyle: "italic" }}>— {ch.name} —</div>
      <div className="endcard-line"></div>
      {next && (
        <div className="endcard-next" style={{ opacity: 0.55 }}>
          Kapitel {ROMAN[next.n]}&nbsp;·&nbsp;
          <span style={{ fontStyle: "italic", fontFamily: "var(--serif)", textTransform: "none", letterSpacing: 0 }}>
            {next.name}
          </span>
        </div>
      )}
    </div>
  );
}

function TheFin() {
  return (
    <div className="endcard">
      <div className="endcard-mark">∎</div>
      <div className="endcard-line"></div>
      <div className="endcard-next">Ende des Buches</div>
      <div className="endcard-ch">— Ashfall —</div>
      <div className="endcard-line"></div>
      <div className="endcard-next" style={{ opacity: 0.55 }}>{BOOK.author}&nbsp;·&nbsp;{BOOK.publisher}</div>
    </div>
  );
}

function Reader() {
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
  const SCREENS = useMemo(buildPages, []);
  const TOTAL = SCREENS.length;

  const initial = loadState();
  const [idx, setIdx] = useState(typeof initial.idx === "number" ? Math.min(initial.idx, TOTAL - 1) : 0);
  const [litUpTo, setLitUpTo] = useState(typeof initial.litUpTo === "number" ? initial.litUpTo : 0);
  const [phase, setPhase] = useState("show");
  const [audioOn, setAudioOn] = useState(t.audioEnabled);

  const screen = SCREENS[idx];
  const currentCh = screen?.ch?.n || (screen?.kind === "chapter-title" ? screen.ch.n : null);

  useEffect(() => {
    if (currentCh && currentCh > litUpTo) setLitUpTo(currentCh);
  }, [currentCh]);

  useEffect(() => {
    saveState({ idx, litUpTo });
  }, [idx, litUpTo]);

  window.useAudioCues(idx);

  useEffect(() => {
    document.documentElement.className = `theme-${t.theme}`;
  }, [t.theme]);

  useEffect(() => {
    document.body.classList.toggle("is-cover", screen?.kind === "cover");
  }, [screen?.kind]);

  useEffect(() => {
    const r = document.documentElement.style;
    const fontMap = {
      cormorant: '"Cormorant Garamond","EB Garamond","Spectral",Georgia,serif',
      garamond:  '"EB Garamond","Cormorant Garamond","Spectral",Georgia,serif',
      spectral:  '"Spectral","EB Garamond",Georgia,serif',
    };
    r.setProperty("--serif", fontMap[t.font] || fontMap.cormorant);
    r.setProperty("--display", fontMap[t.font] || fontMap.cormorant);
    r.setProperty("--type-size", `${t.fontSize}px`);
    r.setProperty("--line-h", String(t.lineHeight));
    r.setProperty("--page-w", `${t.pageWidth}px`);
  }, [t.font, t.fontSize, t.lineHeight, t.pageWidth]);

  const go = useCallback((delta) => {
    const next = idx + delta;
    if (next < 0 || next >= TOTAL) return;
    pageTurnSound(delta > 0);
    setPhase(delta > 0 ? "exit-r" : "exit-l");
    setTimeout(() => {
      setIdx(next);
      setPhase(delta > 0 ? "enter-r" : "enter-l");
      requestAnimationFrame(() => requestAnimationFrame(() => setPhase("show")));
    }, 260);
  }, [idx, TOTAL]);

  const jumpToChapter = useCallback((n) => {
    const target = SCREENS.findIndex(s => s.kind === "chapter-title" && s.ch.n === n);
    if (target === -1) return;
    setIdx(target);
    setPhase("show");
  }, [SCREENS]);

  useEffect(() => {
    const onKey = (e) => {
      const tgt = e.target;
      if (tgt && (tgt.tagName === "INPUT" || tgt.tagName === "TEXTAREA" || tgt.isContentEditable)) return;
      if (e.key === "ArrowRight" || e.key === " " || e.key === "PageDown") { e.preventDefault(); go(+1); }
      else if (e.key === "ArrowLeft" || e.key === "PageUp") { e.preventDefault(); go(-1); }
      else if (e.key === "Home") { e.preventDefault(); setIdx(0); setPhase("show"); }
      else if (e.key === "End")  { e.preventDefault(); setIdx(TOTAL - 1); setPhase("show"); }
      else if (e.key === "t" || e.key === "T") {
        const i = SCREENS.findIndex(s => s.kind === "toc");
        if (i !== -1) { setIdx(i); setPhase("show"); }
      }
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [go, TOTAL, SCREENS]);

  const stageRef = useRef(null);
  const onStageClick = (e) => {
    if (e.target.closest(".audio-bar")) return;
    if (e.target.closest(".twk-panel")) return;
    const rect = stageRef.current.getBoundingClientRect();
    const x = e.clientX - rect.left;
    if (x < rect.width / 2) go(-1); else go(+1);
  };

  const swipeState = useRef({ startX: 0, startY: 0, startTime: 0 });
  useEffect(() => {
    const el = stageRef.current;
    if (!el) return;
    const onTouchStart = (e) => {
      const t = e.changedTouches[0];
      swipeState.current = { startX: t.clientX, startY: t.clientY, startTime: Date.now() };
    };
    const onTouchEnd = (e) => {
      const t = e.changedTouches[0];
      const dx = t.clientX - swipeState.current.startX;
      const dy = t.clientY - swipeState.current.startY;
      const dt = Date.now() - swipeState.current.startTime;
      if (dt > 500) return;
      if (Math.abs(dx) > 50 && Math.abs(dx) > Math.abs(dy) * 1.5) {
        if (dx < 0) go(+1); else go(-1);
        return;
      }
      if (Math.abs(dx) < 20 && Math.abs(dy) < 20) {
        const rect = el.getBoundingClientRect();
        const x = t.clientX - rect.left;
        if (x < rect.width / 2) go(-1); else go(+1);
      }
    };
    el.addEventListener("touchstart", onTouchStart, { passive: true });
    el.addEventListener("touchend", onTouchEnd, { passive: true });
    return () => {
      el.removeEventListener("touchstart", onTouchStart);
      el.removeEventListener("touchend", onTouchEnd);
    };
  }, [go]);

  const [chromeVisible, setChromeVisible] = useState(true);
  const chromeTimer = useRef(null);
  const showChrome = useCallback(() => {
    setChromeVisible(true);
    clearTimeout(chromeTimer.current);
    if (window.innerWidth <= 640) {
      chromeTimer.current = setTimeout(() => setChromeVisible(false), 3000);
    }
  }, []);
  useEffect(() => {
    window.addEventListener("touchstart", showChrome, { passive: true });
    window.addEventListener("mousemove", showChrome, { passive: true });
    return () => {
      window.removeEventListener("touchstart", showChrome);
      window.removeEventListener("mousemove", showChrome);
    };
  }, [showChrome]);
  useEffect(() => {
    showChrome();
  }, [idx]);

  const chrumbLeft  = currentCh ? `Kapitel ${ROMAN[currentCh]}` : (screen.kind === "toc" ? "Inhalt" : screen.kind === "cover" ? "Umschlag" : "");
  const chrumbRight = currentCh ? CHAPTERS[currentCh-1].name : "";
  const progress = idx === 0 ? 0 : (idx / (TOTAL - 1)) * 100;

  const chForAudio = currentCh || (screen.kind === "chapter-title" ? screen.ch.n : null) || 1;
  const showAudio = !!currentCh;

  const themeOpts = [
    { value: "ashfall", label: "Ashfall" },
    { value: "marbre",  label: "Marmor" },
    { value: "beton",   label: "Beton" },
  ];

  const renderScreen = () => {
    switch (screen.kind) {
      case "cover":          return <Couverture />;
      case "halftitle":      return <HalfTitle />;
      case "warning":        return <WarningPage />;
      case "toc":            return <TOC litUpTo={litUpTo} current={currentCh} onJump={jumpToChapter} />;
      case "chapter-title":  return <ChapterTitle ch={screen.ch} />;
      case "body":           return <Body html={screen.html} />;
      case "synopsis":       return <Synopsis ch={screen.ch} />;
      case "endcard":        return <EndCard ch={screen.ch} />;
      case "thefin":         return <TheFin />;
      default: return null;
    }
  };

  return (
    <div className="reader">
      <div className={`chrome-top${chromeVisible ? "" : " chrome-hidden"}`}>
        <div className="crumb-l">{chrumbLeft || <span>&nbsp;</span>}</div>
        <div className="crumb-c"><span className="brand">ASHFALL</span></div>
        <div className="crumb-r">
          {chrumbRight ? (
            <span style={{ fontStyle: "italic", letterSpacing: "0.06em", textTransform: "none" }}>{chrumbRight}</span>
          ) : <span>&nbsp;</span>}
        </div>
      </div>

      <div className="stage" ref={stageRef} onClick={onStageClick}>
        <div className={`page ${phase}`} style={{ textAlign: t.justify ? undefined : "left" }}>
          <div className="page-inner">
            {renderScreen()}
          </div>
        </div>
        <div className="nav-arrow l">‹</div>
        <div className="nav-arrow r">›</div>
      </div>

      <div className={`progress${chromeVisible ? "" : " chrome-hidden"}`}>
        <div className="progress-fill" style={{ width: `${progress}%` }}></div>
      </div>

      {showAudio && (
        <AudioBar
          chapter={chForAudio}
          src={audioSrc(chForAudio)}
          title={CHAPTERS[chForAudio - 1].audioTitle}
          enabled={audioOn}
          onToggle={(v) => setAudioOn(v)}
        />
      )}

      <div className={`chrome-bot${chromeVisible ? "" : " chrome-hidden"}`}>
        <div className="crumb-l">
          <CandlesRow litUpTo={litUpTo} current={currentCh} onSelect={jumpToChapter} />
        </div>
        <div className="crumb-c">
          <span>{String(idx + 1).padStart(2, "0")}</span>
          <span className="dot"></span>
          <span>{String(TOTAL).padStart(2, "0")}</span>
        </div>
        <div className="crumb-r">
          <span>← →&nbsp;&nbsp;Seite&nbsp;&nbsp;·&nbsp;&nbsp;T&nbsp;Inhalt</span>
        </div>
      </div>

      <TweaksPanel title="Einstellungen">
        <TweakSection label="Atmosphäre" />
        <TweakRadio
          label="Thema"
          value={t.theme}
          options={themeOpts}
          onChange={(v) => setTweak("theme", v)}
        />
        <TweakToggle
          label="Soundtrack"
          value={audioOn}
          onChange={(v) => { setAudioOn(v); setTweak("audioEnabled", v); }}
        />

        <TweakSection label="Typografie" />
        <TweakRadio
          label="Schriftart"
          value={t.font}
          options={[
            { value: "cormorant", label: "Cormorant" },
            { value: "garamond",  label: "Garamond" },
            { value: "spectral",  label: "Spectral" },
          ]}
          onChange={(v) => setTweak("font", v)}
        />
        <TweakSlider
          label="Schriftgröße"
          value={t.fontSize} min={14} max={24} step={1} unit="px"
          onChange={(v) => setTweak("fontSize", v)}
        />
        <TweakSlider
          label="Zeilenabstand"
          value={t.lineHeight} min={1.3} max={2.0} step={0.05}
          onChange={(v) => setTweak("lineHeight", v)}
        />
        <TweakSlider
          label="Seitenbreite"
          value={t.pageWidth} min={420} max={760} step={10} unit="px"
          onChange={(v) => setTweak("pageWidth", v)}
        />
        <TweakToggle
          label="Blocksatz"
          value={t.justify}
          onChange={(v) => setTweak("justify", v)}
        />

        <TweakSection label="Navigation" />
        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 6 }}>
          <TweakButton label="Umschlag" onClick={() => { setIdx(0); setPhase("show"); }} />
          <TweakButton label="Inhalt" onClick={() => {
            const i = SCREENS.findIndex(s => s.kind === "toc");
            if (i !== -1) { setIdx(i); setPhase("show"); }
          }} />
        </div>
        <TweakSelect
          label="Gehe zu Kapitel"
          value=""
          options={[{ value: "", label: "—" }, ...CHAPTERS.map(c => ({ value: String(c.n), label: `${ROMAN[c.n]} · ${c.name}` }))]}
          onChange={(v) => v && jumpToChapter(Number(v))}
        />
      </TweaksPanel>
    </div>
  );
}

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