/* global React */
/* ============================================================
   Willow Initiative — generative art layer
   The willow is one organism: it sprouts from the broken capsule
   in the hero, and roots into four sectors further down the page.
   ============================================================ */

/* Deterministic PRNG so the tree is identical on every render. */
function seeded(seed) {
  let s = seed;
  return () => {
    s = (s * 1664525 + 1013904223) % 4294967296;
    return s / 4294967296;
  };
}

/* ---------- Recursive willow branch generator ---------- */
function growWillow({ seed = 7, x, y, angle = -90, len = 150, depth = 0, maxDepth = 5, droop = 0.5 }) {
  const rnd = seeded(seed);
  const paths = [];
  const dots = [];

  const walk = (x0, y0, ang, length, d) => {
    if (d > maxDepth || length < 7) return;
    const rad = (ang * Math.PI) / 180;
    // Willows droop: the further out, the more gravity bends the tip down.
    const sag = droop * d * length * 0.16;
    const x1 = x0 + Math.cos(rad) * length;
    const y1 = y0 + Math.sin(rad) * length + sag;
    const cx = x0 + Math.cos(rad) * length * 0.5 - Math.sin(rad) * length * 0.12;
    const cy = y0 + Math.sin(rad) * length * 0.5 + sag * 0.35;

    paths.push({
      d: `M${x0.toFixed(1)},${y0.toFixed(1)} Q${cx.toFixed(1)},${cy.toFixed(1)} ${x1.toFixed(1)},${y1.toFixed(1)}`,
      w: Math.max(0.5, 3.8 - d * 0.8),
      depth: d,
      len: length + sag,
    });

    // At the outermost branches a willow stops branching and starts hanging:
    // long, near-vertical strands with pale stipple riding down them.
    if (d === maxDepth) {
      const strands = 1 + Math.round(rnd());
      for (let sIdx = 0; sIdx < strands; sIdx++) {
        const sx = x1 + (rnd() - 0.5) * 26;
        const sy = y1 + (rnd() - 0.5) * 10;
        const drop = length * (2.1 + rnd() * 1.5);
        const ex = sx + (rnd() - 0.5) * 22;
        const ey = sy + drop;
        const qx = sx + (rnd() - 0.5) * 30;
        const qy = sy + drop * 0.42;
        paths.push({
          d: `M${sx.toFixed(1)},${sy.toFixed(1)} Q${qx.toFixed(1)},${qy.toFixed(1)} ${ex.toFixed(1)},${ey.toFixed(1)}`,
          w: 0.55,
          depth: d + 1,
          len: drop * 1.15,
        });
        const n = 2 + Math.round(rnd() * 2);
        for (let i = 0; i < n; i++) {
          const t = 0.3 + (i / n) * 0.7;
          dots.push({
            x: (1 - t) * (1 - t) * sx + 2 * (1 - t) * t * qx + t * t * ex,
            y: (1 - t) * (1 - t) * sy + 2 * (1 - t) * t * qy + t * t * ey,
            r: 0.55 + rnd() * 0.95,
            depth: d + 1,
          });
        }
      }
    }

    const branches = d === 0 ? 3 : rnd() > 0.35 ? 3 : 2;
    for (let i = 0; i < branches; i++) {
      const spread = 46 - d * 4;
      const nextAng = ang + (i - (branches - 1) / 2) * spread + (rnd() - 0.5) * 16;
      walk(x1, y1, nextAng, length * (0.66 + rnd() * 0.14), d + 1);
    }
  };

  walk(x, y, angle, len, depth);
  return { paths, dots };
}

/* ---------- Photoreal-ish capsule, snapped in half ---------- */
/* Half a capsule reads as a capsule only if the proportions hold:
   ~152 wide to ~190 tall for the surviving half, tilted a few degrees,
   with a torn edge rather than a clean rim (otherwise it reads as a cup). */
const CAP = { x0: 52, x1: 204, cut: 258, bottom: 452, r: 76 };

const tornEdge = () => {
  const rnd = seeded(404);
  const pts = [];
  const steps = 22;
  for (let i = 0; i <= steps; i++) {
    const t = i / steps;
    const x = CAP.x0 + t * (CAP.x1 - CAP.x0);
    // ellipse arc across the open mouth + a shallow random tear
    const arc = Math.sin(Math.PI * t) * 13;
    const tear = (rnd() - 0.5) * 9 * Math.sin(Math.PI * t);
    pts.push([x.toFixed(1), (CAP.cut - arc + tear).toFixed(1)]);
  }
  return pts;
};

