// fx.jsx — motion & visual-effects components for the ReturnScribe site.
// Vanilla-React adaptations of: container-scroll-animation, animated blur
// numbers, display cards, 3D mouse tilt (splite-style), expandable tabs,
// a WebGL shader backdrop, and animated background paths — all re-skinned
// to the app's Ledger palette.

// ── helpers ──────────────────────────────────────────────────────
const fxClamp = (v, a, b) => Math.max(a, Math.min(b, v));
const fxMotionOK = () => !document.body.classList.contains('motion-calm') &&
  !(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches);

// ── WebGL shader backdrop (web-gl-shader) ────────────────────────
// Slow-drifting warm ink plasma in the theme palette. Falls back to nothing
// (the CSS halo remains) if WebGL is unavailable; renders a single static
// frame under reduced motion.
function ShaderBackdrop({ dark = true, accent = '#F0653A', opacity = 0.5 }) {
  const ref = React.useRef(null);
  React.useEffect(() => {
    const canvas = ref.current;
    if (!canvas) return;
    const gl = canvas.getContext('webgl', { alpha: true, antialias: false });
    if (!gl) return;

    const hexToVec = (hex) => {
      const n = parseInt(hex.slice(1), 16);
      return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255];
    };
    const vsrc = 'attribute vec2 p; void main(){ gl_Position = vec4(p, 0.0, 1.0); }';
    const fsrc = `
      precision mediump float;
      uniform float u_t; uniform vec2 u_r;
      uniform vec3 u_c1; uniform vec3 u_c2; uniform float u_dark;
      float wave(vec2 uv, float t){
        return sin(uv.x*2.6 + t*0.28) * cos(uv.y*3.1 - t*0.21)
             + 0.55*sin((uv.x+uv.y)*4.2 + t*0.36)
             + 0.35*cos(length(uv-vec2(0.62,0.38))*7.0 - t*0.4);
      }
      void main(){
        vec2 uv = gl_FragCoord.xy / u_r;
        float n = wave(uv, u_t);
        float g1 = smoothstep(-1.4, 1.6, n);
        float g2 = smoothstep(0.4, 1.8, n);
        vec3 col = mix(u_c1 * (u_dark > 0.5 ? 0.5 : 0.9), u_c2 * 0.8, g2) * g1;
        float a = g1 * (u_dark > 0.5 ? 0.34 : 0.16);
        gl_FragColor = vec4(col, a);
      }`;
    const mk = (type, src) => {
      const s = gl.createShader(type);
      gl.shaderSource(s, src); gl.compileShader(s);
      return s;
    };
    const prog = gl.createProgram();
    gl.attachShader(prog, mk(gl.VERTEX_SHADER, vsrc));
    gl.attachShader(prog, mk(gl.FRAGMENT_SHADER, fsrc));
    gl.linkProgram(prog);
    if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) return;
    gl.useProgram(prog);
    const buf = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, buf);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1, 3,-1, -1,3]), gl.STATIC_DRAW);
    const loc = gl.getAttribLocation(prog, 'p');
    gl.enableVertexAttribArray(loc);
    gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);
    const uT = gl.getUniformLocation(prog, 'u_t');
    const uR = gl.getUniformLocation(prog, 'u_r');
    gl.uniform3fv(gl.getUniformLocation(prog, 'u_c1'), hexToVec(accent));
    gl.uniform3fv(gl.getUniformLocation(prog, 'u_c2'), hexToVec(dark ? '#7DBE6E' : '#3A7D3E'));
    gl.uniform1f(gl.getUniformLocation(prog, 'u_dark'), dark ? 1 : 0);
    gl.enable(gl.BLEND);
    gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);

    let raf = 0, running = true, visible = true;
    const resize = () => {
      const w = canvas.clientWidth, h = canvas.clientHeight;
      canvas.width = Math.max(2, Math.floor(w * 0.5));   // half-res, it's a blur anyway
      canvas.height = Math.max(2, Math.floor(h * 0.5));
      gl.viewport(0, 0, canvas.width, canvas.height);
      gl.uniform2f(uR, canvas.width, canvas.height);
    };
    resize();
    window.addEventListener('resize', resize);
    const io = new IntersectionObserver((es) => { visible = es[0].isIntersecting; });
    io.observe(canvas);
    const draw = (now) => {
      if (!running) return;
      if (visible && !document.hidden) {
        gl.uniform1f(uT, now / 1000);
        gl.drawArrays(gl.TRIANGLES, 0, 3);
      }
      if (fxMotionOK()) raf = requestAnimationFrame(draw);
    };
    raf = requestAnimationFrame(draw);
    return () => { running = false; cancelAnimationFrame(raf); io.disconnect(); window.removeEventListener('resize', resize); };
  }, [dark, accent]);
  return <canvas ref={ref} className="shader-backdrop" style={{ opacity }} aria-hidden="true"></canvas>;
}

