/* eslint-disable */
/* ============================================================
   components.jsx — building blocks for the portfolio page
   ============================================================
   Exports (attached to `window` so the in-browser Babel build
   can use them across files without a real module system):

     - WheelCarousel  : 3D coverflow-style carousel used by all
                        three section bands (apps / prints / essays)
     - AppCard        : content card for the "Apps" stream
     - PrintCard      : content card for the "3D Prints" stream
     - EssayCard      : content card for the "Writing" stream
     - Overlay        : full-screen "case study" modal that opens
                        when the centre card is clicked
     - ArchiveView    : flat grid for retired / older items

   Visual Update (June 2026)
   -------------------------
   Cards lead with a small set of *key features* (a platform·status
   chip on the hero + a tech/spec footer line) instead of free-form
   tags.  The overlay is an editorial case-study: prose + a gallery
   strip on the left, a sticky spec panel on the right that carries
   a generated QR + App Store button when an app is live.
   ============================================================ */

const { useState, useRef, useEffect } = React;

/* Small shared helpers ------------------------------------------------ */

// Capitalise a status word ('beta' → 'Beta').
const cap = (s) => (s ? s.charAt(0).toUpperCase() + s.slice(1) : s);

// Colour-coded status dot.  Colour comes from the --status-* token
// for the given lifecycle value (idea | alpha | beta | live).
const StatusDot = ({ status }) => (
  <span className="status-dot" style={{ background: `var(--status-${status})` }} />
);

// Status pill used in the overlay spec panel — dot + label, tinted
// by the status colour via currentColor.
const StatusPill = ({ status }) => (
  <span className="status-pill" style={{ color: `var(--status-${status})` }}>
    <span className="status-dot" style={{ background: 'currentColor' }} />
    {cap(status)}
  </span>
);

/* ============================================================
   CARDS
   ============================================================
   Each card receives `active` from the WheelCarousel — `true`
   only for the centre card.  Clicking the centre card opens the
   overlay; clicking a side card bubbles up so .wheel-card rotates
   the carousel instead (we stopPropagation only on the centre).
*/

const AppCard = ({ item, active, onOpen }) => (
  <article
    className="card-inner card-app"
    style={{ height: '100%', display: 'flex', flexDirection: 'column' }}
    onClick={(e) => {
      if (!active) return;
      e.stopPropagation();
      onOpen(item, 'app');
    }}
  >
    <div className="card-thumb">
      {/* platforms on the left, colour-coded status on the right */}
      <span className="chip">{item.platforms?.join(' · ')}</span>
      <span className="chip chip-right">
        <StatusDot status={item.status} />
        <span className="chip-status" style={{ color: `var(--status-${item.status})` }}>
          {cap(item.status)}
        </span>
      </span>
      {/* `item.image` is optional — when set in data.jsx the thumb
          component swaps the CSS placeholder for a real <img>. */}
      <AppThumb kind={item.thumbKind} image={item.image} />
    </div>
    <div className="card-body">
      <h3 className="card-title">{item.name}</h3>
      <p className="card-lede">{item.lede}</p>
      <div className="card-foot">
        <span className="card-tech">{item.stack?.join(' · ')}</span>
      </div>
    </div>
  </article>
);

const PrintCard = ({ item, active, onOpen }) => (
  <article
    className="card-inner card-print"
    style={{ height: '100%', display: 'flex', flexDirection: 'column' }}
    onClick={(e) => {
      if (!active) return;
      e.stopPropagation();
      onOpen(item, 'print');
    }}
  >
    <div className="card-thumb">
      {/* colour-coded status on the right */}
      <span className="chip chip-right">
        <StatusDot status={item.status} />
        <span className="chip-status" style={{ color: `var(--status-${item.status})` }}>
          {cap(item.status)}
        </span>
      </span>
      <PrintThumb glyph={item.glyph} image={item.image} />
    </div>
    <div className="card-body">
      <h3 className="card-title">{item.name}</h3>
      <p className="card-lede">{item.lede}</p>
      <div className="card-foot">
        <span className="card-tech">
          {[item.material, item.printTime].filter(Boolean).join(' · ')}
        </span>
      </div>
    </div>
  </article>
);

