// Rotating thumbnail carousel — anticlockwise continuous vertical scroll on the right edge.
// Thumbs alternate horizontal offset + shade to echo the stacked-rectangle brand mark.

const { useState, useRef, useEffect } = React;

// Each thumb has a fixed visual rhythm: varied widths, x-offsets, and tones.
// These cycle so the column always looks composed regardless of project count.
const RHYTHM = [
  { w: 0.78, x: 0.10, h: 220, tone: 'var(--grey-1)' },
  { w: 0.92, x: 0.04, h: 260, tone: 'var(--grey-3)' },
  { w: 0.70, x: 0.22, h: 200, tone: 'var(--grey-2)' },
  { w: 0.88, x: 0.06, h: 280, tone: 'var(--grey-5)' },
  { w: 0.74, x: 0.18, h: 230, tone: 'var(--grey-2)' },
  { w: 0.84, x: 0.12, h: 250, tone: 'var(--grey-4)' },
  { w: 0.66, x: 0.28, h: 210, tone: 'var(--grey-1)' },
  { w: 0.94, x: 0.02, h: 270, tone: 'var(--grey-3)' }];


function Thumb({ project, rhythm, onClick, position, hoveredPos, onHoverChange }) {
  const [videoRatio, setVideoRatio] = useState(null);
  const [slideRatio, setSlideRatio] = useState(null);
  const [imageRatio, setImageRatio] = useState(null);
  const [slideIdx, setSlideIdx] = useState(0);
  const [visible, setVisible] = useState(false);
  const ref = useRef(null);

  const hasVideo = Boolean(project.video);
  const hasImage = Boolean(project.image);
  const hasEmbed = Boolean(project.thumbEmbed);
  const hasSlideshow = Boolean(project.thumbSlideshow && project.thumbSlideshow.length);
  const mediaScale = hasVideo ? (project.videoScale ?? 1) : hasImage ? (project.imageScale ?? 1) : hasSlideshow ? (project.slideshowScale ?? 1) : 1;
  const fitsRatio = (hasVideo && videoRatio) || (hasSlideshow && slideRatio) || (hasImage && imageRatio);

  // The track renders three copies of every thumb for the seamless loop, but
  // only ~1 copy is ever on screen — defer mounting real video/iframe/img src
  // until a thumb actually scrolls into view, so idle copies don't all fetch
  // and decode at once on load.
  useEffect(() => {
    if (!ref.current || !(hasVideo || hasImage || hasEmbed || hasSlideshow)) return;
    const io = new IntersectionObserver((entries) => {
      if (entries[0].isIntersecting) { setVisible(true); io.disconnect(); }
    }, { rootMargin: '400px 0px' });
    io.observe(ref.current);
    return () => io.disconnect();
  }, [hasVideo, hasImage, hasEmbed, hasSlideshow]);

  // Rapid flip through the slideshow frames — loops continuously.
  useEffect(() => {
    if (!hasSlideshow || !visible) return;
    const id = setInterval(() => {
      setSlideIdx((i) => (i + 1) % project.thumbSlideshow.length);
    }, 180);
    return () => clearInterval(id);
  }, [hasSlideshow, visible, project.thumbSlideshow]);

  const isHovered = hoveredPos === position;
  const dist = hoveredPos !== null ? position - hoveredPos : null;
  const isNeighbor = dist !== null && Math.abs(dist) === 1;
  const thumbTransform = isHovered
    ? 'scale(1.1)'
    : isNeighbor
      ? `translateY(${Math.sign(dist) * 10}px)`
      : 'none';

  return (
    <div
      ref={ref}
      className="thumb"
      data-project={project.id}
      role="button"
      tabIndex={0}
      aria-label={`${project.no} — ${project.title}`}
      onKeyDown={(e) => {
        if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onClick(project, ref.current); }
      }}
      onFocus={() => onHoverChange(true)}
      onBlur={() => onHoverChange(false)}
      onMouseEnter={() => onHoverChange(true)}
      onMouseLeave={() => onHoverChange(false)}
      onClick={() => onClick(project, ref.current)}
      style={{
        width: `${rhythm.w * mediaScale * 100}%`,
        marginLeft: `${rhythm.x * 100}%`,
        height: (hasEmbed || fitsRatio) ? 'auto' : rhythm.h,
        aspectRatio: hasEmbed ? '16 / 9' : (fitsRatio || undefined),
        marginBottom: (hasVideo || hasImage || hasEmbed || hasSlideshow) ? -24 : 0,
        background: rhythm.tone,
        position: 'relative',
        cursor: 'pointer',
        marginTop: project.id === 'p06' ? -14 : -28,
        boxShadow: isHovered ? '0 18px 50px -20px rgba(0,0,0,0.45)' : 'none',
        transition: 'box-shadow 250ms ease, transform 350ms cubic-bezier(.2,.7,.2,1)',
        transform: thumbTransform,
      }}>
      {!(hasVideo || hasImage || hasEmbed || hasSlideshow)
        ? <div style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}>
            <ThumbPlaceholder no={project.no} title={project.title} />
          </div>
        : !visible
        ? null
        : project.thumbEmbed
        ? <iframe src={project.thumbEmbed} title={project.title} scrolling="no" loading="lazy"
            style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', border: 'none', display: 'block', pointerEvents: 'none' }} />
        : hasVideo
        ? <video
            src={project.video}
            autoPlay muted loop playsInline preload="metadata"
            onLoadedMetadata={(e) => {
              const v = e.currentTarget;
              if (v.videoWidth && v.videoHeight) {
                setVideoRatio(`${v.videoWidth} / ${v.videoHeight}`);
              }
            }}
            style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover' }}
          />
        : project.image
          ? <img
              src={project.image}
              alt={project.title}
              loading="lazy" decoding="async"
              onLoad={(e) => {
                if (imageRatio) return;
                const img = e.currentTarget;
                if (img.naturalWidth && img.naturalHeight) {
                  setImageRatio(`${img.naturalWidth} / ${img.naturalHeight}`);
                }
              }}
              style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover' }}
            />
          : hasSlideshow
          ? <React.Fragment>
              <img
                src={project.thumbSlideshow[slideIdx]}
                alt={project.title}
                loading="lazy" decoding="async"
                onLoad={(e) => {
                  if (slideRatio) return;
                  const img = e.currentTarget;
                  if (img.naturalWidth && img.naturalHeight) {
                    setSlideRatio(`${img.naturalWidth} / ${img.naturalHeight}`);
                  }
                }}
                style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover' }}
              />
              <div aria-hidden style={{ position: 'absolute', inset: 0, border: '1px solid var(--rule)', pointerEvents: 'none' }} />
            </React.Fragment>
          : null
      }

      {/* grey-out + title on hover (per the storyboard) */}
      <div
        aria-hidden
        style={{
          position: 'absolute', inset: 0,
          background: 'rgba(10,10,10,0.62)',
          opacity: isHovered ? 1 : 0,
          transition: 'opacity 220ms ease',
          display: 'flex', alignItems: 'center', justifyContent: 'flex-start',
          padding: '0 28px'
        }}>
        <div style={{ color: '#fafafa' }}>
          <div style={{
            fontFamily: 'var(--mono)', fontSize: 11, letterSpacing: 2,
            opacity: 0.75, marginBottom: 8
          }}>{project.no} — {project.kind.toUpperCase()}</div>
          <div style={{
            fontFamily: 'var(--display)', fontStyle: 'italic',
            fontSize: 34, lineHeight: 1.05, fontWeight: 500,
            textWrap: 'balance'
          }}>{project.title}</div>
        </div>
      </div>
    </div>);

}