// ── Animated background paths (background-paths) ─────────────────
function FlowPaths({ count = 18, flip = false, global = false }) {
  const boost = global ? 2.4 : 1;
  // long, thin curves fanning from the top-left corner across the whole page
  const paths = Array.from({ length: count }, (_, i) => ({
    d: `M${-80 - i * 7} ${-40 + i * 13}C${380 - i * 6} ${110 + i * 15} ${860 + i * 4} ${360 + i * 12} ${1720 + i * 6} ${640 + i * 15}`,
    w: 0.8 + i * 0.03,
    o: Math.min(0.3, (0.05 + i * 0.008) * boost),
    dur: 24 + i * 1.4,
  }));
  return (
    <div className={`flow-paths ${flip ? 'flip' : ''} ${global ? 'global' : ''}`} aria-hidden="true">
      <svg viewBox="0 0 1600 1000" fill="none" preserveAspectRatio="none">
        {paths.map((p, i) => (
          <path key={i} d={p.d} stroke="var(--accent)" strokeWidth={p.w} strokeOpacity={p.o}
            strokeLinecap="round" vectorEffect="non-scaling-stroke"
            style={{
              strokeDasharray: '90 600',
              animation: `fxDash ${p.dur}s linear infinite`,
              animationDelay: `${-i * 1.8}s`,
            }}/>
        ))}
      </svg>
    </div>
  );
}

// ── Container scroll animation (container-scroll-animation) ──────
// Children sit in a 3D card that starts tilted back and straightens
// as the section scrolls through the viewport.
function ContainerScroll({ header, children }) {
  const wrapRef = React.useRef(null);
  const stageRef = React.useRef(null);
  const cardRef = React.useRef(null);
  const headRef = React.useRef(null);

  React.useEffect(() => {
    let raf = 0;
    const reveal = { v: 0, playing: false };
    const update = () => {
      const stage = stageRef.current;
      if (!stage) return;
      const vh = window.innerHeight;
      const r = stage.getBoundingClientRect();
      const start = vh, end = vh * 0.3;
      const scrollP = fxClamp((start - r.top) / (start - end), 0, 1);
      // effective progress: even if the user jumps straight here (anchor link),
      // the reveal timeline still plays the tilt → flat entrance.
      const p = Math.min(scrollP, reveal.v);
      const ok = fxMotionOK();
      const rot = ok ? (1 - p) * 26 : 0;
      const scale = ok ? 1.05 - 0.05 * p : 1;
      const ty = ok ? (1 - p) * -34 : 0;
      if (cardRef.current) cardRef.current.style.transform = `rotateX(${rot}deg) scale(${scale})`;
      if (headRef.current) headRef.current.style.transform = `translateY(${ty}px)`;
    };
    const playReveal = () => {
      if (reveal.playing || reveal.v >= 1) return;
      reveal.playing = true;
      const t0 = performance.now();
      const dur = 1100;
      const tick = (now) => {
        const q = fxClamp((now - t0) / dur, 0, 1);
        reveal.v = 1 - Math.pow(1 - q, 3);
        update();
        if (q < 1) requestAnimationFrame(tick);
      };
      requestAnimationFrame(tick);
    };
    const io = new IntersectionObserver((es) => {
      es.forEach((e) => { if (e.isIntersecting) playReveal(); });
    }, { threshold: 0.12 });
    if (stageRef.current) io.observe(stageRef.current);
    const onScroll = () => { cancelAnimationFrame(raf); raf = requestAnimationFrame(update); };
    update();
    window.addEventListener('scroll', onScroll, { passive: true });
    window.addEventListener('resize', onScroll);
    return () => {
      window.removeEventListener('scroll', onScroll);
      window.removeEventListener('resize', onScroll);
      cancelAnimationFrame(raf);
      io.disconnect();
    };
  }, []);

  return (
    <div className="cscroll" ref={wrapRef}>
      <div className="cscroll-head" ref={headRef}>{header}</div>
      <div className="cscroll-stage" ref={stageRef}>
        <div className="cscroll-card" ref={cardRef}>{children}</div>
      </div>
    </div>
  );
}