const EssayCard = ({ item, active, onOpen }) => (
  <article
    className="card-inner card-essay"
    style={{ height: '100%', display: 'flex', flexDirection: 'column' }}
    onClick={(e) => {
      if (!active) return;
      e.stopPropagation();
      onOpen(item, 'essay');
    }}
  >
    <div className="card-thumb">
      {/* Essays have no lifecycle status — the chip carries the date. */}
      <span className="chip">{item.date}</span>
      <EssayThumb essay={item} image={item.image} />
    </div>
    <div className="card-body">
      <h3 className="card-title">{item.name}</h3>
      <p className="card-lede">{item.lede}</p>
      <div className="card-foot">
        <span className="card-tech">
          {[item.readTime, item.topic].filter(Boolean).join(' · ')}
        </span>
      </div>
    </div>
  </article>
);

/* ============================================================
   GALLERY — screenshot strip + lightbox
   ============================================================
   Renders `images` as a horizontal strip of thumbnails inside the
   overlay.  Clicking one opens a simple lightbox above the modal.
   The lightbox closes on click; stopPropagation keeps that click
   from also closing the overlay behind it.
*/
const Gallery = ({ images }) => {
  const [active, setActive] = useState(null);
  if (!images || images.length === 0) return null;

  return (
    <div className="gallery">
      <div className="gallery-strip">
        {images.map((src, i) => (
          <button
            key={i}
            type="button"
            className="gallery-thumb"
            onClick={() => setActive(src)}
            aria-label={`View screenshot ${i + 1}`}
          >
            <img src={src} alt="" loading="lazy" draggable={false} />
          </button>
        ))}
      </div>
      {active && (
        <div
          className="lightbox open"
          onClick={(e) => { e.stopPropagation(); setActive(null); }}
        >
          <img src={active} alt="" />
        </div>
      )}
    </div>
  );
};

/* ============================================================
   QR CODE — client-side, generated from the App Store URL
   ============================================================
   Uses the `qrcodejs` global (loaded in index.html).  Rendering
   happens entirely in-browser — no network request, nothing is
   sent anywhere.  If the library failed to load we render nothing
   and let the App Store button stand on its own.
*/
const QrCode = ({ url, size = 116 }) => {
  const ref = useRef(null);
  useEffect(() => {
    const el = ref.current;
    if (!el || !window.QRCode) return;
    el.innerHTML = '';
    new window.QRCode(el, {
      text: url,
      width: size,
      height: size,
      colorDark: '#10141A',
      colorLight: '#EFF1F4',
      correctLevel: window.QRCode.CorrectLevel.M,
    });
  }, [url, size]);

  if (!window.QRCode) return null;
  return <div className="qr" ref={ref} aria-label="Scan to open in the App Store" />;
};

// "Download on the App Store" pill.  Self-contained (inline Apple
// glyph) so we don't hot-link Apple's hosted badge asset.
const AppStoreButton = ({ url }) => (
  <a className="store-btn" href={url} target="_blank" rel="noopener noreferrer">
    <svg className="store-apple" viewBox="0 0 16 16" aria-hidden="true">
      <path d="M11.182.008C11.148-.03 9.923.023 8.857 1.18c-1.066 1.156-.902 2.482-.878 2.516.024.034 1.52.087 2.475-1.258.955-1.345.762-2.391.728-2.43Zm3.314 11.733c-.048-.096-2.325-1.234-2.113-3.422.212-2.189 1.675-2.789 1.698-2.854.023-.065-.597-.79-1.254-1.157a3.692 3.692 0 0 0-1.563-.434c-.108-.003-.483-.095-1.254.116-.508.139-1.653.589-1.968.607-.316.018-1.256-.522-2.267-.665-.647-.125-1.333.131-1.824.328-.49.196-1.422.754-2.074 2.237-.652 1.482-.311 3.83-.067 4.56.244.729.625 1.924 1.273 2.796.576.984 1.34 1.667 1.659 1.899.319.232 1.219.386 1.843.067.502-.308 1.408-.485 1.766-.472.357.013 1.061.154 1.782.539.571.197 1.111.115 1.652-.105.541-.221 1.324-1.059 2.238-2.758.347-.79.505-1.217.473-1.282Z" />
    </svg>
    <span className="store-text">
      <span className="store-sm">Download on the</span>
      <span className="store-lg">App Store</span>
    </span>
  </a>
);