function Carousel({ projects, duration, onSelect, paused, lockWheel }) {
  const [hoveredPos, setHoveredPos] = useState(null);
  const hoverRef  = useRef(false);
  hoverRef.current = hoveredPos !== null;
  const easeRef   = useRef(1);      // auto-scroll speed factor, eased toward 0 while hovering
  const trackRef  = useRef(null);
  const posRef    = useRef(-1);     // -1 = uninitialised; set to halfRef on first valid tick
  const velRef    = useRef(0);      // extra velocity injected by wheel (px/ms)
  const halfRef   = useRef(0);      // height of one copy — the step size for wrapping
  const lastRef   = useRef(null);   // timestamp of previous RAF frame
  const rafRef    = useRef(null);

  // ResizeObserver keeps halfRef current whenever videos load and change thumb heights.
  // Divides by 3 because the track renders three identical copies.
  useEffect(() => {
    if (!trackRef.current) return;
    const measure = () => {
      if (trackRef.current) halfRef.current = trackRef.current.offsetHeight / 3;
    };
    measure();
    const ro = new ResizeObserver(measure);
    ro.observe(trackRef.current);
    return () => ro.disconnect();
  }, []);

  // RAF scroll loop — replaces the CSS animation
  useEffect(() => {
    function tick(now) {
      const dt = lastRef.current != null ? Math.min(now - lastRef.current, 50) : 0;
      lastRef.current = now;

      if (dt > 0 && !paused && halfRef.current > 0) {
        // On first valid tick, start at the middle copy so there is a full
        // halfRef of backward-scroll room before hitting the seam.
        if (posRef.current < 0) posRef.current = halfRef.current;

        const target = (hoverRef.current || window.__rheonReduced) ? 0 : 1;
        easeRef.current += (target - easeRef.current) * Math.min(1, dt / 180);
        const baseSpeed = (halfRef.current / (duration * 1000)) * easeRef.current; // px/ms
        posRef.current += (baseSpeed + velRef.current) * dt;
        velRef.current *= Math.pow(0.92, dt / 16.67);          // time-normalised friction
        if (Math.abs(velRef.current) < 0.0001) velRef.current = 0;

        // Step-based wrap: keep posRef in [0, 2*halfRef).
        // Forward seam (copy C → copy B) and backward seam (copy A → copy B)
        // are both seamless because all three copies are identical.
        if (posRef.current >= 2 * halfRef.current) posRef.current -= halfRef.current;
        if (posRef.current <  0)                   posRef.current += halfRef.current;

        if (trackRef.current) {
          trackRef.current.style.transform = `translateY(-${posRef.current}px)`;
        }
      }

      rafRef.current = requestAnimationFrame(tick);
    }
    rafRef.current = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(rafRef.current);
  }, [duration, paused]);

  // Wheel listener — fires on the whole window so scrolling anywhere drives the carousel
  useEffect(() => {
    const MAX_VEL = 2.0;
    const onWheel = (e) => {
      // lockWheel: a modal is open — let the wheel scroll the modal, don't
      // preventDefault or inject velocity. The RAF auto-scroll keeps running.
      if (paused || lockWheel) return;
      e.preventDefault();
      // normalise deltaMode: 0 = px, 1 = lines (~20px), 2 = pages (~400px)
      const dy = e.deltaMode === 1 ? e.deltaY * 20 : e.deltaMode === 2 ? e.deltaY * 400 : e.deltaY;
      velRef.current = Math.max(-MAX_VEL, Math.min(MAX_VEL, velRef.current + dy * 0.008));
    };
    window.addEventListener('wheel', onWheel, { passive: false });
    return () => window.removeEventListener('wheel', onWheel);
  }, [paused, lockWheel]);

  const sequence = [];
  for (let i = 0; i < 3; i++) {
    projects.forEach((p, j) => {
      sequence.push({ p, rhythm: RHYTHM[(i * projects.length + j) % RHYTHM.length] });
    });
  }

  let posCounter = 0;
  const renderCopy = (copyKey) => sequence.map((s, k) => {
    const pos = posCounter++;
    return <Thumb
      key={`${copyKey}-${k}`}
      project={s.p}
      rhythm={s.rhythm}
      onClick={onSelect}
      position={pos}
      hoveredPos={hoveredPos}
      onHoverChange={(isHovering) => setHoveredPos(isHovering ? pos : null)} />;
  });

  return (
    <div className="carousel-frame" style={{
      position: 'absolute', top: 0, right: 0, bottom: 0,
      width: '100%',
      overflow: 'hidden',
      isolation: 'isolate',
      zIndex: 1,
      maskImage: 'linear-gradient(to bottom, transparent 0, #000 9%, #000 91%, transparent 100%)',
      WebkitMaskImage: 'linear-gradient(to bottom, transparent 0, #000 9%, #000 91%, transparent 100%)'
    }}>
      <div
        ref={trackRef}
        className="carousel-track"
        style={{
          position: 'absolute',
          top: 0, left: 0, right: 0,
          display: 'flex',
          flexDirection: 'column',
          willChange: 'transform'
        }}>
        {renderCopy('a')}
        {renderCopy('b')}
        {renderCopy('c')}
      </div>
    </div>);

}

// Standalone Grid-view toggle — lives in the top-right of the page, outside
// the carousel column, so it never overlaps a moving thumbnail.
function GridButton({ onClick }) {
  return (
    <button
      type="button"
      className="grid-btn"
      onClick={onClick}
      aria-label="Open grid view">
      <svg width="14" height="14" viewBox="0 0 16 16" fill="none">
        <rect x="1" y="1" width="6" height="6" stroke="currentColor" strokeWidth="1.4" />
        <rect x="9" y="1" width="6" height="6" stroke="currentColor" strokeWidth="1.4" />
        <rect x="1" y="9" width="6" height="6" stroke="currentColor" strokeWidth="1.4" />
        <rect x="9" y="9" width="6" height="6" stroke="currentColor" strokeWidth="1.4" />
      </svg>
      <span>GRID VIEW</span>
    </button>
  );
}

window.Carousel = Carousel;
window.GridButton = GridButton;
