/* 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 modal that opens when the
                        centre card is clicked (or any "→" arrow)

   The horizontal track-style Carousel that lived here in earlier
   prototypes has been deleted — only WheelCarousel is mounted by
   app.jsx now, so the unused code went with it.
   ============================================================ */

const { useState, useRef, useEffect } = React;

/* ============================================================
   CARDS
   ============================================================
   Each card receives `active` from the WheelCarousel — `true`
   only for the centre card.  We use it to gate the overlay-open
   behaviour: clicking a side card should rotate the carousel,
   NOT open the overlay.  We achieve that by:
     1. Letting the click bubble up to .wheel-card on side cards
        (which is where the rotate handler lives), and
     2. stopPropagation()-ing the click on the centre card so the
        rotate handler doesn't fire when we open the overlay.
*/

const AppCard = ({ item, active, onOpen }) => (
  <article
    className="card-inner card-app"
    style={{ height: '100%', display: 'flex', flexDirection: 'column' }}
    onClick={(e) => {
      // Only the centre card opens the overlay; side cards bubble
      // up so .wheel-card's onClick rotates the carousel instead.
      if (!active) return;
      e.stopPropagation();
      onOpen(item, 'app');
    }}
  >
    <div className="card-thumb">
      <span className="card-thumb-tag">{item.kicker}</span>
      <AppThumb kind={item.thumbKind} />
    </div>
    <div className="card-body">
      <div className="card-meta">
        <span>App</span>
        <span>{item.tags[item.tags.length - 1]}</span>
      </div>
      <h3 className="card-title">{item.name}</h3>
      <p className="card-desc">{item.desc}</p>
      <div className="card-foot">
        <div className="card-tags">
          {item.tags.slice(0, 2).map((t) => (
            <span key={t} className="card-tag">{t}</span>
          ))}
        </div>
        {/* Arrow always opens the overlay regardless of card position
            so a user can deep-click into a side item if they want. */}
        <span
          className="card-arrow"
          onClick={(e) => { e.stopPropagation(); onOpen(item, 'app'); }}
        >
          →
        </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">
      <span className="card-thumb-tag">{item.kicker}</span>
      <PrintThumb glyph={item.glyph} />
    </div>
    <div className="card-body">
      <div className="card-meta">
        <span>Print</span>
        <span>{item.tags[0]}</span>
      </div>
      <h3 className="card-title">{item.name}</h3>
      <p className="card-desc">{item.desc}</p>
      <div className="card-foot">
        <div className="card-tags">
          {item.tags.slice(0, 2).map((t) => (
            <span key={t} className="card-tag">{t}</span>
          ))}
        </div>
        <span
          className="card-arrow"
          onClick={(e) => { e.stopPropagation(); onOpen(item, 'print'); }}
        >
          →
        </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">
      <span className="card-thumb-tag">{item.kicker.split(' · ')[1]}</span>
      <EssayThumb essay={item} />
    </div>
    <div className="card-body">
      <div className="card-meta">
        <span>Essay</span>
        <span>{item.kicker.split(' · ')[0]}</span>
      </div>
      <h3 className="card-title">{item.name}</h3>
      <p className="card-desc">{item.desc}</p>
      <div className="card-foot">
        <div className="card-tags">
          {item.tags.slice(0, 2).map((t) => (
            <span key={t} className="card-tag">{t}</span>
          ))}
        </div>
        <span
          className="card-arrow"
          onClick={(e) => { e.stopPropagation(); onOpen(item, 'essay'); }}
        >
          →
        </span>
      </div>
    </div>
  </article>
);