// ── Animated blur number (animated-blur-number) ──────────────────
// Counts to `value` when scrolled into view; digits de-blur as they settle.
// Re-blurs briefly whenever `value` changes (e.g. pricing toggle).
function BlurNumber({ value, decimals = 0, prefix = '', duration = 1300, className = '' }) {
  const ref = React.useRef(null);
  const [started, setStarted] = React.useState(false);
  const [disp, setDisp] = React.useState(0);
  const [settled, setSettled] = React.useState(false);

  React.useEffect(() => {
    const io = new IntersectionObserver((es) => {
      es.forEach((e) => { if (e.isIntersecting) { setStarted(true); io.disconnect(); } });
    }, { threshold: 0.4 });
    if (ref.current) io.observe(ref.current);
    return () => io.disconnect();
  }, []);

  React.useEffect(() => {
    if (!started) return;
    if (!fxMotionOK()) { setDisp(value); setSettled(true); return; }
    setSettled(false);
    const from = disp;
    const t0 = performance.now();
    let raf;
    const tick = (now) => {
      const p = Math.min(1, (now - t0) / duration);
      const eased = 1 - Math.pow(1 - p, 3);
      setDisp(from + (value - from) * eased);
      if (p < 1) raf = requestAnimationFrame(tick);
      else setSettled(true);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [started, value]);

  const whole = Math.floor(disp).toLocaleString('en-US');
  const cents = decimals ? (disp - Math.floor(disp)).toFixed(decimals).slice(2) : null;
  return (
    <span ref={ref} className={`blur-num ${settled ? 'settled' : ''} ${className}`}>
      {prefix}{whole}{cents !== null && <span className="cents">.{cents}</span>}
    </span>
  );
}

// ── Display cards (display-cards) ────────────────────────────────
// Three stacked, fanned cards; hovering lifts each out of the stack.
function DisplayCards({ cards = [] }) {
  return (
    <div className="dcards">
      {cards.map((c, i) => (
        <div className={`dcard dcard-${i}`} key={i} style={{ '--tone': `var(--${c.tone || 'accent'})` }}>
          <div className="dcard-top">
            <span className="dcard-ic">{c.icon}</span>
            <span className="dcard-kind">{c.kind}</span>
            <span className="dcard-when">{c.when}</span>
          </div>
          <div className="dcard-title">{c.title}</div>
          <div className="dcard-sub">{c.sub}</div>
        </div>
      ))}
    </div>
  );
}

// ── Expandable tabs (expandable-tabs) ────────────────────────────
// Icon-only pills; the active one expands to reveal its label.
function ExpandableTabs({ tabs, active, onChange }) {
  return (
    <div className="xtabs" role="tablist">
      {tabs.map((tb) => {
        const on = active === tb.id;
        return (
          <button
            key={tb.id}
            role="tab"
            aria-selected={on}
            className={`xtab ${on ? 'on' : ''}`}
            onClick={() => onChange(tb.id)}
            title={tb.label}
          >
            <span className="xtab-ic">{tb.icon}</span>
            <span className="xtab-label">{tb.label}</span>
          </button>
        );
      })}
    </div>
  );
}

// ── 3D mouse tilt (splite-style interactive 3D) ──────────────────
function useMouseTilt(maxDeg = 7) {
  const outerRef = React.useRef(null);
  const innerRef = React.useRef(null);
  React.useEffect(() => {
    const outer = outerRef.current, inner = innerRef.current;
    if (!outer || !inner) return;
    let raf = 0;
    let target = { x: 0, y: 0 }, cur = { x: 0, y: 0 };
    const loop = () => {
      cur.x += (target.x - cur.x) * 0.12;
      cur.y += (target.y - cur.y) * 0.12;
      inner.style.transform = `rotateY(${cur.x}deg) rotateX(${cur.y}deg)`;
      if (Math.abs(cur.x - target.x) > 0.01 || Math.abs(cur.y - target.y) > 0.01) raf = requestAnimationFrame(loop);
      else raf = 0;
    };
    const kick = () => { if (!raf) raf = requestAnimationFrame(loop); };
    const onMove = (e) => {
      if (!fxMotionOK()) return;
      const r = outer.getBoundingClientRect();
      const nx = ((e.clientX - r.left) / r.width) * 2 - 1;
      const ny = ((e.clientY - r.top) / r.height) * 2 - 1;
      target = { x: fxClamp(nx, -1, 1) * maxDeg, y: fxClamp(-ny, -1, 1) * maxDeg };
      kick();
    };
    const onLeave = () => { target = { x: 0, y: 0 }; kick(); };
    outer.addEventListener('pointermove', onMove);
    outer.addEventListener('pointerleave', onLeave);
    return () => {
      cancelAnimationFrame(raf);
      outer.removeEventListener('pointermove', onMove);
      outer.removeEventListener('pointerleave', onLeave);
    };
  }, [maxDeg]);
  return { outerRef, innerRef };
}

Object.assign(window, {
  ShaderBackdrop, FlowPaths, ContainerScroll, BlurNumber,
  DisplayCards, ExpandableTabs, useMouseTilt,
});
