// components/highlight-marker.jsx
//
// <HighlightMarker> — rotating phrase wrapped in a hand-drawn highlighter
// stroke.
//
// User direction (current iteration):
//   - The outer wrapper reserves the width of the LONGEST variant so the
//     surrounding sentence text never jitters as variants rotate.
//   - Inside that reserved space, the marker stroke + text are sized to
//     the CURRENT variant only, so the swipe always reads as a snug,
//     hand-drawn marker across THESE words (not a fixed-width banner
//     that leaves empty highlighter on either side).
//   - The active block is CENTERED within the reserved space — symmetric
//     whitespace looks intentional; left-align would leave a one-sided
//     chasm before the next sentence word.
//
// Crossfade choreography (per swap):
//   t=0    text fades 1 → 0 in 120ms
//   t=120  idx swaps, .hl-active resizes to the new variant width (CSS
//          width transition 280ms), stroke stretches via
//          preserveAspectRatio="none"
//   t=120  next variant text fades 0 → 1 in 140ms
//
// Per-variant tilt is deterministic (idx-based, range -1.2°..+1.2°) so
// repeats of the same variant look identical.

// HIGHLIGHT_COLORS — two-tone palette per hue. Inner is the lighter,
// brighter tint that the text sits on; outer is a SUBTLY darker tint
// of the same hue that only peeks through where the imperfect SVG
// path edges wobble away from the outer bg's rectangular bounds.
// Contrast is intentionally ~10-12% luminance — enough to read as
// "deeper ink at the edge" but not so much that the outer rim
// becomes a distinct second band.
const HIGHLIGHT_COLORS = {
  icyBlue: { outer: "#6DC9F2", inner: "#7CD5FF" },
  mint:    { outer: "#87E89E", inner: "#9CFAB8" },
  peach:   { outer: "#F09765", inner: "#FFB088" },
};

// Render a variant string with optional \n breakpoints. The break renders
// as a <br> with class .hl-mobile-break, which CSS toggles on/off per
// breakpoint — hidden on desktop (so text reads as one line with a
// preserved space), shown on mobile (so the phrase wraps to 2 lines).
function renderVariantText(text) {
  if (typeof text !== "string" || text.indexOf("\n") === -1) return text;
  const parts = text.split("\n");
  const nodes = [];
  parts.forEach((p, i) => {
    if (i > 0) {
      nodes.push(" ");
      nodes.push(<br key={"br" + i} className="hl-mobile-break" />);
    }
    nodes.push(p);
  });
  return nodes;
}