const Capsule = ({ id = "cap" }) => {
  const edge = React.useMemo(tornEdge, []);
  const edgeD = "M" + edge.map(p => p.join(",")).join(" L");
  const bodyD = `${edgeD} L${CAP.x1},${CAP.bottom - CAP.r} A${CAP.r},${CAP.r} 0 0 1 ${CAP.x0},${CAP.bottom - CAP.r} Z`;

  return (
    <g transform="rotate(-7 128 350)">
      <defs>
        <linearGradient id={`${id}-body`} x1="0" y1="0" x2="1" y2="0">
          <stop offset="0%"   stopColor="#05130E" />
          <stop offset="10%"  stopColor="#12503C" />
          <stop offset="30%"  stopColor="#2E9C74" />
          <stop offset="45%"  stopColor="#A8E3C8" />
          <stop offset="56%"  stopColor="#3A9E79" />
          <stop offset="80%"  stopColor="#0E3B2C" />
          <stop offset="100%" stopColor="#04120D" />
        </linearGradient>
        <linearGradient id={`${id}-rim`} x1="0" y1="0" x2="1" y2="0">
          <stop offset="0%"   stopColor="rgba(242,238,225,0)" />
          <stop offset="45%"  stopColor="rgba(242,238,225,0.8)" />
          <stop offset="100%" stopColor="rgba(242,238,225,0)" />
        </linearGradient>
        <radialGradient id={`${id}-spec`} cx="0.5" cy="0.5" r="0.5">
          <stop offset="0%"   stopColor="rgba(255,255,255,0.95)" />
          <stop offset="100%" stopColor="rgba(255,255,255,0)" />
        </radialGradient>
        <filter id={`${id}-grain`}>
          <feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="3" result="n" />
          <feColorMatrix in="n" type="saturate" values="0" />
          <feComponentTransfer><feFuncA type="linear" slope="0.12" /></feComponentTransfer>
          <feComposite operator="in" in2="SourceGraphic" />
        </filter>
        <filter id={`${id}-soft`} x="-40%" y="-40%" width="180%" height="180%">
          <feGaussianBlur stdDeviation="10" />
        </filter>
      </defs>

      {/* cast shadow */}
      <ellipse cx="128" cy="468" rx="88" ry="13" fill="#000" opacity="0.5" filter={`url(#${id}-soft)`} />

      {/* body */}
      <path d={bodyD} fill={`url(#${id}-body)`} />
      <path d={bodyD} fill="#fff" filter={`url(#${id}-grain)`} opacity="0.55" />

      {/* the capsule's overlap band — the tell that this is a two-piece capsule */}
      <path
        d={`M${CAP.x0 + 3},${CAP.cut + 46} L${CAP.x1 - 3},${CAP.cut + 46}`}
        stroke="rgba(4,18,13,0.5)" strokeWidth="2.5" fill="none"
      />

      {/* speculars */}
      <rect x="82" y="272" width="15" height="150" rx="7.5" fill={`url(#${id}-spec)`} opacity="0.8" />
      <rect x="170" y="280" width="6" height="120" rx="3" fill="rgba(255,255,255,0.2)" />

      {/* torn mouth: dark interior + lit edge */}
      <path d={`${edgeD} L${CAP.x1},${CAP.cut + 16} L${CAP.x0},${CAP.cut + 16} Z`} fill="#020C08" opacity="0.95" />
      <path d={edgeD} fill="none" stroke={`url(#${id}-rim)`} strokeWidth="2.4" strokeLinejoin="round" />

      {/* powder lifting out of the break */}
      {Array.from({ length: 30 }).map((_, i) => {
        const r = seeded(91 + i * 13);
        const a = r() * Math.PI * 2;
        const d = 10 + r() * 66;
        return (
          <circle
            key={i}
            cx={128 + Math.cos(a) * d}
            cy={CAP.cut - 8 - Math.abs(Math.sin(a)) * 30 - r() * 18}
            r={0.6 + r() * 1.7}
            fill="#E8F0A4"
            opacity={0.2 + r() * 0.5}
          />
        );
      })}
    </g>
  );
};

/* ---------- Hero art: capsule + willow ---------- */
const HeroWillow = () => {
  const tree = React.useMemo(
    () => growWillow({ seed: 21, x: 128, y: 252, angle: -88, len: 88, maxDepth: 4, droop: 0.85 }),
    []
  );
  return (
    <div className="hero-stage" aria-hidden="true">
      <svg viewBox="0 0 256 500" preserveAspectRatio="xMidYMax meet" role="presentation">
        {/* branches draw from the trunk outward */}
        <g fill="none" stroke="#E8F0A4" strokeLinecap="round">
          {tree.paths.map((p, i) => (
            <path
              key={i}
              className="grow-path"
              d={p.d}
              strokeWidth={p.w}
              opacity={0.92 - p.depth * 0.1}
              style={{
                "--len": p.len * 1.6,
                animationDelay: `${0.35 + p.depth * 0.34}s`,
                animationDuration: `${1.1 + p.depth * 0.16}s`,
              }}
            />
          ))}
        </g>
        <g>
          {tree.dots.map((d, i) => (
            <circle
              key={i}
              className="leaf-dot"
              cx={d.x} cy={d.y} r={d.r}
              fill="#E8F0A4"
              opacity={0.45}
              style={{ animationDelay: `${0.7 + d.depth * 0.34 + (i % 9) * 0.02}s` }}
            />
          ))}
        </g>
        <Capsule />
      </svg>
    </div>
  );
};

