// app.jsx — main App with the cinematic swap state machine
const { useState, useEffect, useRef, useCallback } = React;

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "activeTapePalette": "default",
  "swapStyle": "cinematic",
  "caseOpen": true,
  "editorialStyle": "serif"
}/*EDITMODE-END*/;

// NOTE: editorialStyle is kept for compatibility but the invented-quote
// variants it used to toggle have been removed with the placeholder content.

const PALETTE_MAP = {
  default: null,                  /* keep section accent */
  coral:   '#F2876B',
  sky:     '#7FB5E8',
  sage:    '#93CE8F',
  lilac:   '#B9A3E3',
  /* legacy stored values from the dark era → nearest new pastel */
  orange:  '#F2876B',
  red:     '#F2876B',
  blue:    '#7FB5E8',
  amber:   '#F2D98A',
};

function App() {
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
  // Apply tape palette override
  const accentOverride = PALETTE_MAP[t.activeTapePalette];

  const [active, setActive] = useState(() => localStorage.getItem('mv-section') || null); // null = landing
  const [swapState, setSwapState] = useState('idle'); // idle | ejecting | loading
  const [pendingTarget, setPendingTarget] = useState(null);
  const [openProject, setOpenProject] = useState(null);
  const [counter, setCounter] = useState('00:00:00');

  const timersRef = useRef([]);
  const startTimeRef = useRef(Date.now());

  useEffect(() => {
    localStorage.setItem('mv-section', active || '');
  }, [active]);

  // Reset counter when section changes
  useEffect(() => {
    startTimeRef.current = Date.now();
    if (!active) {
      setCounter('--:--:--');
    }
  }, [active]);

  // Counter tick — only when idle and a tape is loaded
  useEffect(() => {
    if (swapState !== 'idle' || !active) return;
    const tick = () => {
      const elapsed = Math.floor((Date.now() - startTimeRef.current) / 1000);
      const h = String(Math.floor(elapsed / 3600)).padStart(2, '0');
      const m = String(Math.floor((elapsed % 3600) / 60)).padStart(2, '0');
      const s = String(elapsed % 60).padStart(2, '0');
      setCounter(`${h}:${m}:${s}`);
    };
    tick();
    const id = setInterval(tick, 1000);
    return () => clearInterval(id);
  }, [active, swapState]);

  // Clear any pending timers
  const clearTimers = () => {
    timersRef.current.forEach(clearTimeout);
    timersRef.current = [];
  };
  useEffect(() => () => clearTimers(), []);

  const goTo = useCallback((nextId) => {
    if (nextId === active) return;
    clearTimers();

    const style = t.swapStyle;
    const ejectDuration = style === 'cinematic' ? 480 : style === 'subtle' ? 200 : 0;
    const loadDuration  = style === 'cinematic' ? 600 : style === 'subtle' ? 240 : 0;

    if (style === 'instant') {
      setActive(nextId);
      setSwapState('idle');
      return;
    }

    // If a tape is currently loaded — eject first
    if (active) {
      setSwapState('ejecting');
      setPendingTarget(nextId);
      timersRef.current.push(setTimeout(() => {
        setActive(nextId);
        setSwapState(nextId ? 'loading' : 'idle');
        if (nextId) {
          timersRef.current.push(setTimeout(() => setSwapState('idle'), loadDuration));
        }
      }, ejectDuration));
    } else {
      // No tape loaded → just load
      setActive(nextId);
      if (nextId) {
        setSwapState('loading');
        timersRef.current.push(setTimeout(() => setSwapState('idle'), loadDuration));
      } else {
        setSwapState('idle');
      }
    }
  }, [active, t.swapStyle]);

  const eject = useCallback(() => goTo(null), [goTo]);
  const goHome = useCallback(() => goTo(null), [goTo]);

  // Scroll to top when active changes
  useEffect(() => {
    if (swapState === 'loading' || swapState === 'idle') {
      window.scrollTo({ top: 0, behavior: 'smooth' });
    }
  }, [active]);

  const currentSection = SECTIONS.find(s => s.id === active);
  // Apply palette override to deck only
  const deckSection = currentSection && accentOverride
    ? { ...currentSection, accent: accentOverride }
    : currentSection;

  const contentVisible = swapState === 'idle';

  return (
    <>
      <Nav
        active={active || 'home'}
        sections={SECTIONS}
        onSelect={goTo}
        onHome={goHome}
      />

      {/* Persistent Tape Deck */}
      <div className="wrap-wide" style={{ paddingTop: 24 }}>
        <TapeDeck
          section={deckSection}
          onEject={eject}
          swapState={swapState}
          counter={counter}
        />
      </div>

      {/* Body — content swaps with fade */}
      <main className="wrap-wide" style={{ position: 'relative', minHeight: 600 }}>
        <div
          className={`swap-content ${contentVisible ? 'swap-in' : 'swap-out'}`}
          key={active || 'home'}
        >
          {!active && (
            <LandingSection
              sections={SECTIONS}
              onSelect={goTo}
              caseOpen={t.caseOpen}
              editorialStyle={t.editorialStyle}
            />
          )}
          {active === 'past'      && <PastSection      editorialStyle={t.editorialStyle} />}
          {active === 'present'   && <PresentSection   editorialStyle={t.editorialStyle} />}
          {active === 'future'    && <FutureSection    editorialStyle={t.editorialStyle} />}
          {active === 'references' && <ReferencesSection />}
        </div>
      </main>

      <Footer />

      <ProjectModal project={openProject} onClose={() => setOpenProject(null)} />

      <TweaksPanel title="Tweaks">
        <TweakSection label="Active tape palette">
          <TweakSelect
            label="Override accent"
            value={t.activeTapePalette}
            onChange={(v) => setTweak('activeTapePalette', v)}
            options={[
              { value: 'default', label: 'Default (per tape)' },
              { value: 'coral',   label: 'Coral'  },
              { value: 'sky',     label: 'Sky'    },
              { value: 'sage',    label: 'Sage'   },
              { value: 'lilac',   label: 'Lilac'  },
            ]}
          />
        </TweakSection>
        <TweakSection label="Swap animation">
          <TweakRadio
            label="Style"
            value={t.swapStyle}
            onChange={(v) => setTweak('swapStyle', v)}
            options={[
              { value: 'cinematic', label: 'Cinematic' },
              { value: 'subtle',    label: 'Subtle' },
              { value: 'instant',   label: 'Instant' },
            ]}
          />
        </TweakSection>
        <TweakSection label="Index layout">
          <TweakRadio
            label="Tapes shown as"
            value={t.caseOpen ? 'open' : 'closed'}
            onChange={(v) => setTweak('caseOpen', v === 'open')}
            options={[
              { value: 'open',   label: 'Tracklist' },
              { value: 'closed', label: 'Cards' },
            ]}
          />
        </TweakSection>
      </TweaksPanel>
    </>
  );
}