/* ============================================================
   OVERLAY — editorial case-study modal
   ============================================================
   Layout: a full-width title + lede, then a two-column grid —
     left  : prose (`body`) + the screenshot Gallery
     right : a sticky spec panel (hero image, key features, and —
             for live apps with an App Store URL — a QR + button)
   Closes via the ✕, the dim backdrop, or Escape; body scroll is
   locked while open.
*/

// Header meta string under the title-tag, per section kind.
const headerMeta = (item, kind) => {
  if (kind === 'app')   return [item.platforms?.join(' · '), cap(item.status)].filter(Boolean).join(' · ');
  if (kind === 'print') return [item.material, cap(item.status)].filter(Boolean).join(' · ');
  if (kind === 'essay') return [item.date, item.readTime].filter(Boolean).join(' · ');
  return '';
};

// [label, value] rows for the spec panel, per section kind.
// `value` may be a string (skipped when empty) or a React element.
const specRows = (item, kind) => {
  if (kind === 'app') return [
    ['Platforms', item.platforms?.join(' · ')],
    ['Status', <StatusPill status={item.status} />],
    ['Tech', item.stack?.join(' · ')],
  ];
  if (kind === 'print') return [
    ['Material', item.material],
    ['Print time', item.printTime],
    ['Status', <StatusPill status={item.status} />],
  ];
  if (kind === 'essay') return [
    ['Published', item.date],
    ['Read', item.readTime],
    ['Topic', item.topic],
  ];
  return [];
};

const Overlay = ({ open, item, kind, onClose }) => {
  // Escape-to-close + body-scroll lock for the lifetime of `open`.
  useEffect(() => {
    if (!open) return;
    const onKey = (e) => { if (e.key === 'Escape') onClose(); };
    window.addEventListener('keydown', onKey);
    document.body.style.overflow = 'hidden';
    return () => {
      window.removeEventListener('keydown', onKey);
      document.body.style.overflow = '';
    };
  }, [open, onClose]);

  // Before the overlay is ever opened we still render an (invisible)
  // backdrop so the first open transition has something to fade onto.
  if (!item) {
    return (
      <div className={`overlay-backdrop ${open ? 'open' : ''}`} onClick={onClose}>
        <div />
      </div>
    );
  }

  const kindLabel =
    kind === 'app' ? 'App' :
    kind === 'print' ? '3D Print' :
    'Thought Piece';

  // Hero shown in the spec panel — prefer a wider `overlayImage`
  // crop, fall back to the card `image`.
  const heroImg = item.overlayImage || item.image;
  const Hero = () => {
    if (kind === 'app')   return <AppThumb   kind={item.thumbKind} image={heroImg} />;
    if (kind === 'print') return <PrintThumb glyph={item.glyph}    image={heroImg} />;
    return <EssayThumb essay={item} image={heroImg} />;
  };

  const rows = specRows(item, kind).filter(([, v]) => v);
  const showStore = kind === 'app' && item.status === 'live' && item.appStore;

  return (
    <div className={`overlay-backdrop ${open ? 'open' : ''}`} onClick={onClose}>
      <div className="overlay" onClick={(e) => e.stopPropagation()}>
        <div className="overlay-head">
          <span className="overlay-tag">{kindLabel} · {headerMeta(item, kind)}</span>
          <button className="overlay-close" onClick={onClose} aria-label="Close">✕</button>
        </div>

        <div className="overlay-body">
          <div className="overlay-title-row">
            <h2 className="overlay-title">{item.name}</h2>
            <p className="overlay-lede">{item.lede}</p>
          </div>

          <div className="overlay-grid">
            {/* LEFT — prose + screenshot gallery */}
            <div className="overlay-main">
              <div className="overlay-prose">
                {(item.body || []).map((b, i) => (
                  <React.Fragment key={i}>
                    {b.h && <h3>{b.h}</h3>}
                    {b.p && <p>{b.p}</p>}
                  </React.Fragment>
                ))}
              </div>
              <Gallery images={item.gallery} />
            </div>

            {/* RIGHT — sticky spec panel */}
            <aside className="overlay-spec">
              <div className="spec-hero"><Hero /></div>
              <dl className="spec-list">
                {rows.map(([k, v], i) => (
                  <div className="spec-row" key={i}>
                    <dt>{k}</dt>
                    <dd>{v}</dd>
                  </div>
                ))}
              </dl>
              {showStore && (
                <div className="spec-store">
                  <QrCode url={item.appStore} />
                  <AppStoreButton url={item.appStore} />
                </div>
              )}
            </aside>
          </div>
        </div>
      </div>
    </div>
  );
};