/* ============================================================
   OVERLAY — full-screen modal for case-study / essay detail
   ============================================================
   Opens when the centre card is clicked.  Closes via:
     - the ✕ button
     - clicking the dim backdrop outside the card
     - pressing Escape
   While open we lock body scroll so the page underneath
   doesn't scroll behind the dialog.
*/
const Overlay = ({ open, item, kind, onClose }) => {
  // Side-effect: wire up the Escape-to-close listener and the
  // body-scroll lock for the lifetime of the open state.
  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]);

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

  // Friendly label for the type-tag in the overlay header.
  const kindLabel =
    kind === 'app' ? 'App' :
    kind === 'print' ? '3D Print' :
    'Thought Piece';

  return (
    <div className={`overlay-backdrop ${open ? 'open' : ''}`} onClick={onClose}>
      {/* stopPropagation here keeps clicks on the card from
          reaching the backdrop's onClose handler. */}
      <div className="overlay" onClick={(e) => e.stopPropagation()}>
        <div className="overlay-head">
          <span className="overlay-tag">{kindLabel} · {item.kicker}</span>
          <button className="overlay-close" onClick={onClose} aria-label="Close">✕</button>
        </div>
        <div className="overlay-body">
          <div className="overlay-title-row">
            <div>
              <h2 className="overlay-title">{item.name}</h2>
              <p className="overlay-lede">{item.lede || item.desc}</p>
            </div>
            <div className="overlay-meta">
              {Object.entries(item.meta || {}).map(([k, v]) => (
                <div key={k}><span>{k} </span><strong>{v}</strong></div>
              ))}
            </div>
          </div>
          <div className="overlay-grid">
            <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>
            <div className="overlay-side">
              <div className="side-hero">
                {kind === 'app' && <AppThumb kind={item.thumbKind} />}
                {kind === 'print' && <PrintThumb glyph={item.glyph} />}
                {kind === 'essay' && <EssayThumb essay={item} />}
              </div>
              <div className="side-list">
                {(item.tags || []).map((t) => (
                  <div key={t} className="item">
                    <span className="k">tag</span>
                    <span className="v">{t}</span>
                  </div>
                ))}
              </div>
            </div>
          </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.

   How rotation can happen:
     - ← / → keyboard arrows (window-level listener)
     - ← / → control buttons in the carousel header
     - clicking a side card (-1, +1)
     - clicking a pagination dot below the wheel
     - >>> dragging the wheel left/right with mouse, finger, or pen <<<

   Drag-to-swipe is implemented with the Pointer Events API so
   one code path covers mouse, touch and stylus.  We use a
   simple "threshold swipe" model: as long as the user drags
   more than SWIPE_THRESHOLD pixels horizontally before releasing,
   we rotate one step in that direction.  We do NOT live-follow
   the cards under the cursor — the 3D coverflow transform fights
   raw translation, and the user picked threshold swipe as the
   feel they wanted.

   Disambiguating click vs drag:
     - any pointer travel above DRAG_NOISE marks the gesture as
       a drag, so the trailing click event is suppressed.
     - this is what stops a side-card swipe from also firing
       that card's "click to rotate" handler twice (once for the
       drag, once for the click).
*/