function HighlightMarker({ variants, color = "peach", intervalMs = 3000, armed = true, reduced = false, stack = false, endPunct = "" }) {
  const [idx, setIdx] = React.useState(0);
  const [phase, setPhase] = React.useState("in"); // "in" | "out"
  const measureRefs = React.useRef([]);
  const [widths, setWidths] = React.useState([]);

  // Measure every variant's natural width. Runs once on mount + again
  // after fonts load (Gelasio is async; if we don't re-measure, every
  // variant gets its FALLBACK-font width and the wrapper ends up too
  // narrow once the real font swaps in).
  React.useLayoutEffect(() => {
    function measure() {
      if (!measureRefs.current.length) return;
      const measured = measureRefs.current.map((el) => (el ? el.offsetWidth : 0));
      setWidths(measured);
    }
    measure();
    if (document.fonts && document.fonts.ready && document.fonts.ready.then) {
      document.fonts.ready.then(measure);
    }
  }, [variants]);

  // Rotation loop. Gated by `armed` (section visible) + `reduced`.
  React.useEffect(() => {
    if (reduced || !armed) return;
    if (variants.length <= 1) return;
    const tick = setInterval(() => {
      setPhase("out");
      // After the fade-out, swap idx and fade the new variant in. The
      // .hl-active CSS width-transition runs simultaneously, so the
      // stroke morphs in lock-step with the swap.
      setTimeout(() => {
        setIdx((i) => (i + 1) % variants.length);
        setPhase("in");
      }, 120);
    }, intervalMs);
    return () => clearInterval(tick);
  }, [armed, reduced, intervalMs, variants.length]);

  const maxW = widths.length ? Math.max(...widths) : null;
  const curW = widths.length ? widths[idx] : null;
  // .hl-active has 6px padding on each side → add 12 to reserve space
  // for the longest variant when it lands as current.
  const HL_PAD = 12;

  // In stack mode (2-line text box) we skip the dynamic-width logic
  // entirely. CSS sets a fixed max-width and lets phrases naturally
  // wrap to 2 lines. The marker height grows with the wrapped text.
  const fixedWrapperWidth = stack ? null : (maxW != null ? { width: (maxW + HL_PAD) + "px" } : null);
  const fixedActiveWidth  = stack ? null : (curW != null ? { width: (curW + HL_PAD) + "px" } : null);
  // Two-tone palette. The OUTER layer (.hl-active background) uses the
  // darker tint; the INNER SVG stroke uses the lighter tint over it.
  const palette = HIGHLIGHT_COLORS[color] || HIGHLIGHT_COLORS.peach;
  const outerFill = palette.outer;
  const innerFill = palette.inner;
  // Idx-based pseudo-random tilt. Stable across re-renders.
  const tilt = ((idx * 37) % 25 - 12) / 10;

  return (
    <span
      className={"hl-marker hl-marker-" + color + (stack ? " hl-marker-stack" : "")}
      aria-live="polite"
      aria-atomic="true"
      style={fixedWrapperWidth}
    >
      {/* Active block: sized to the current variant only, centered
          horizontally within the reserved width. Houses the stroke
          (absolute, fills .hl-active) and the visible text. */}
      <span
        className="hl-active"
        style={fixedActiveWidth ? { ...fixedActiveWidth, background: outerFill } : { background: outerFill }}
      >
        {/* INNER decorative SVG — lighter same-hue tint with hand-drawn
            imperfect edge. Sits ON TOP of the outer CSS bg, slightly
            inset on top + bottom so the darker outer rim shows through
            as a "ink pooled at the edge" effect.
            preserveAspectRatio="none" stretches the path to the span's
            width; the small ~1-2 unit corner/edge variations scale
            anisotropically, reading as a marker stroke. Crucially this
            layer is purely visual — even if its em-height misses an
            ascender, the OUTER bg still covers that pixel. */}
        <svg
          className="hl-stroke hl-stroke-inner"
          viewBox="0 0 200 40"
          preserveAspectRatio="none"
          aria-hidden="true"
          style={stack ? null : { transform: `translateY(-50%) rotate(${tilt}deg)` }}
        >
          <path
            d="M 5 8
               C 35 4, 90 6, 145 4
               C 178 3, 197 6, 197 11
               C 198 18, 196 25, 197 30
               C 196 35, 170 37, 135 35
               C 90 38, 40 35, 6 37
               C 2 36, 1 33, 2 30
               C 1 24, 3 17, 3 11
               C 3 6, 3 5, 5 8
               Z"
            fill={innerFill}
            fillOpacity="1"
          />
        </svg>
        <span className={"hl-text hl-text-" + phase} key={idx}>
          {renderVariantText(variants[idx])}{endPunct}
        </span>
      </span>

      {/* Hidden mirrors used purely for offsetWidth measurement of every variant.
          Include the same endPunct so wrapper width reservation accounts for it. */}
      <span className="hl-measures" aria-hidden="true">
        {variants.map((v, i) => (
          <span
            key={i}
            ref={(el) => { measureRefs.current[i] = el; }}
            className="hl-measure-item"
          >{renderVariantText(v)}{endPunct}</span>
        ))}
      </span>
    </span>
  );
}

window.HighlightMarker = HighlightMarker;
window.HIGHLIGHT_COLORS = HIGHLIGHT_COLORS;
