/* global React */

/**
 * vertical-cut-reveal.jsx
 *
 * Provides the <VerticalCutReveal> component — a text animation that reveals
 * characters, words, or lines by sliding them up from behind a clip mask,
 * creating a "cut into view" typographic effect.
 *
 * How it works:
 *  1. The text is split into cells (characters / words / lines).
 *  2. Each cell gets an overflow:hidden clip wrapper.
 *  3. The inner span starts at translateY(110%) — hidden below the clip.
 *  4. On trigger (mount or IntersectionObserver), each span transitions to
 *     translateY(0) with a per-cell stagger delay.
 *
 * Props:
 *  children        React node   — text content (nested elements are flattened)
 *  splitBy         "characters" | "words" | "lines"  (default "characters")
 *  duration        number  — transition duration in seconds  (default 0.9)
 *  staggerDuration number  — per-cell delay increment in seconds (default 0.04)
 *  staggerFrom     "first" | "last" | "center" | "random" | number
 *  reverse         boolean — if true, cells slide down instead of up
 *  delay           number  — global delay before stagger begins (seconds)
 *  easing          string  — CSS easing function
 *  triggerOnView   boolean — animate on IntersectionObserver instead of mount
 *  className       string  — extra classes on the root <span>
 *  uppercase       boolean — applies uppercase CSS transform to the wrapper
 *
 * Exports: window.VerticalCutReveal
 */

const { useState: useStateVCR, useEffect: useEffectVCR, useRef: useRefVCR, useMemo: useMemoVCR } = React;

function VerticalCutReveal({
  children,
  splitBy = "characters",
  staggerDuration = 0.04,
  staggerFrom = "first",
  reverse = false,
  delay = 0,
  duration = 0.9,
  easing = "cubic-bezier(0.22, 1.4, 0.36, 1)",
  triggerOnView = false,
  className = "",
  inlineFlex = true,
  uppercase = false,
}) {
  // Recursively extract a plain string from React children so we can
  // split arbitrary child nodes (strings, spans, etc.) into cells.
  function flattenText(node) {
    if (node == null || node === false || node === true) return "";
    if (typeof node === "string" || typeof node === "number") return String(node);
    if (Array.isArray(node)) return node.map(flattenText).join("");
    if (typeof node === "object" && node.props && node.props.children !== undefined) {
      return flattenText(node.props.children);
    }
    return "";
  }
  const text = flattenText(children);
  const containerRef = useRefVCR(null);

  // `active` flips to true when the animation should begin.
  // If triggerOnView is false it's true from the start (animates on mount).
  const [active, setActive] = useStateVCR(!triggerOnView);

  // Watch the root element and trigger once at least 20% is visible.
  useEffectVCR(() => {
    if (!triggerOnView) return;
    const el = containerRef.current;
    if (!el) return;
    const io = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          setActive(true);
          io.disconnect();
        }
      },
      { threshold: 0.2, rootMargin: "-50px" }
    );
    io.observe(el);
    return () => io.disconnect();
  }, [triggerOnView]);

  // Build the cell groups. Each group is one word (or line), and `space`
  // flags whether a trailing &nbsp; spacer should follow it.
  const groups = useMemoVCR(() => {
    if (splitBy === "lines") {
      return text.split("\n").map((line) => ({ cells: [line], space: false, line: true }));
    }
    const words = text.split(" ");
    return words.map((w, wi) => {
      const cells = splitBy === "characters" ? Array.from(w) : [w];
      return { cells, space: wi < words.length - 1, line: false };
    });
  }, [text, splitBy]);

  // Total cell count is needed to compute "last" and "center" stagger origins.
  const total = useMemoVCR(
    () => groups.reduce((sum, g) => sum + g.cells.length, 0),
    [groups]
  );

  // Returns the stagger delay (in seconds) for cell at index i.
  const getDelay = (i) => {
    if (staggerFrom === "first") return i * staggerDuration;
    if (staggerFrom === "last") return (total - 1 - i) * staggerDuration;
    if (staggerFrom === "center")
      return Math.abs(Math.floor(total / 2) - i) * staggerDuration;
    if (typeof staggerFrom === "number")
      return Math.abs(staggerFrom - i) * staggerDuration;
    if (staggerFrom === "random") {
      // Deterministic pseudo-random using a simple LCG — avoids re-renders
      // changing the order on every render cycle.
      return ((i * 9301 + 49297) % total) * staggerDuration / 4;
    }
    return i * staggerDuration;
  };

  let cellIdx = 0;
  const flexClass = splitBy === "lines" ? "flex flex-col" : "inline-flex flex-wrap";
  const ucClass = uppercase ? "uppercase" : "";

  return (
    <span
      ref={containerRef}
      className={`${flexClass} ${ucClass} ${className}`}
      aria-label={text}
    >
      {groups.map((g, gi) => (
        <span
          key={gi}
          className={`inline-flex ${g.line ? "w-full" : ""}`}
          aria-hidden="true"
        >
          <span className="inline-flex overflow-hidden" style={{ paddingBottom: "0.05em" }}>
            {g.cells.map((cell, ci) => {
              const d = getDelay(cellIdx++);
              const translate = reverse ? "-110%" : "110%";
              return (
                <span
                  key={ci}
                  className="inline-block"
                  style={{ overflow: "hidden" }}
                >
                  <span
                    className="inline-block"
                    style={{
                      transform: active ? "translateY(0)" : `translateY(${translate})`,
                      transition: `transform ${duration}s ${easing} ${delay + d}s`,
                      willChange: "transform",
                    }}
                  >
                    {cell === " " ? " " : cell}
                  </span>
                </span>
              );
            })}
          </span>
          {g.space && <span>&nbsp;</span>}
        </span>
      ))}
    </span>
  );
}

// Expose to window so sections.jsx can reference it without ES module imports
window.VerticalCutReveal = VerticalCutReveal;