// Pixel thresholds — tune these to taste.
const DRAG_NOISE = 5;       // anything under this is treated as a click, not a drag
const SWIPE_THRESHOLD = 60; // minimum horizontal travel to actually rotate

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

  // Drag bookkeeping is held in a ref so updates inside pointer
  // handlers don't trigger re-renders (which would cancel the
  // gesture).  Fields:
  //   pointerId  : id of the active pointer, null when idle
  //   startX     : clientX where the press began
  //   isDragging : have we moved more than DRAG_NOISE yet?
  //   wasDrag    : did the just-finished gesture qualify as a
  //                drag?  Read by child onClick handlers via
  //                consumeDragClick() to decide whether to fire.
  const dragRef = useRef({
    pointerId: null,
    startX: 0,
    isDragging: false,
    wasDrag: false,
  });

  // Rotate the wheel by `delta` (positive = next, negative = prev).
  // Modulo arithmetic wraps around the ends so the wheel is endless.
  const go = (delta) => {
    setIndex((i) => (i + delta + total) % total);
  };

  /* --------- Pointer-event drag handlers ---------
     Attached to the .wheel container.  We use Pointer Events
     instead of separate mouse/touch listeners so the same code
     path works for mouse, touch and pen with no branching.
  */

  const onPointerDown = (e) => {
    // Ignore non-primary mouse buttons (right-click, middle-click).
    // Touch and pen always report button=0 so they're unaffected.
    if (e.button !== undefined && e.button !== 0) return;

    dragRef.current = {
      pointerId: e.pointerId,
      startX: e.clientX,
      isDragging: false,
      wasDrag: false,
    };

    // setPointerCapture redirects subsequent move/up events to
    // this element even if the pointer leaves it.  Without this
    // a fast drag that ends off-stage would leave the wheel in a
    // half-stuck state.  Optional-chained because some test envs
    // omit the API.
    e.currentTarget.setPointerCapture?.(e.pointerId);
  };

  const onPointerMove = (e) => {
    const s = dragRef.current;
    // Ignore pointers other than the one we're currently tracking.
    if (s.pointerId !== e.pointerId) return;

    const dx = e.clientX - s.startX;
    // Once we've crossed the noise threshold we're committed —
    // mark the gesture as a drag so the trailing click is
    // suppressed even if the user wiggles back to startX.
    if (!s.isDragging && Math.abs(dx) > DRAG_NOISE) {
      s.isDragging = true;
    }
  };

  const onPointerUp = (e) => {
    const s = dragRef.current;
    if (s.pointerId !== e.pointerId) return;

    const dx = e.clientX - s.startX;

    if (s.isDragging) {
      // The next click event (which fires after pointerup) should
      // be ignored — it's the tail end of a drag, not a real click.
      s.wasDrag = true;

      // Decide whether the drag was committed enough to rotate.
      // Dragging right (positive dx) means "show me the previous
      // card", since the visual effect is sliding the deck right.
      if (dx > SWIPE_THRESHOLD) go(-1);
      else if (dx < -SWIPE_THRESHOLD) go(1);
    }

    // Release pointer capture and reset the active pointer slot.
    e.currentTarget.releasePointerCapture?.(e.pointerId);
    s.pointerId = null;
    s.isDragging = false;
  };

  // Called by the wheel-card and its children to ask: "was this
  // click really a click, or the trailing event of a finished
  // drag?"  Returns true if the click should be ignored.  Self-
  // resetting so each drag suppresses exactly one click.
  const consumeDragClick = () => {
    if (dragRef.current.wasDrag) {
      dragRef.current.wasDrag = false;
      return true;
    }
    return false;
  };

  /* --------- Keyboard navigation ---------
     Window-level listener so the user doesn't need to focus the
     carousel to use ← / →.  We bail if the focus is inside the
     overlay (otherwise arrow keys would rotate carousels behind
     the open dialog).
  */
  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]);

  /* --------- Position helper ---------
     Given an item's index in the array, return its position
     relative to the centre card as a string the CSS understands:
       "0"      = centre, full size
       "-1"/"1" = immediate flanks
       "-2"/"2" = far flanks
       "hidden" = off-stage (zero opacity)
     We compute the shortest signed distance around the ring so
     the wheel wraps cleanly past the ends of the array.
  */
  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>

      {/* The .wheel is the drag surface.  Pointer handlers live
          here, not on individual cards, so a drag that starts on
          the centre card but ends past a flank still works. */}
      <div
        className="wheel"
        onPointerDown={onPointerDown}
        onPointerMove={onPointerMove}
        onPointerUp={onPointerUp}
        onPointerCancel={onPointerUp}
      >
        <div className="wheel-stage">
          {items.map((item, i) => {
            const pos = getPos(i);
            return (
              <div
                key={item.id}
                className="wheel-card"
                data-pos={pos}
                onClick={(e) => {
                  // Suppress the click that fires at the end of a
                  // drag — otherwise dragging across a side card
                  // would rotate twice.
                  if (consumeDragClick()) {
                    e.stopPropagation();
                    return;
                  }
                  // Centre card click is handled by the inner
                  // <article> (it stops propagation and opens the
                  // overlay).  We only get here for side-card clicks.
                  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>

      {/* Pagination dots.  The active dot stretches into a pill
          (handled in CSS).  Clicking jumps the wheel directly. */}
      <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>
    </section>
  );
};

/* ------------------------------------------------------------
   Expose to global scope.  This file is loaded as a Babel
   <script type="text/babel">, not an ES module, so we attach
   each component to `window` for app.jsx to pick up.
   ------------------------------------------------------------ */
window.WheelCarousel = WheelCarousel;
window.AppCard       = AppCard;
window.PrintCard     = PrintCard;
window.EssayCard     = EssayCard;
window.Overlay       = Overlay;
