// family-archive.jsx — the Norman–Weil family photo archive.
// Archive aesthetic (EB Garamond + Space Mono, hairline rules) on Heirloom's
// warm cream. Hash routing: #/  ·  #/b/:branch  ·  #/b/:branch/:index
const { useState, useEffect, useRef, useCallback } = React;

const DWELL = 6500; // ms each slide is shown when auto-playing
const FADE = 1200; // ms crossfade

// ---- data: group photos by branch ----------------------------------------
const BRANCHES = window.BRANCHES;
const BYBRANCH = {};
BRANCHES.forEach((b) => {BYBRANCH[b.key] = [];});
window.PHOTOS.forEach((p) => {(BYBRANCH[p.branch] || (BYBRANCH[p.branch] = [])).push(p);});
BYBRANCH['all'] = window.PHOTOS; // the whole family, in manifest order
const branchMeta = (k) => k === 'all' ?
{ key: 'all', name: 'The Family' } :
BRANCHES.find((b) => b.key === k);
const tone = (i) => window.GRAYS[(i % window.GRAYS.length + window.GRAYS.length) % window.GRAYS.length];
const slidePath = (branch, i) => branch === 'all' ? '/all/' + i : '/b/' + branch + '/' + i;
const galleryOf = (branch) => branch === 'all' ? '/' : '/b/' + branch;

// ---- people: group photos by the person named in each ---------------------
// PEOPLE[slug] = [{ p, gi }]  (gi = the photo's index in the full library)
const PEOPLE = {};
window.PHOTOS.forEach((p, gi) => {
  const name = (p.names || '').trim();
  if (!name) return;
  const key = encodeURIComponent(name);
  (PEOPLE[key] || (PEOPLE[key] = [])).push({ p, gi });
});

// path for a slideshow slide, by mode
const pathFor = (mode, key, i) =>
mode === 'all' ? '/all/' + i :
mode === 'branch' ? '/b/' + key + '/' + i :
'/p/' + key + '/' + i;

// Decide what happens when a single photo is clicked:
// if it shows a known person, play just that person's photos; otherwise fall
// back to the general slideshow starting from this photo.
const openPhoto = (p, globalIndex) => {
  const name = (p.names || '').trim();
  const key = encodeURIComponent(name);
  const list = name && PEOPLE[key];
  if (list && list.length) {
    const idx = list.findIndex((x) => x.p === p);
    go('/p/' + key + '/' + (idx < 0 ? 0 : idx));
  } else {
    const gi = globalIndex != null ? globalIndex : window.PHOTOS.indexOf(p);
    go('/all/' + Math.max(0, gi));
  }
};

// real span of years across the archive, e.g. "1987–2024"
const _years = window.PHOTOS.map((p) => parseInt(p.year, 10)).filter((n) => !isNaN(n));
const YEAR_RANGE = _years.length ? Math.min(..._years) + '\u2013' + Math.max(..._years) : '';

// ---- a single photograph (real <img>, or a placeholder tone) --------------
function Frame({ photo, idx, fit, style, className }) {
  if (photo && photo.file) {
    return <img src={photo.file} alt={photo.names || ''} loading="lazy" decoding="async"
    className={'ph-photo ' + (className || '')}
    style={{ display: 'block', width: '100%', height: '100%', objectFit: fit || 'cover', ...style }} />;
  }
  return <div className={'ph-photo ' + (className || '')} style={{ background: tone(idx || 0), ...style }} />;
}