/* ============================================================
   WHEEL CAROUSEL — 3D coverflow-style rotator
   ============================================================
   Maintains a single integer `index` pointing at the centre card.
   Every other card is positioned relative to it via the `data-pos`
   attribute (-2, -1, 0, 1, 2, or "hidden") which the CSS in
   index.html turns into 3D transforms.

   Rotation: ← / → keys, the header control buttons, clicking a side
   card, clicking a pagination dot, or dragging the wheel.  Drag is
   implemented with the Pointer Events API (one path for mouse /
   touch / pen) using a threshold-swipe model.
*/

const DRAG_NOISE = 5;       // travel under this is a click, not a drag
const SWIPE_THRESHOLD = 60; // minimum horizontal travel to rotate

const WheelCarousel = ({ num, title, sub, items, renderCard, archiveHref }) => {
  const [index, setIndex] = useState(0);
  const total = items.length;

  // Drag bookkeeping in a ref so pointer-handler updates don't
  // re-render (which would cancel the gesture).
  const dragRef = useRef({ pointerId: null, startX: 0, isDragging: false, wasDrag: false });

  const go = (delta) => setIndex((i) => (i + delta + total) % total);

  const onPointerDown = (e) => {
    if (e.button !== undefined && e.button !== 0) return;
    dragRef.current = { pointerId: e.pointerId, startX: e.clientX, isDragging: false, wasDrag: false };
    // setPointerCapture is deferred to onPointerMove so clean clicks
    // aren't retargeted away from the inner article's onClick.
  };

  const onPointerMove = (e) => {
    const s = dragRef.current;
    if (s.pointerId !== e.pointerId) return;
    const dx = e.clientX - s.startX;
    if (!s.isDragging && Math.abs(dx) > DRAG_NOISE) {
      s.isDragging = true;
      e.currentTarget.setPointerCapture?.(e.pointerId);
    }
  };

  const onPointerUp = (e) => {
    const s = dragRef.current;
    if (s.pointerId !== e.pointerId) return;
    const dx = e.clientX - s.startX;
    if (s.isDragging) {
      s.wasDrag = true;
      if (dx > SWIPE_THRESHOLD) go(-1);
      else if (dx < -SWIPE_THRESHOLD) go(1);
    }
    if (e.currentTarget.hasPointerCapture?.(e.pointerId)) {
      e.currentTarget.releasePointerCapture?.(e.pointerId);
    }
    s.pointerId = null;
    s.isDragging = false;
  };

  // Capture-phase click suppressor — eats the trailing click after a
  // drag so a swipe doesn't also fire a card's click handler.
  const onClickCapture = (e) => {
    if (dragRef.current.wasDrag) {
      dragRef.current.wasDrag = false;
      e.stopPropagation();
      e.preventDefault();
    }
  };

  // Window-level ← / → navigation (ignored while the overlay is open).
  useEffect(() => {
    const onKey = (e) => {
      if (e.target.closest && e.target.closest('.overlay')) return;
      if (e.key === 'ArrowLeft') go(-1);
      if (e.key === 'ArrowRight') go(1);
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [total]);

  // Shortest signed distance around the ring → a data-pos string.
  const getPos = (i) => {
    let d = i - index;
    if (d > total / 2) d -= total;
    if (d < -total / 2) d += total;
    if (d < -2 || d > 2) return 'hidden';
    return String(d);
  };

  return (
    <section className="carousel" data-carousel-mode="wheel">
      <div className="carousel-head">
        <div className="carousel-title-group">
          <span className="carousel-num">{num}</span>
          <h2 className="carousel-title">{title}</h2>
          <span className="carousel-sub">{sub}</span>
        </div>
        <div className="carousel-controls">
          <button className="ctrl-btn" onClick={() => go(-1)} aria-label="Previous">←</button>
          <span className="carousel-count">
            {String(index + 1).padStart(2, '0')} / {String(total).padStart(2, '0')}
          </span>
          <button className="ctrl-btn" onClick={() => go(1)} aria-label="Next">→</button>
        </div>
      </div>

      <div
        className="wheel"
        onPointerDown={onPointerDown}
        onPointerMove={onPointerMove}
        onPointerUp={onPointerUp}
        onPointerCancel={onPointerUp}
        onClickCapture={onClickCapture}
      >
        <div className="wheel-stage">
          {items.map((item, i) => {
            const pos = getPos(i);
            return (
              <div
                key={item.id}
                className="wheel-card"
                data-pos={pos}
                onClick={() => {
                  if (pos === '0') return;
                  if (pos === '-1' || pos === '-2') go(-1);
                  if (pos === '1'  || pos === '2')  go(1);
                }}
              >
                {renderCard(item, pos === '0')}
              </div>
            );
          })}
        </div>
      </div>

      <div className="wheel-dots">
        {items.map((_, i) => (
          <button
            key={i}
            className={`wheel-dot ${i === index ? 'active' : ''}`}
            onClick={() => setIndex(i)}
            aria-label={`Go to ${i + 1}`}
          />
        ))}
      </div>

      {archiveHref && (
        <div className="archive-link-row">
          <a className="archive-link" href={archiveHref}>
            View archive <span className="arrow">→</span>
          </a>
        </div>
      )}
    </section>
  );
};

/* ============================================================
   ARCHIVE VIEW — flat grid for retired / older items
   ============================================================
   Rendered on the dedicated archive pages instead of the 3D
   coverflow.  Items are filtered for `archived: true` by app.jsx
   before being passed in; this component just lays them out using
   the same cards.
*/
const ArchiveView = ({ section, items, openOverlay }) => {
  const META = {
    apps: {
      title: 'Apps',
      lede:  'Older app concepts and shipped pieces that have stepped out of the spotlight.',
      render: (item) => <AppCard   item={item} active onOpen={openOverlay} />,
    },
    '3ddesign': {
      title: '3D Prints',
      lede:  'Earlier prints — design experiments, retired functional pieces, lessons in tolerance.',
      render: (item) => <PrintCard item={item} active onOpen={openOverlay} />,
    },
    thoughts: {
      title: 'Thought pieces',
      lede:  'Older essays — still here, no longer front-of-mind.',
      render: (item) => <EssayCard item={item} active onOpen={openOverlay} />,
    },
  };

  const m = META[section] || META.apps;

  return (
    <section className="archive-band">
      <header className="archive-head">
        <a className="archive-back" href="/">
          <span className="arrow">←</span> Back to home
        </a>
        <div className="archive-title-group">
          <span className="archive-eyebrow">Archive</span>
          <h1 className="archive-title">{m.title}</h1>
          <p className="archive-lede">{m.lede}</p>
        </div>
      </header>

      {items.length === 0 ? (
        <div className="archive-empty">Nothing archived here yet.</div>
      ) : (
        <div className="archive-grid">
          {items.map((item) => (
            <div key={item.id} className="archive-cell">
              {m.render(item)}
            </div>
          ))}
        </div>
      )}
    </section>
  );
};

/* ------------------------------------------------------------
   Expose to global scope (loaded as <script type="text/babel">,
   not an ES module).
   ------------------------------------------------------------ */
window.WheelCarousel = WheelCarousel;
window.AppCard       = AppCard;
window.PrintCard     = PrintCard;
window.EssayCard     = EssayCard;
window.Overlay       = Overlay;
window.ArchiveView   = ArchiveView;