/* ---------- Scroll progress within an element ---------- */
function useScrollProgress(ref) {
  const [p, setP] = React.useState(0);
  React.useEffect(() => {
    let ticking = false;
    let last = -1;
    const measure = () => {
      ticking = false;
      const el = ref.current;
      if (!el) return;
      const r = el.getBoundingClientRect();
      const total = r.height - window.innerHeight;
      let next = total <= 0 ? (r.top <= 0 ? 1 : 0) : Math.min(1, Math.max(0, -r.top / total));
      // Quantise: the tree redraws hundreds of paths, so only re-render on
      // meaningful movement rather than on every scroll event.
      next = Math.round(next * 60) / 60;
      if (next !== last) { last = next; setP(next); }
    };
    const onScroll = () => {
      if (ticking) return;
      ticking = true;
      requestAnimationFrame(measure);
    };
    measure();
    window.addEventListener("scroll", onScroll, { passive: true });
    window.addEventListener("resize", onScroll);
    return () => {
      window.removeEventListener("scroll", onScroll);
      window.removeEventListener("resize", onScroll);
    };
  }, [ref]);
  return p;
}

/* ---------- Reveal on scroll ---------- */
const JReveal = ({ children, delay = 0, className = "", as: As = "div", ...rest }) => {
  const ref = React.useRef(null);
  const [seen, setSeen] = React.useState(false);
  React.useEffect(() => {
    const el = ref.current;
    if (!el) return;
    const io = new IntersectionObserver(
      ([e]) => { if (e.isIntersecting) { setSeen(true); io.disconnect(); } },
      { threshold: 0.15, rootMargin: "0px 0px -8% 0px" }
    );
    io.observe(el);
    return () => io.disconnect();
  }, []);
  return (
    <As ref={ref} className={`j-reveal ${seen ? "in" : ""} ${className}`} style={{ transitionDelay: `${delay}ms`, ...(rest.style || {}) }} {...rest}>
      {children}
    </As>
  );
};

/* ---------- Count-up number ---------- */
const CountUp = ({ to, duration = 1900, prefix = "", suffix = "", decimals = 0 }) => {
  const ref = React.useRef(null);
  const [val, setVal] = React.useState(0);
  React.useEffect(() => {
    const el = ref.current;
    if (!el) return;
    if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) { setVal(to); return; }
    let raf;
    const io = new IntersectionObserver(([e]) => {
      if (!e.isIntersecting) return;
      io.disconnect();
      const t0 = performance.now();
      const tick = (t) => {
        const k = Math.min(1, (t - t0) / duration);
        const eased = 1 - Math.pow(1 - k, 4);
        setVal(to * eased);
        if (k < 1) raf = requestAnimationFrame(tick);
      };
      raf = requestAnimationFrame(tick);
    }, { threshold: 0.4 });
    io.observe(el);
    return () => { io.disconnect(); cancelAnimationFrame(raf); };
  }, [to, duration]);
  const shown = decimals
    ? val.toFixed(decimals)
    : Math.round(val).toLocaleString();
  return <span ref={ref}>{prefix}{shown}{suffix}</span>;
};

/* ---------- Typed line ---------- */
const Typed = ({ parts, speed = 46, startDelay = 300 }) => {
  const ref = React.useRef(null);
  const [n, setN] = React.useState(0);
  const [go, setGo] = React.useState(false);
  const full = React.useMemo(() => parts.map(p => p.t).join(""), [parts]);

  React.useEffect(() => {
    const el = ref.current;
    if (!el) return;
    const io = new IntersectionObserver(([e]) => { if (e.isIntersecting) { setGo(true); io.disconnect(); } }, { threshold: 0.35 });
    io.observe(el);
    return () => io.disconnect();
  }, []);

  React.useEffect(() => {
    if (!go) return;
    if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) { setN(full.length); return; }
    let i = 0, timer;
    const start = setTimeout(function step() {
      i += 1;
      setN(i);
      if (i < full.length) timer = setTimeout(step, speed);
    }, startDelay);
    return () => { clearTimeout(start); clearTimeout(timer); };
  }, [go, full, speed, startDelay]);

  // Re-split the typed prefix back across the styled parts.
  let left = n;
  const out = parts.map((p, i) => {
    const take = Math.max(0, Math.min(p.t.length, left));
    left -= take;
    const txt = p.t.slice(0, take);
    return p.em ? <em key={i}>{txt}</em> : <React.Fragment key={i}>{txt}</React.Fragment>;
  });

  return (
    <span ref={ref}>
      {out}
      {n < full.length && <i className="caret" />}
    </span>
  );
};

window.growWillow = growWillow;
window.HeroWillow = HeroWillow;
window.useScrollProgress = useScrollProgress;
window.JReveal = JReveal;
window.CountUp = CountUp;
window.Typed = Typed;
