// gallery.jsx
// Portfolio filmstrip gallery for DB0001 — Dysbalans Visual Identity.
// Desktop: horizontal scroll-snap, 75% image + 25% info panel per frame.
// Mobile: vertical scroll, image stacked above info.
// Thumbnail strip at bottom syncs with active frame.
// Click image (or [↗]) → fullscreen lightbox with ← → keyboard/arrow nav.
// Updated 04.06.2026: 25 frames, integer numbering, full PL/EN descriptions,
//                     VK idle overlay after 40s inactivity.

(function () {
  const { useState, useEffect, useRef, useCallback } = React;

  const DB = 'assets/portfolio/db/';
  const VK_IDLE_TIMEOUT = 40000;

  // ── Frame data ─────────────────────────────────────────────────────────────
  const FRAMES = [
    {
      id: '01', file: '01.jpg',
      title:   'REJESTR TOŻSAMOŚCI WIZUALNYCH', sub:   'Księga Projektu · DB0001/2026',
      titleEn: 'REGISTER OF VISUAL IDENTITIES', subEn: 'Project Book · DB0001/2026',
      desc:   'Ta sama okładka dla każdego klienta. Indywidualizacja oprawy i rozpoznawalność systemu. Moja otoczka, twoja treść, jeden archiwalny porządek.',
      descEn: 'Same cover, every client. Not personalised packaging, a recognisable system. My frame, your content, one archival order.',
    },
    {
      id: '02', file: '02.jpg',
      title:   'WIZJA ŚWIATA', sub:   'Concept art · narracja wizualna',
      titleEn: 'WORLD VISION',  subEn: 'Concept art · visual narrative',
      desc:   'Warszawa, po wszystkim. Obraz który ustawia tonację całego systemu: ciężar, historia, coś co przetrwało.',
      descEn: 'Warsaw, after everything. The image that sets the tone for the entire system: weight, history, something that survived.',
    },
    {
      id: '03', file: 'type-1.jpg',
      title:   'SYN NOVA × NIPPO', sub:   'Typografia systemu · dwa kroje',
      titleEn: 'SYN NOVA × NIPPO', subEn: 'System typefaces · two cuts',
      desc:   'SYN Nova Bold uderza. Nippo zostaje z myślą. Dwa fonty, jedna hierarchia, zero przypadku.',
      descEn: 'SYN Nova Bold hits. Nippo stays with the thought. Two typefaces, one hierarchy, zero coincidence.',
    },
    {
      id: '04', file: 'type-2.jpg',
      title:   'TABELA WAGOWA', sub:   'SYN Nova · Nippo · warianty',
      titleEn: 'WEIGHT TABLE',   subEn: 'SYN Nova · Nippo · variants',
      desc:   'Pełna tabela wagowa obu krojów. SYN Nova jako font zmienny, sześć stopni nacisku. Nippo, od extralight do semibold. Polski ogon w każdym wariancie.',
      descEn: 'Full weight table of both typefaces. SYN Nova as a variable font, six degrees of weight. Nippo, from extralight to semibold. Polish diacritics in every variant.',
    },
    {
      id: '05', file: 'palette.jpg',
      title:   'PALETA BARW',    sub:   '#0B0B0B · #FFFFFF · #A90000 · #F2AF0D',
      titleEn: 'COLOUR PALETTE', subEn: '#0B0B0B · #FFFFFF · #A90000 · #F2AF0D',
      desc:   'Cztery kolory. Żaden nie jest przypadkowy. #0B0B0B pochłania. #FFFFFF nie negocjuje. #A90000, krew i sygnał alarmowy. #F2AF0D, energia i ostrzeżenie w jednym, jedyny ciepły punkt w zimnym systemie.',
      descEn: 'Four colours. None of them accidental. #0B0B0B absorbs. #FFFFFF does not negotiate. #A90000, blood and alarm signal. #F2AF0D, energy and warning in one, the only warm point in a cold system.',
    },
    {
      id: '06', file: '05.jpg',
      title:   'DEFINICJA NAZWY', sub:   'Logotyp · etymologia',
      titleEn: 'NAME DEFINITION',  subEn: 'Logotype · etymology',
      desc:   'Nazwa nie jest przypadkowa. Łacińskie dis i bilanx, rozdzielenie i waga z dwoma szalami. Stan zaburzenia równowagi. Nie-chaos. Przesunięcie w strukturze. Entropia w układzie.',
      descEn: 'The name is not accidental. Latin dis and bilanx, separation and a scale with two pans. A state of imbalance. Not chaos. A shift in structure.',
    },
    {
      id: '07', file: '06.jpg',
      title:   'KONTEKST I ŹRÓDŁO', sub:   '(im)balanced · manifest',
      titleEn: 'CONTEXT AND SOURCE', subEn: '(im)balanced · manifesto',
      desc:   'Od (im)balanced do ÐysBalans. Każda iteracja była decyzją, nie przypadkiem.',
      descEn: 'From (im)balanced to ÐysBalans. Every iteration was a decision, not an accident.',
    },
    {
      id: '08', file: '08.jpg',
      title:   'ENTROPIA — 間', sub:   'Ma · wariant żółty',
      titleEn: 'ENTROPY — 間',   subEn: 'Ma · yellow variant',
      desc:   '間. Japońska pusta przestrzeń jako aktywny element, nie tło. Punkt wyjścia sygnetu. Energia w strukturze.',
      descEn: '間. Japanese empty space as an active element, not a background. The starting point of the signet. Energy in structure.',
    },
    {
      id: '09', file: '09.jpg',
      title:   'ENTROPIA — 間', sub:   'Ma · wariant czerwony',
      titleEn: 'ENTROPY — 間',   subEn: 'Ma · red variant',
      desc:   '間 przestało być inspiracją. Stało się znakiem. Forma wyłoniła się z siatki, nie z rysunku wolną ręką.',
      descEn: '間 stopped being an inspiration. It became a sign. The form emerged from the grid, not from a free hand.',
    },
    {
      id: '10', file: '10.jpg',
      title:   'SYGNET // ÐB', sub:   'Close-up · identyfikacja podstawowa',
      titleEn: 'SIGNET // ÐB',  subEn: 'Close-up · primary identification',
      desc:   'Wersja ostateczna. Glitch jako element celowy. Niedoskonałość wbudowana w system. Ani błąd, ani ozdoba.',
      descEn: 'Final version. Glitch as an intentional element. Imperfection built into the system, not a flaw, not decoration.',
    },
    {
      id: '11', file: '11.jpg',
      title:   'ORZEŁ KORONNY', sub:   'Element systemu · znak heraldyczny',
      titleEn: 'CROWNED EAGLE',  subEn: 'System element · heraldic mark',
      desc:   'Pierwszy znak dystynktywny. Orzeł jako figura historyczna przepisana w geometrię systemu ÐysBalans.',
      descEn: 'First distinguishing mark. The eagle as a historical figure rewritten into the geometry of the ÐysBalans system.',
    },
    {
      id: '12', file: '12.jpg',
      title:   'PIORUN', sub:   'Element systemu · insygnia narracyjne',
      titleEn: 'LIGHTNING', subEn: 'System element · narrative insignia',
      desc:   'Drugi znak dystynktywny. Długi miecz jako forma. Ostrze, nie ornament. Siła bez zbędnego gestu.',
      descEn: 'Second distinguishing mark. A longsword as form. A blade, not an ornament. Strength without unnecessary gesture.',
    },
    {
      id: '13', file: 'heraldry-b.jpg',
      title:   'ORZEŁ I PIORUN',     sub:   'Para znaków · heraldyka CRTW',
      titleEn: 'EAGLE AND LIGHTNING', subEn: 'Mark pair · CRTW heraldry',
      desc:   'Orzeł i Piorun jako para. Razem tworzą heraldykę CRTW. System identyfikacji i jego wizualne uzupełnienia.',
      descEn: 'Eagle and Lightning as a pair. Together they form the heraldry of CRTW. Not decoration. An identification system.',
    },
    {
      id: '14', file: '13.jpg',
      title:   'MOCKUP // CRTW VEHICLE', sub:   'Identyfikacja ruchoma · 2226',
      titleEn: 'MOCKUP // CRTW VEHICLE', subEn: 'Mobile identification · 2226',
      desc:   'Fanart. Pojazd terenowy z lore 2226, świat w którym CRTW operuje. Narracja wykracza poza brandbook.',
      descEn: 'Fanart. An off-road vehicle from the 2226 lore, the world in which CRTW operates. The narrative goes beyond the brandbook.',
    },
    {
      id: '15', file: '14.jpg',
      title:   'MOCKUP // CRTW HQ', sub:   'Architektura narracyjna · 2226',
      titleEn: 'MOCKUP // CRTW HQ', subEn: 'Narrative architecture · 2226',
      desc:   'Siedziba Centralnego Rejestru Tożsamości Wizualnych. Istnieje tylko w fikcji i na każdej stronie tego projektu.',
      descEn: 'Headquarters of the Central Register of Visual Identities. It exists only in fiction and on every page of this project.',
    },
    {
      id: '16', file: '15.jpg',
      title:   'MOCKUP // WIZYTÓWKI',     sub:   'Identyfikacja osobista',
      titleEn: 'MOCKUP // BUSINESS CARDS', subEn: 'Personal identification',
      desc:   'Trzy wizytówki, trzy konteksty, jeden system. Każda z innego powodu, każda z tego samego miejsca.',
      descEn: 'Three business cards, three contexts, one system. Each for a different reason, each from the same place.',
    },
    {
      id: '17', file: 'book-b.jpg',
      title:   'MOCKUP // KSIĘGA PROJEKTU', sub:   'B5 landscape · wersja B',
      titleEn: 'MOCKUP // PROJECT BOOK',    subEn: 'B5 landscape · variant B',
      desc:   'Drugi wariant tej samej oprawy. Ciemna strona formatu B5.',
      descEn: 'Second variant of the same binding. The dark side of the B5 format.',
    },
    {
      id: '18', file: '16.jpg',
      title:   'MOCKUP // KSIĘGA PROJEKTU', sub:   'B5 landscape · kraft',
      titleEn: 'MOCKUP // PROJECT BOOK',    subEn: 'B5 landscape · kraft',
      desc:   'B5 po dłuższej krawędzi. Mockup systemowy okładek, nie tylko ÐysBalans. Tak wygląda każda sprawa z archiwum CRTW.',
      descEn: 'B5 on the long edge. A systemic cover mockup, not only ÐysBalans. This is what every case from the CRTW archive looks like.',
    },
    {
      id: '19', file: '17.jpg',
      title:   'MOCKUP // TECZKA PROJEKTU', sub:   'Dokumentacja · dwa warianty',
      titleEn: 'MOCKUP // PROJECT FOLDER',  subEn: 'Documentation · two variants',
      desc:   'Dwie teczki projektu. Dokumentacja jako obiekt fizyczny: materiał, faktura, nadruk.',
      descEn: 'Two project folders. Documentation as a physical object: material, texture, print.',
    },
    {
      id: '20', file: '18.jpg',
      title:   'MOCKUP // KOPERTA STRACHU', sub:   'Korespondencja · format C4',
      titleEn: 'MOCKUP // KOPERTA STRACHU', subEn: 'Correspondence · C4 format',
      desc:   'Format C4 złożone. Korespondencja jako element identyfikacji. Lepiej widoczna, rozpoznawalna na biurku część systemu.',
      descEn: 'C4 format, folded. Correspondence as an element of identity. More visible, more recognisable on a desk.',
    },
    {
      id: '21', file: 'envelope-b.jpg',
      title:   'MOCKUP // KOPERTA', sub:   'Format mniejszy · korespondencja',
      titleEn: 'MOCKUP // ENVELOPE', subEn: 'Smaller format · correspondence',
      desc:   'Mniejszy format korespondencji. Ten sam system, mniejsza skala.',
      descEn: 'Smaller correspondence format. Same system, smaller scale.',
    },
    {
      id: '22', file: '20.jpg',
      title:   'MOCKUP // ŻÓŁTE PUDEŁKO A4', sub:   'Gold edition · cztery nośniki',
      titleEn: 'MOCKUP // YELLOW BOX A4',    subEn: 'Gold edition · four carriers',
      desc:   'Żółte pudełko A4. Jeden z czterech fizycznych nośników systemu. #F2AF0D na całej powierzchni, nie jako akcent.',
      descEn: 'Yellow A4 box. One of four physical carriers of the system. #F2AF0D across the entire surface, not as an accent.',
    },
    {
      id: '23', file: '21.jpg',
      title:   'POLSKA, ROK 2226', sub:   'Narracja · otoczka marki',
      titleEn: 'POLAND, YEAR 2226', subEn: 'Narrative · brand frame',
      desc:   'Polska, rok 2226. Dowód na to, że otoczka kształtuje markę tak samo jak treść, którą niesie. Nie popisuję się umiejętnością pisania prozy. Zwracam uwagę jak ważna jest historia i otoczka dla marki.',
      descEn: 'Poland, 2226. Not a display of prose. Proof that the frame shapes a brand as much as the content it carries.',
    },
    {
      id: '24', file: '22.jpg',
      title:   'FANART // SCENERIA', sub:   'ÐysBalans · świat równoległy',
      titleEn: 'FANART // SCENERY',   subEn: 'ÐysBalans · parallel world',
      desc:   'Fanart. Sceneria w której ÐysBalans istnieje. Świat równoległy. Spójny bez briefu.',
      descEn: 'Fanart. The setting in which ÐysBalans exists. A parallel world. Coherent without a brief.',
    },
    {
      id: '25', file: '23.jpg',
      title:   'SYGNET · LOGOTYP · MMXXVI', sub:   'Studio ÐysBalans · zamknięcie',
      titleEn: 'SIGNET · LOGOTYPE · MMXXVI', subEn: 'Studio ÐysBalans · closing',
      desc:   'Sygnet. Logotyp. Rok powstania.',
      descEn: 'Signet. Logotype. MMXXVI. A closing without a moral.',
    },
  ];

  // ── Lightbox ────────────────────────────────────────────────────────────────
  function GalleryLightbox({ idx, onClose, onNav, en }) {
    const frame = FRAMES[idx];

    useEffect(() => {
      const prev = document.body.style.overflow;
      document.body.style.overflow = 'hidden';
      return () => { document.body.style.overflow = prev; };
    }, []);

    useEffect(() => {
      function onKey(e) {
        if (e.key === 'Escape')          { e.preventDefault(); onClose(); }
        else if (e.key === 'ArrowRight') onNav(idx + 1);
        else if (e.key === 'ArrowLeft')  onNav(idx - 1);
      }
      window.addEventListener('keydown', onKey);
      return () => window.removeEventListener('keydown', onKey);
    }, [idx, onClose, onNav]);

    const t = en && frame.titleEn ? frame.titleEn : frame.title;
    const s = en && frame.subEn   ? frame.subEn   : frame.sub;

    return (
      <div className="glb">
        <div className="glb__bd" onClick={onClose}></div>
        <button
          className="glb__arrow glb__arrow--prev"
          onClick={() => onNav(idx - 1)}
          disabled={idx <= 0}
          aria-label="poprzedni"
        >‹</button>

        <div className="glb__stage">
          {frame.ph ? (
            <div className="glb__ph">
              <span className="glb__ph-label">// {t}</span>
            </div>
          ) : (
            <img className="glb__img" src={DB + frame.file} alt={t} />
          )}
          <div className="glb__cap">
            <span className="glb__cap-id">{frame.id} / {FRAMES.length}</span>
            <span className="glb__cap-title">{t}</span>
            {s && <span className="glb__cap-sub">— {s}</span>}
          </div>
        </div>

        <button
          className="glb__arrow glb__arrow--next"
          onClick={() => onNav(idx + 1)}
          disabled={idx >= FRAMES.length - 1}
          aria-label="następny"
        >›</button>
        <button className="glb__close" onClick={onClose}>
          {en ? '[ESC] CLOSE' : '[ESC] ZAMKNIJ'}
        </button>
      </div>
    );
  }

  // ── Gallery view ────────────────────────────────────────────────────────────
  function GalleryView({ onClose }) {
    const { lang } = useLang();
    const en = lang === 'en';
    const [activeIdx, setActiveIdx] = useState(0);
    const [lbIdx, setLbIdx]         = useState(null);
    const [vkIdle, setVkIdle]       = useState(false);
    const filmRef    = useRef(null);
    const thumbsRef  = useRef(null);
    const lbIdxRef   = useRef(null);
    const vkIdleRef  = useRef(false);
    const vkWakeRef  = useRef(null);

    // Keep refs in sync
    useEffect(() => { lbIdxRef.current = lbIdx; }, [lbIdx]);
    useEffect(() => { vkIdleRef.current = vkIdle; }, [vkIdle]);

    // VK idle timer — gallery-scoped, resets on any activity
    useEffect(() => {
      let timer;
      function wake() {
        if (vkIdleRef.current) return;   // already showing — don't reset
        clearTimeout(timer);
        timer = setTimeout(() => {
          if (lbIdxRef.current !== null) { wake(); return; } // lb open, defer
          setVkIdle(true);
        }, VK_IDLE_TIMEOUT);
      }
      vkWakeRef.current = wake; // expose so handleVkClose can restart
      const evts = ['mousemove', 'mousedown', 'keydown', 'touchstart', 'wheel'];
      evts.forEach((ev) => window.addEventListener(ev, wake, { passive: true }));
      wake();
      return () => {
        clearTimeout(timer);
        evts.forEach((ev) => window.removeEventListener(ev, wake));
        vkWakeRef.current = null;
      };
    }, []);

    function handleVkClose() {
      setVkIdle(false);
      // Give a brief pause before restarting the timer
      setTimeout(() => { vkWakeRef.current && vkWakeRef.current(); }, 200);
    }

    // Detect which frame is visible
    useEffect(() => {
      const film = filmRef.current;
      if (!film) return;
      const nodes = film.querySelectorAll('[data-fidx]');
      const obs = new IntersectionObserver(
        (entries) => {
          entries.forEach((e) => {
            if (e.isIntersecting && e.intersectionRatio >= 0.5) {
              setActiveIdx(parseInt(e.target.dataset.fidx, 10));
            }
          });
        },
        { root: film, threshold: 0.5 }
      );
      nodes.forEach((n) => obs.observe(n));
      return () => obs.disconnect();
    }, []);

    // Keep active thumb in view
    useEffect(() => {
      const thumbs = thumbsRef.current;
      if (!thumbs) return;
      const th = thumbs.querySelector('[data-tidx="' + activeIdx + '"]');
      if (!th) return;
      thumbs.scrollLeft = th.offsetLeft - thumbs.offsetWidth / 2 + th.offsetWidth / 2;
    }, [activeIdx]);

    // Scroll filmstrip to frame N
    const goTo = useCallback((idx) => {
      const film = filmRef.current;
      if (!film) return;
      const clamped = Math.max(0, Math.min(FRAMES.length - 1, idx));
      const isVertical = film.scrollHeight > film.clientHeight &&
                         film.scrollWidth  <= film.clientWidth + 4;
      if (isVertical) {
        const node = film.querySelectorAll('[data-fidx]')[clamped];
        if (node) film.scrollTo({ top: node.offsetTop, behavior: 'smooth' });
      } else {
        film.scrollTo({ left: clamped * film.offsetWidth, behavior: 'smooth' });
      }
    }, []);

    // Keyboard navigation — guarded when lightbox or VK overlay is open
    useEffect(() => {
      function onKey(e) {
        if (lbIdx !== null) return;
        if (vkIdle)         return;
        if (e.key === 'Escape')          { e.preventDefault(); onClose(); }
        else if (e.key === 'ArrowRight') goTo(activeIdx + 1);
        else if (e.key === 'ArrowLeft')  goTo(activeIdx - 1);
      }
      window.addEventListener('keydown', onKey);
      return () => window.removeEventListener('keydown', onKey);
    }, [activeIdx, lbIdx, vkIdle, goTo, onClose]);

    const lbNav = useCallback((idx) => {
      setLbIdx(Math.max(0, Math.min(FRAMES.length - 1, idx)));
    }, []);

    return (
      <div className="view">
        <div className="gallery">

          {/* ── bar ── */}
          <div className="gallery__bar">
            <div className="gallery__bar-l">
              <span className="gallery__gid">DB0001 / 2026</span>
              <span className="gallery__gtitle">DYSBALANS — VISUAL IDENTITY</span>
            </div>
            <div className="gallery__bar-r">
              <span className="gallery__gctr">
                {String(activeIdx + 1).padStart(2, '0')}/{String(FRAMES.length).padStart(2, '0')}
              </span>
              <button className="gallery__gclose" onClick={onClose}>
                {en ? '[ESC] BACK' : '[ESC] WRÓĆ'}
              </button>
            </div>
          </div>

          {/* ── filmstrip ── */}
          <div className="gallery__film" ref={filmRef}>
            {FRAMES.map((fr, i) => {
              const t = en && fr.titleEn ? fr.titleEn : fr.title;
              const s = en && fr.subEn   ? fr.subEn   : fr.sub;
              const d = en && fr.descEn  ? fr.descEn  : fr.desc;

              return (
                <div className="gallery__frame" key={fr.id} data-fidx={i}>
                  {/* image cell */}
                  <div
                    className={`gallery__imgcell${fr.ph ? ' gallery__imgcell--ph' : ''}`}
                    onClick={() => !fr.ph && setLbIdx(i)}
                  >
                    {fr.ph ? (
                      <div className="gallery__plh">
                        <span className="gallery__plh-label">// W PRZYGOTOWANIU</span>
                        <span className="gallery__plh-id">{fr.id}</span>
                      </div>
                    ) : (
                      <img
                        className="gallery__fimg"
                        src={DB + fr.file}
                        alt={t}
                        loading={i < 3 ? 'eager' : 'lazy'}
                      />
                    )}
                  </div>

                  {/* info panel */}
                  <div className="gallery__panel">
                    <span className="gallery__pnum">{fr.id}</span>
                    <span className="gallery__ptitle">{t}</span>
                    {s && <span className="gallery__psub">{s}</span>}
                    {d && <p className="gallery__pdesc">{d}</p>}
                    <div className="gallery__pdiv"></div>
                    <span className="gallery__pmeta">
                      {en ? 'DB0001 · BRANDING · NARRATIVE' : 'DB0001 · BRANDING · NARRACJA'}
                    </span>
                    {!fr.ph && (
                      <button className="gallery__pzoom" onClick={() => setLbIdx(i)}>
                        {en ? '[↗] enlarge' : '[↗] powiększ'}
                      </button>
                    )}
                  </div>
                </div>
              );
            })}
          </div>

          {/* ── thumbnail strip ── */}
          <div className="gallery__thumbs" ref={thumbsRef}>
            {FRAMES.map((fr, i) => (
              <button
                key={fr.id}
                className={`gallery__thumb${i === activeIdx ? ' is-active' : ''}`}
                data-tidx={i}
                onClick={() => goTo(i)}
                aria-label={en && fr.titleEn ? fr.titleEn : fr.title}
              >
                {fr.ph ? (
                  <span className="gallery__thph">··</span>
                ) : (
                  <img src={DB + fr.file} alt="" loading="lazy" />
                )}
                <span className="gallery__thid">{fr.id}</span>
              </button>
            ))}
          </div>

        </div>

        {/* Lightbox */}
        {lbIdx !== null && (
          <GalleryLightbox
            idx={lbIdx}
            onClose={() => setLbIdx(null)}
            onNav={lbNav}
            en={en}
          />
        )}

        {/* Voight-Kampff idle overlay */}
        {vkIdle && window.VKIdleOverlay &&
          React.createElement(window.VKIdleOverlay, { onClose: handleVkClose })
        }
      </div>
    );
  }

  Object.assign(window, { GalleryView, GALLERY_FRAMES: FRAMES });
})();