const WORD_LIMIT = 3000;

// Word-count logic
// ─────────────────
// Hybrid opt-in:
//
//   • STRICT mode — if any [data-prose] elements exist, ONLY those count.
//     Use this once the real assignment text is in: wrap each prose block
//     with <div data-prose>…</div> and the counter ignores all chrome.
//
//   • AUTO mode — fallback while placeholder copy is in play. Counts
//     semantic prose tags (p, h1–h4, li, blockquote) anywhere in the body.
//     Chrome built with <div>/<span> is naturally excluded.
//
//   • [data-noword] subtrees are excluded in either mode. Use on a
//     references / bibliography block once you have one.
const PROSE_SELECTOR = 'p, h1, h2, h3, h4, blockquote, li';
const countWordsIn = (root) => {
  if (!root) return { words: 0, mode: 'auto' };

  // Clone so we can prune [data-noword] subtrees safely
  const clone = root.cloneNode(true);
  clone.querySelectorAll('[data-noword]').forEach(el => el.remove());

  const strictNodes = clone.querySelectorAll('[data-prose]');
  const nodes = strictNodes.length > 0
    ? strictNodes
    : clone.querySelectorAll(PROSE_SELECTOR);

  let words = 0;
  nodes.forEach(n => {
    // Skip nested prose elements when in strict mode to avoid double-counting
    // (a <p> inside a [data-prose] wrapper would otherwise be counted twice in
    // auto mode — strict mode never picks <p> directly, so it's only an issue
    // when [data-prose] wraps another [data-prose]; closest() guards both.)
    if (strictNodes.length > 0 && n.parentElement && n.parentElement.closest('[data-prose]')) return;
    const txt = n.innerText || n.textContent || '';
    words += (txt.match(/\S+/g) || []).length;
  });
  return { words, mode: strictNodes.length > 0 ? 'strict' : 'auto' };
};