// ---- hash routing ----------------------------------------------------------
function useRoute() {
  const parse = () => {
    const h = location.hash.replace(/^#\/?/, '');
    const seg = h.split('/').filter(Boolean);
    if (seg[0] === 'menu') return { view: 'menu' };
    if (seg[0] === 'all' && seg[1] != null) return { view: 'show', mode: 'all', key: 'all', index: parseInt(seg[1], 10) };
    if (seg[0] === 'p' && seg[1] && seg[2] != null) return { view: 'show', mode: 'person', key: seg[1], index: parseInt(seg[2], 10) };
    if (seg[0] === 'b' && seg[1] && seg[2] != null) return { view: 'show', mode: 'branch', key: seg[1], index: parseInt(seg[2], 10) };
    if (seg[0] === 'b' && seg[1]) return { view: 'branchGrid', branch: seg[1] };
    return { view: 'gallery' };
  };
  const [route, setRoute] = useState(parse);
  useEffect(() => {
    const on = () => {setRoute(parse());window.scrollTo(0, 0);};
    window.addEventListener('hashchange', on);
    return () => window.removeEventListener('hashchange', on);
  }, []);
  return route;
}
const go = (h) => {location.hash = h;};

// shows once per visit (resets on a fresh page load, not on in-session nav)
let seenWelcome = false;

function Welcome({ onDone }) {
  const [closing, setClosing] = useState(false);
  const dismiss = () => {if (closing) return;setClosing(true);setTimeout(onDone, 520);};
  return (
    <div className={'welcome' + (closing ? ' closing' : '')} onClick={dismiss}>
      <div className="welcome-card" onClick={(e) => e.stopPropagation()} style={{ textAlign: "center" }}>
        <div className="w-rule"></div>
        <div className="kicker" style={{ fontFamily: "\"EB Garamond\"", fontSize: "16px" }}></div>
        <h1 className="w-title" style={{ fontWeight: "500", textAlign: "center" }}>Norman – Weil</h1>
        <p className="w-sub">Biannual Family Meeting Portraits</p>
        <p className="w-by" style={{ fontFamily: "\"EB Garamond\"", fontSize: "15px", fontWeight: "600" }}>by Amanda Weil</p>
        <button className="w-enter" onClick={dismiss} style={{ fontFamily: "\"EB Garamond\"", backgroundColor: "rgba(23, 21, 15, 0.72)", color: "rgba(255, 255, 255, 0.99)", fontSize: "15px" }}>VIEW THE GALLERY
</button>
      </div>
    </div>);
}

/* ===========================================================================
   GALLERY — the entire family, all branches together (landing page)
   =========================================================================== */
function Gallery() {
  const total = window.PHOTOS.length;
  const [welcome, setWelcome] = useState(!seenWelcome);
  const closeWelcome = () => {seenWelcome = true;setWelcome(false);};
  return (
    <div className="page-wide" style={{ width: "100%" }}>
      {welcome && <Welcome onDone={closeWelcome} />}
      <div className="bar gallery-bar">
        <div className="gb-side gb-left">
          <span className="wordmark" style={{ fontSize: "26px" }}>Norman&nbsp;<span className="dash" style={{ fontSize: "26px" }}>–</span>&nbsp;Weil</span>
        </div>
        <button className="ss-btn" onClick={() => go('/all/0')} aria-label="Start the full slideshow">
          <span className="ss-play" aria-hidden="true">▶</span>Start slideshow
        </button>
        <div className="gb-side gb-right">
          <button className="menu-btn" onClick={() => go('/menu')} aria-label="Browse by branch" style={{ fontSize: "15px" }}>
            <span className="mb-lines" aria-hidden="true"><i></i><i></i><i></i></span>
            Branches
          </button>
        </div>
      </div>
      <header className="gallery-head">
        <div className="kicker" style={{ fontSize: "15px" }}>THE BIANNUAL NORMAN-WEIL FAMILY MEETING</div>
        <p className="lede">{YEAR_RANGE}</p>
      </header>
      <div className="grid">
        {window.PHOTOS.map((p, i) =>
        <button key={i} className="grid-cell" onClick={() => openPhoto(p, i)} aria-label={p.names}>
            <Frame photo={p} idx={i} />
          </button>
        )}
      </div>
      <footer className="home-foot">A private family archive · kept &amp; shared with love</footer>
    </div>);

}

/* ===========================================================================
   MENU — the five branches (“browse by branch”)
   =========================================================================== */
// A branch cover that quietly cross-fades through that branch's members.
const ROLL_DWELL = 8000,ROLL_FADE = 3400;
function RollingThumb({ photos, idx, delay = 0 }) {
  const pics = (photos || []).filter((p) => p && p.file);
  const [cur, setCur] = useState(0);
  const [prev, setPrev] = useState(null);
  useEffect(() => {
    if (pics.length < 2) return;
    let t;
    const tick = () => {
      setCur((c) => {
        const next = (c + 1) % pics.length;
        setPrev(c);
        // preload the following image so the fade never catches a half-decoded frame
        const im = new Image();im.src = pics[(next + 1) % pics.length].file;
        return next;
      });
      t = setTimeout(tick, ROLL_DWELL);
    };
    // stagger each branch so they don't all flip in unison
    const startT = setTimeout(tick, ROLL_DWELL + delay);
    return () => {clearTimeout(startT);clearTimeout(t);};
  }, [pics.length, delay]);
  // clear the outgoing layer after its fade completes
  useEffect(() => {
    if (prev == null) return;
    const t = setTimeout(() => setPrev(null), ROLL_FADE + 120);
    return () => clearTimeout(t);
  }, [prev]);

  if (!pics.length) return <Frame photo={null} idx={idx} className="branch-thumb" />;
  return (
    <span className="branch-thumb roll-thumb">
      {prev != null && prev !== cur &&
      <img key={'p' + prev} src={pics[prev].file} alt="" className="roll-img" />}
      <img key={'c' + cur} src={pics[cur].file} alt="" className="roll-img roll-in"
      style={{ animationDuration: ROLL_FADE + 'ms' }} />
    </span>);

}

function Menu() {
  const total = window.PHOTOS.length;
  return (
    <div className="page">
      <div className="bar">
        <button className="bar-btn" onClick={() => go('/')} aria-label="Back to full gallery">‹</button>
        <div className="bar-title"><div className="bt-name" style={{ fontSize: 20 }}>Browse by Branch</div></div>
        <span style={{ width: 38, flex: '0 0 auto' }}></span>
      </div>
      <header className="home-head" style={{ height: "auto", padding: "clamp(26px,7vw,48px) clamp(20px,5vw,40px) 26px" }}>
        <h1 className="display" style={{ width: "100%" }}>Norman <span className="dash" style={{ padding: "0px 0px 0px 3px" }}>–</span>&nbsp;Weil Family</h1>
        <p className="lede">{total} photographs · {YEAR_RANGE}</p>
      </header>
      <nav className="branch-list">
        {BRANCHES.map((b, i) => {
          const photos = BYBRANCH[b.key] || [];
          return (
            <button key={b.key} className="branch-row" onClick={() => go('/b/' + b.key)}>
              <RollingThumb photos={photos} idx={i} delay={0} />
              <span className="branch-name">
                <span className="bn-title" style={{ fontSize: "40px" }}>{b.name}</span>
                <span className="bn-sub" style={{ fontSize: "15px" }}>{photos.length} photographs</span>
              </span>
            </button>);

        })}
      </nav>
    </div>);

}

/* ===========================================================================
   BRANCH — gallery grid
   =========================================================================== */
function Branch({ branch }) {
  const meta = branchMeta(branch);
  const photos = BYBRANCH[branch] || [];
  if (!meta) return <div className="page"><p className="lede" style={{ padding: 40 }}>Unknown branch.</p></div>;
  return (
    <div className="page">
      <div className="bar">
        <button className="bar-btn" onClick={() => go('/menu')} aria-label="Back to branches">‹</button>
        <div className="bar-title">
          <div className="bt-name">{meta.name}</div>
          <div className="bt-sub">{photos.length} PHOTOGRAPHS</div>
        </div>
        <button className="bar-btn" onClick={() => go('/b/' + branch + '/0')} aria-label="Play slideshow">▷</button>
      </div>
      <div className="grid">
        {photos.map((p, i) =>
        <button key={i} className="grid-cell" onClick={() => openPhoto(p)} aria-label={p.names}>
            <Frame photo={p} idx={i} />
          </button>
        )}
      </div>
    </div>);

}

/* ===========================================================================
   SLIDESHOW — auto-advance, Ken Burns, captions
   =========================================================================== */
function Slideshow({ mode, pkey, index }) {
  const photos =
  mode === 'all' ? window.PHOTOS :
  mode === 'branch' ? BYBRANCH[pkey] || [] :
  (PEOPLE[pkey] || []).map((x) => x.p);
  const N = photos.length;
  const start = Number.isFinite(index) ? Math.max(0, Math.min(index, N - 1)) : 0;
  const back = mode === 'branch' ? '/b/' + pkey : '/';
  const [cur, setCur] = useState(start);
  const [playing, setPlaying] = useState(true);
  const [layers, setLayers] = useState([{ k: start, i: start }]);
  const stageRef = useRef(null);
  const dirRef = useRef(0);

  // when a person's photos have all been shown, flow on into the general slideshow
  const handoff = useCallback(() => {
    const arr = PEOPLE[pkey] || [];
    const lastGi = arr.length ? arr[Math.min(cur, arr.length - 1)].gi : 0;
    go('/all/' + (lastGi + 1) % window.PHOTOS.length);
  }, [pkey, cur]);

  // sync url + localStorage
  useEffect(() => {
    const want = '#' + pathFor(mode, pkey, cur);
    if (location.hash !== want) history.replaceState(null, '', want);
    try {localStorage.setItem('nw:last:' + mode + ':' + pkey, String(cur));} catch (e) {}
  }, [cur, mode, pkey]);

  const advance = useCallback((delta) => {
    if (mode === 'person' && delta > 0 && cur >= N - 1) {handoff();return;}
    dirRef.current = delta;
    setCur((c) => {
      const next = (c + delta + N) % N;
      setLayers((ls) => [...ls.slice(-1), { k: Date.now(), i: next }]);
      return next;
    });
  }, [N, mode, cur, handoff]);

  // prune old layers after the crossfade completes
  useEffect(() => {
    if (layers.length > 1) {
      const t = setTimeout(() => setLayers((ls) => ls.slice(-1)), FADE + 60);
      return () => clearTimeout(t);
    }
  }, [layers]);

  // auto-advance (a single-photo person still hands off after one dwell)
  useEffect(() => {
    if (!playing) return;
    if (N < 2 && mode !== 'person') return;
    const t = setTimeout(() => advance(1), DWELL);
    return () => clearTimeout(t);
  }, [cur, playing, advance, N, mode]);

  // keyboard
  useEffect(() => {
    const onKey = (e) => {
      if (e.key === 'ArrowRight') {advance(1);} else
      if (e.key === 'ArrowLeft') {advance(-1);} else
      if (e.key === 'Escape') {if (document.fullscreenElement) document.exitFullscreen();else go(back);} else
      if (e.key === ' ') {e.preventDefault();setPlaying((p) => !p);} else
      if (e.key.toLowerCase() === 'f') {toggleFs();}
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [advance, back]);

  const toggleFs = () => {
    const el = stageRef.current;
    if (!el) return;
    if (document.fullscreenElement) document.exitFullscreen();else
    if (el.requestFullscreen) el.requestFullscreen();
  };

  if (N === 0) return <div className="page"><p className="lede" style={{ padding: 40 }}>No photographs.</p></div>;
  const p = photos[cur];
  const locName = mode === 'person' ?
  (p.names || 'Family').toUpperCase() :
  ((mode === 'all' ? branchMeta(p.branch) : branchMeta(pkey)) || { name: 'The Family' }).name.toUpperCase();

  return (
    <div className="stage" ref={stageRef}>
      <div className="stage-bar">
        <button className="bar-btn ghost" onClick={() => go(back)} aria-label="Back">‹</button>
        <span className="stage-loc" style={{ fontSize: "15px" }}>{locName} · {String(cur + 1).padStart(2, '0')} / {N}</span>
        <button className="bar-btn ghost" onClick={toggleFs} aria-label="Fullscreen">⤢</button>
      </div>

      <div className="stage-photo" onClick={() => setPlaying((pl) => !pl)} style={{ padding: "5px 40px" }}>
        <div className="photo-box">
          {layers.map((L) =>
          <div key={L.k} className="kb-layer" style={{ animation: `fadeIn ${FADE}ms ease forwards` }}>
              <div className="kb-inner" style={{ animationDuration: DWELL + FADE + 'ms', transformOrigin: L.i % 2 ? '70% 30%' : '30% 65%' }}>
                <Frame photo={photos[L.i]} idx={L.i} fit="cover" style={{ position: 'absolute', inset: 0 }} />
              </div>
            </div>
          )}
          {playing && N > 1 && <div className="progress" key={'p' + cur}><span style={{ animationDuration: DWELL + 'ms' }} /></div>}
        </div>
        <button className="edge edge-l" onClick={(e) => {e.stopPropagation();advance(-1);}} aria-label="Previous">‹</button>
        <button className="edge edge-r" onClick={(e) => {e.stopPropagation();advance(1);}} aria-label="Next">›</button>
      </div>

      <div className="caption">
        <div className="cap-names">{p.names || 'Norman–Weil Family'}</div>
        {p.year && <div className="cap-date" style={{ fontSize: "15px" }}>{p.year}</div>}
        {p.caption && <div className="cap-text">{p.caption}</div>}
        <div className="controls">
          <button className="ctl" onClick={() => advance(-1)} aria-label="Previous">‹</button>
          <button className="ctl play" onClick={() => setPlaying((pl) => !pl)} aria-label={playing ? 'Pause' : 'Play'}>{playing ? '❙❙' : '▷'}</button>
          <button className="ctl" onClick={() => advance(1)} aria-label="Next">›</button>
        </div>
      </div>
    </div>);

}

/* =========================================================================== */
function App() {
  const route = useRoute();
  if (route.view === 'show') return <Slideshow key={route.mode + ':' + route.key} mode={route.mode} pkey={route.key} index={route.index} />;
  if (route.view === 'branchGrid') return <Branch branch={route.branch} />;
  if (route.view === 'menu') return <Menu />;
  return <Gallery />;
}

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