// Gallery — Kalatua-style masonry photo grid (3 columns, break-inside: avoid).
// Matches #gallery / .gallery-header / .gallery-label / .gallery-heading /
// .gallery-subline / .gallery-masonry / .gallery-item / .gallery-img-overlay /
// .gallery-cta / .gallery-cta-btn classes from index.html.
//
// Animations:
//   - Header: label → heading → subline, fade + y40→0, stagger 100ms, power3.out,
//     ScrollTrigger @ top 85%, once.
//   - Masonry images: each .gallery-img-overlay wipes to xPercent:101% with
//     stagger (delay = i*100ms), duration 850ms, power2.inOut, once.
// Uses data.galleryFeature when provided for header copy + cta; falls back to
// data.gallery for the image list.

function Gallery({ data }) {
  const sectionRef = React.useRef(null);
  const labelRef   = React.useRef(null);
  const headingRef = React.useRef(null);
  const sublineRef = React.useRef(null);
  const masonryRef = React.useRef(null);
  const overlayRefs = React.useRef([]);
  const firedRef = React.useRef(false);

  React.useEffect(() => {
    if (!data) return;
    if (firedRef.current) return;
    if (typeof gsap === 'undefined' || typeof ScrollTrigger === 'undefined') return;
    firedRef.current = true;

    // --- Header ---
    gsap.set(labelRef.current,   { y: 40 });
    gsap.set(headingRef.current, { y: 40 });
    gsap.set(sublineRef.current, { y: 20 });

    gsap.timeline({
      scrollTrigger: {
        trigger: labelRef.current,
        start: 'top 85%',
        once: true,
      },
    })
      .to(labelRef.current,   { opacity: 1, y: 0, duration: 0.65, ease: 'power3.out' }, 0)
      .to(headingRef.current, { opacity: 1, y: 0, duration: 0.65, ease: 'power3.out' }, 0.10)
      .to(sublineRef.current, { opacity: 1, y: 0, duration: 0.55, ease: 'power3.out' }, 0.20);

    // --- Image wipe reveals ---
    overlayRefs.current.forEach((ov, i) => {
      if (!ov) return;
      gsap.to(ov, {
        x: '101%',
        duration: 0.85,
        ease: 'power2.inOut',
        delay: i * 0.10,
        scrollTrigger: {
          trigger: masonryRef.current,
          start: 'top 90%',
          once: true,
        },
      });
    });

    const refresh = setTimeout(() => ScrollTrigger.refresh(), 300);
    return () => clearTimeout(refresh);
  }, [data]);

  // Compute grid-row span from each image's aspect ratio AND greedy-place
  // every item into the shortest column so the column bottoms line up
  // (true balanced masonry — CSS column-fill / grid auto-flow can't do this).
  //
  // Stability: the column ASSIGNMENT (which column each item lives in) is
  // cached per column-count. We only re-run the optimizer when the column
  // count actually changes (e.g. desktop ↔ tablet ↔ mobile). Spans still
  // recompute on every layout() since they depend on column width — but
  // since assignment is fixed, items don't reshuffle as photos load in.
  const assignmentCacheRef = React.useRef({ assignment: null, cols: 0 });
  React.useEffect(() => {
    if (!data) return;
    const masonry = masonryRef.current;
    if (!masonry) return;
    const ROW = 8;
    const GAP = 12;
    const items = Array.from(masonry.querySelectorAll('.gallery-item'));
    const imgs  = items.map(it => it.querySelector('img'));

    const colCount = () => {
      const styles = window.getComputedStyle(masonry);
      return styles.gridTemplateColumns.split(' ').filter(Boolean).length || 3;
    };

    const layout = () => {
      const cols = colCount();
      const cs   = getComputedStyle(masonry);
      const gridW = masonry.clientWidth
        - parseFloat(cs.paddingLeft || 0)
        - parseFloat(cs.paddingRight || 0);
      const colW = (gridW - GAP * (cols - 1)) / cols;

      // Compute every item's span + a category tag so the optimizer can avoid
      // clustering visually-similar photos together. Prefer explicit
      // data-category (set from content.json), fall back to the src folder.
      const catOf = (it, src) => {
        const explicit = it && it.dataset && it.dataset.category;
        if (explicit) return explicit;
        const m = (src || '').match(/\/images\/([^/]+)\//);
        return m ? m[1] : 'misc';
      };
      const meta = items.map((it, i) => {
        const img = imgs[i];
        const ratio = (img && img.naturalWidth)
          ? (img.naturalHeight / img.naturalWidth)
          : 4 / 3;
        const h    = colW * ratio;
        const span = Math.max(1, Math.ceil((h + GAP) / (ROW + GAP)));
        const cat  = catOf(it, img && img.getAttribute('src'));
        return { it, span, i, cat };
      });

      // Find the most balanced 3-bin partition (min spread). LPT is a great
      // starting point; then we hill-climb by swapping items between bins
      // to shrink the gap between tallest and shortest column. Result: very
      // tight bottom alignment so the section feels complete.
      const lptAssign = (() => {
        const order = meta.map((m, idx) => ({ idx, span: m.span }))
                          .sort((a, b) => b.span - a.span);
        const sums = new Array(cols).fill(0);
        const assign = new Array(meta.length);
        for (const { idx, span } of order) {
          let c = 0;
          for (let k = 1; k < cols; k++) if (sums[k] < sums[c]) c = k;
          assign[idx] = c;
          sums[c] += span;
        }
        return assign;
      })();

      const sumsOf = (assign) => {
        const s = new Array(cols).fill(0);
        for (let i = 0; i < meta.length; i++) s[assign[i]] += meta[i].span;
        return s;
      };
      const spreadOf = (s) => Math.max(...s) - Math.min(...s);

      // Per-column row layout (top-of-each-item in row units, in DOM order).
      // Used to detect category clustering both within and across columns.
      const rowsOf = (assign) => {
        const heads = new Array(cols).fill(0);
        const placed = new Array(meta.length); // { col, top, bot }
        meta.forEach(({ span }, i) => {
          const c = assign[i];
          placed[i] = { col: c, top: heads[c], bot: heads[c] + span };
          heads[c] += span;
        });
        return placed;
      };

      // Category-clustering cost: count pairs (i, j) of the SAME category that
      // either (a) sit in the same column without an item of a different
      // category between them, or (b) sit in adjacent columns with vertically
      // overlapping row-spans. These are the visual "next to each other" cases.
      const clusterOf = (assign) => {
        const placed = rowsOf(assign);
        let cost = 0;
        for (let a = 0; a < meta.length; a++) {
          for (let b = a + 1; b < meta.length; b++) {
            if (meta[a].cat !== meta[b].cat) continue;
            const A = placed[a], B = placed[b];
            const colDist = Math.abs(A.col - B.col);
            if (colDist > 1) continue;
            const overlap = Math.min(A.bot, B.bot) - Math.max(A.top, B.top);
            if (colDist === 0) {
              // same column — check no foreign-cat item sits between them
              const lo = Math.min(A.bot, B.bot), hi = Math.max(A.top, B.top);
              let blocked = false;
              for (let k = 0; k < meta.length; k++) {
                if (k === a || k === b) continue;
                if (assign[k] !== A.col) continue;
                if (meta[k].cat === meta[a].cat) continue;
                const K = placed[k];
                if (K.top >= lo && K.bot <= hi) { blocked = true; break; }
              }
              if (!blocked) cost += 2; // stacked in same column = strongest cluster
            } else if (overlap > 0) {
              cost += 1; // side-by-side overlap
            }
          }
        }
        return cost;
      };

      // Combined objective: spread dominates (we don't want to wreck the
      // bottom alignment), but cluster cost breaks ties and is allowed to
      // push spread up by a few row-units to scatter same-category photos.
      const ALPHA = 4; // each cluster pair "worth" 4 row-units of extra spread
      const costOf = (assign) => spreadOf(sumsOf(assign)) + ALPHA * clusterOf(assign);

      // Reuse cached assignment if column count hasn't changed — keeps items
      // anchored to their column so the gallery doesn't reshuffle as photos
      // load in or on small resizes that don't cross a breakpoint.
      const cache = assignmentCacheRef.current;
      let best;
      if (cache.assignment && cache.cols === cols && cache.assignment.length === meta.length) {
        best = cache.assignment;
      } else {
        best = lptAssign.slice();
        let bestCost = costOf(best);

        // Try every single move (item i -> col c) and every pairwise swap.
        // For 14 items × 3 cols this is ~600 ops — instant.
        let improved = true;
        while (improved) {
          improved = false;
          for (let i = 0; i < meta.length; i++) {
            for (let c = 0; c < cols; c++) {
              if (best[i] === c) continue;
              const trial = best.slice(); trial[i] = c;
              const sp = costOf(trial);
              if (sp < bestCost) { best = trial; bestCost = sp; improved = true; }
            }
            for (let j = i + 1; j < meta.length; j++) {
              if (best[i] === best[j]) continue;
              const trial = best.slice();
              const t = trial[i]; trial[i] = trial[j]; trial[j] = t;
              const sp = costOf(trial);
              if (sp < bestCost) { best = trial; bestCost = sp; improved = true; }
            }
          }
        }
        assignmentCacheRef.current = { assignment: best, cols };
      }

      // Apply: per column, place items in DOM order so visual flow stays
      // top-to-bottom within each column (less jarring than LPT order).
      const colRows = new Array(cols).fill(0);
      meta.forEach(({ it, span }, i) => {
        const c = best[i];
        it.style.gridColumnStart = c + 1;
        it.style.gridRowStart    = colRows[c] + 1;
        it.style.gridRowEnd      = `span ${span}`;
        colRows[c] += span;
      });
    };

    // Wait for ALL images to have natural dimensions before doing the
    // initial layout — running too early lets some items use the 4:3
    // fallback ratio, which then shifts when the real image loads.
    let pending = 0;
    imgs.forEach(img => {
      if (!img) return;
      if (img.complete && img.naturalWidth) return;
      pending++;
    });
    if (pending === 0) {
      layout();
    } else {
      imgs.forEach(img => {
        if (!img) return;
        if (img.complete && img.naturalWidth) return;
        const done = () => {
          pending--;
          if (pending === 0) layout();
        };
        img.addEventListener('load',  done, { once: true });
        img.addEventListener('error', done, { once: true });
      });
    }

    let raf = 0;
    const onResize = () => { cancelAnimationFrame(raf); raf = requestAnimationFrame(layout); };
    window.addEventListener('resize', onResize);
    return () => window.removeEventListener('resize', onResize);
  }, [data]);

  if (!data) return null;

  // Prefer the curated galleryFeature block for copy + CTA; fall back to site defaults.
  const feature = data.galleryFeature || {};
  const heading = feature.heading   || (data.instagram && data.instagram.handle) || 'Follow the Story';
  const label   = feature.eyebrow   || 'FOLLOW THE STORY';
  const subline = feature.subheading || (data.social && data.social.instagramHandle) || '';
  const sublineHref = (feature.cta && feature.cta.href) || (data.social && data.social.instagram) || '#';
  const cta = feature.cta || { label: 'View on Instagram', href: sublineHref };

  // Image set — prefer gallery array for masonry (more variety + taller mix).
  const photos = (data.gallery && data.gallery.length ? data.gallery : (feature.photos || [])).slice(0, 20);

  return (
    <section id="gallery" ref={sectionRef}>
      <div className="gallery-header">
        <p className="gallery-label" ref={labelRef}>{label}</p>
        <h2 className="gallery-heading" ref={headingRef}>{heading}</h2>
        <a
          href={sublineHref}
          target="_blank"
          rel="noopener noreferrer"
          className="gallery-subline"
          ref={sublineRef}
        >
          {subline}
        </a>
      </div>

      <div className="gallery-masonry" ref={masonryRef}>
        {photos.map((photo, i) => (
          <div key={i} className="gallery-item" data-category={photo.category || undefined}>
            <img
              src={photo.src}
              alt={photo.alt || ''}
              loading={i < 3 ? 'eager' : 'lazy'}
              decoding="async"
              fetchpriority={i < 3 ? 'high' : 'low'}
            />
            <div
              className="gallery-img-overlay"
              ref={(el) => (overlayRefs.current[i] = el)}
            ></div>
          </div>
        ))}
      </div>

      <div className="gallery-cta">
        <a
          href={cta.href}
          target="_blank"
          rel="noopener noreferrer"
          className="gallery-cta-btn"
        >
          <span>{cta.label}</span>
        </a>
      </div>
    </section>
  );
}