function Footer() {
  const [stats, setStats] = useState({ words: 0, minutes: 0, mode: 'auto' });
  const measureRef = useRef(null);

  useEffect(() => {
    const compute = () => {
      const { words, mode } = countWordsIn(measureRef.current);
      setStats({ words, mode, minutes: Math.max(1, Math.round(words / 220)) });
    };
    compute();
    const t1 = setTimeout(compute, 400);
    const t2 = setTimeout(compute, 1500);
    return () => { clearTimeout(t1); clearTimeout(t2); };
  }, []);

  const pct = Math.min(100, Math.round((stats.words / WORD_LIMIT) * 100));
  const over = stats.words > WORD_LIMIT;
  const strict = stats.mode === 'strict';

  return (
    <footer style={{
      borderTop: '1px solid var(--c-border)',
      padding: '48px 0 40px',
      marginTop: 64,
      background: '#FFFFFF',
    }}>
      <div className="wrap-wide footer-grid" style={{
        display: 'grid',
        gridTemplateColumns: 'minmax(0, 1fr) minmax(0, 2fr) minmax(0, 1fr)',
        gap: 32,
        alignItems: 'start',
      }}>
        <div>
          <div style={{ fontFamily: 'var(--font-display)', fontSize: 19, fontWeight: 700, letterSpacing: '-0.02em', color: 'var(--c-smoke)' }}>
            Merijn Verhaak
          </div>
          <div style={{ fontFamily: 'var(--font-body)', fontSize: 11, fontWeight: 600, letterSpacing: '0.14em', color: 'var(--c-muted)', textTransform: 'uppercase', marginTop: 6 }}>
            FBP Portfolio · 2023–2026
          </div>
        </div>
        <div style={{ fontFamily: 'var(--font-body)', fontSize: 13, color: 'var(--c-whisper)', lineHeight: 1.7, maxWidth: '52ch' }}>
          Designed and built by Merijn Verhaak — portfolio for the Industrial Design bachelor at TU/e.
        </div>
        <div className="footer-right" style={{ fontFamily: 'var(--font-body)', fontSize: 11, fontWeight: 600, color: 'var(--c-muted)', letterSpacing: '0.14em', textTransform: 'uppercase', textAlign: 'right', lineHeight: 1.8 }}>
          Industrial Design<br/>
          TU/e · 2023–2026<br/>
          Eindhoven, NL
        </div>
      </div>

      {/* Word-count read-out — print-style budget strip.
          Counts the ENTIRE assignment body (all four tapes) excluding any
          element tagged [data-noword] (e.g. references / bibliography). */}
      <div className="wrap-wide" style={{ marginTop: 40 }}>
        <div style={{
          borderTop: '2px solid var(--c-smoke)',
          paddingTop: 14,
          display: 'grid',
          gridTemplateColumns: '1fr',
          gap: 12,
        }}>
          {/* Row 1 — total / limit / read time */}
          <div style={{
            display: 'flex',
            alignItems: 'baseline',
            justifyContent: 'space-between',
            gap: 24,
            flexWrap: 'wrap',
          }}>
            <div style={{ display: 'flex', alignItems: 'baseline', gap: 14 }}>
              <span style={{ fontFamily: 'var(--font-body)', fontSize: 11, fontWeight: 700, letterSpacing: '0.16em', color: 'var(--c-muted)', textTransform: 'uppercase' }}>
                {strict ? 'Prose · tagged' : 'Prose · auto'}
              </span>
              <span style={{
                fontFamily: 'var(--font-display)',
                fontSize: 22,
                fontWeight: 700,
                color: over ? 'var(--ink-coral)' : 'var(--c-smoke)',
                letterSpacing: '-0.01em',
                fontVariantNumeric: 'tabular-nums',
              }}>
                {stats.words.toLocaleString()}
              </span>
              <span style={{ fontFamily: 'var(--font-body)', fontSize: 12, letterSpacing: '0.08em', color: 'var(--c-whisper)', textTransform: 'uppercase' }}>
                / {WORD_LIMIT.toLocaleString()} words · {strict ? 'body prose only · excl. headings & chrome' : 'p, h1–h4, li · sans references'}
              </span>
            </div>
            <div style={{ fontFamily: 'var(--font-body)', fontSize: 12, letterSpacing: '0.08em', color: 'var(--c-whisper)', textTransform: 'uppercase' }}>
              ≈ {stats.minutes} min read · 220 wpm
            </div>
          </div>

          {/* Row 2 — budget bar */}
          <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
            <div style={{
              flex: 1,
              height: 5,
              background: '#E9EDF1',
              overflow: 'hidden',
              position: 'relative',
            }}>
              <div style={{
                width: `${pct}%`,
                height: '100%',
                background: over ? 'var(--p-coral)' : 'var(--p-sky)',
                transition: 'width 600ms cubic-bezier(.2,.7,.2,1)',
              }} />
              {/* tick marks at 25/50/75 */}
              {[25, 50, 75].map(p => (
                <div key={p} style={{
                  position: 'absolute', top: 0, bottom: 0, left: `${p}%`,
                  width: 1, background: '#FFFFFF',
                }} />
              ))}
            </div>
            <div style={{
              fontFamily: 'var(--font-body)',
              fontSize: 12,
              fontWeight: 600,
              letterSpacing: '0.08em',
              color: over ? 'var(--ink-coral)' : 'var(--c-whisper)',
              textTransform: 'uppercase',
              fontVariantNumeric: 'tabular-nums',
              minWidth: 90,
              textAlign: 'right',
            }}>
              {over
                ? `+${(stats.words - WORD_LIMIT).toLocaleString()} over`
                : `${(WORD_LIMIT - stats.words).toLocaleString()} left`}
            </div>
          </div>
        </div>
      </div>

      {/* Hidden measurement container — renders every tape so the counter
          reflects the whole assignment regardless of which side is loaded.
          aria-hidden + offscreen positioning keeps it out of the user view
          and out of the accessibility tree. */}
      <div
        ref={measureRef}
        aria-hidden="true"
        style={{
          position: 'fixed',
          left: -99999,
          top: -99999,
          width: 1200,
          pointerEvents: 'none',
          opacity: 0,
        }}
      >
        <LandingSection sections={SECTIONS} onSelect={() => {}} caseOpen={true} editorialStyle="serif" />
        <PastSection editorialStyle="serif" />
        <PresentSection editorialStyle="serif" />
        <FutureSection editorialStyle="serif" />
      </div>
    </footer>
  );
}

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