// WebGL hero — a drifting dust field in a key light, pushed around by the pointer.
// three.js is injected on mount so it never blocks first paint.

const { useEffect: useEffectGL, useRef: useRefGL, useState: useStateGL } = React;

const THREE_SRC = 'https://unpkg.com/three@0.160.1/build/three.min.js';

let threePromise = null;
function loadThree() {
  if (window.THREE) return Promise.resolve(window.THREE);
  if (threePromise) return threePromise;
  threePromise = new Promise((resolve, reject) => {
    const s = document.createElement('script');
    s.src = THREE_SRC;
    s.async = true;
    s.crossOrigin = 'anonymous';
    s.onload = () => (window.THREE ? resolve(window.THREE) : reject(new Error('three missing')));
    s.onerror = () => reject(new Error('three failed to load'));
    document.head.appendChild(s);
  });
  return threePromise;
}

const VERT = `
uniform float uTime;
uniform vec2  uPointer;
uniform float uRadius;
uniform float uStrength;
uniform float uSize;
uniform float uDpr;
attribute float aSeed;
attribute float aScale;
varying float vSeed;
varying float vGlow;

void main() {
  vec3 p = position;

  // slow rise + lateral sway, wrapped through the field height
  float rise = uTime * (0.35 + aSeed * 0.55);
  p.y = mod(p.y + rise + 40.0, 80.0) - 40.0;
  p.x += sin(uTime * 0.16 + aSeed * 11.0) * 1.35;

  // pointer pushes motes aside
  vec2 d = p.xy - uPointer;
  float f = 1.0 - smoothstep(0.0, uRadius, length(d));
  p.xy += normalize(d + vec2(0.0001)) * f * uStrength;

  vGlow = f;
  vSeed = aSeed;

  vec4 mv = modelViewMatrix * vec4(p, 1.0);
  gl_PointSize = uSize * aScale * uDpr * (30.0 / -mv.z) * (1.0 + f * 1.7);
  gl_Position = projectionMatrix * mv;
}
`;

const FRAG = `
uniform vec3  uColorA;
uniform vec3  uColorB;
uniform float uOpacity;
varying float vSeed;
varying float vGlow;

void main() {
  vec2 c = gl_PointCoord - 0.5;
  float d = length(c);
  if (d > 0.5) discard;
  float a = pow(smoothstep(0.5, 0.0, d), 2.1);
  vec3 col = mix(uColorA, uColorB, smoothstep(0.6, 1.0, vSeed));
  col = mix(col, uColorB, vGlow * 0.75);
  gl_FragColor = vec4(col, a * uOpacity * (0.3 + vSeed * 0.7) * (1.0 + vGlow * 1.4));
}
`;

function HeroCanvas() {
  const mountRef = useRefGL(null);
  const [ready, setReady] = useStateGL(false);

  useEffectGL(() => {
    const mount = mountRef.current;
    if (!mount) return;

    // Honour reduced-motion by simply not running the effect at all.
    const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;
    if (reduced) return;

    let disposed = false;
    let raf = 0;
    let renderer, scene, camera, points, geo, mat, io;

    const coarse = matchMedia('(pointer: coarse)').matches;
    const COUNT = coarse ? 900 : 2200;

    // pointer state, in world units on the z=0 plane
    const ptr = { x: 0, y: 0, tx: 0, ty: 0, active: false };
    let visibleH = 0, visibleW = 0;
    let idle = 0;

    loadThree().then((THREE) => {
      if (disposed) return;

      renderer = new THREE.WebGLRenderer({ alpha: true, antialias: false, powerPreference: 'low-power' });
      renderer.setClearColor(0x000000, 0);
      mount.appendChild(renderer.domElement);

      scene = new THREE.Scene();
      camera = new THREE.PerspectiveCamera(55, 1, 0.1, 200);
      camera.position.z = 34;

      const pos = new Float32Array(COUNT * 3);
      const seed = new Float32Array(COUNT);
      const scale = new Float32Array(COUNT);
      for (let i = 0; i < COUNT; i++) {
        pos[i * 3 + 0] = (Math.random() - 0.5) * 110;
        pos[i * 3 + 1] = (Math.random() - 0.5) * 80;
        pos[i * 3 + 2] = -32 + Math.random() * 38;
        seed[i] = Math.random();
        scale[i] = 0.45 + Math.random() * 1.15;
      }

      geo = new THREE.BufferGeometry();
      geo.setAttribute('position', new THREE.BufferAttribute(pos, 3));
      geo.setAttribute('aSeed', new THREE.BufferAttribute(seed, 1));
      geo.setAttribute('aScale', new THREE.BufferAttribute(scale, 1));

      mat = new THREE.ShaderMaterial({
        vertexShader: VERT,
        fragmentShader: FRAG,
        transparent: true,
        depthWrite: false,
        blending: THREE.AdditiveBlending,
        uniforms: {
          uTime:     { value: 0 },
          uPointer:  { value: new THREE.Vector2(0, 0) },
          uRadius:   { value: coarse ? 8.5 : 7.0 },
          uStrength: { value: coarse ? 4.0 : 3.4 },
          uSize:     { value: coarse ? 2.1 : 1.9 },
          uDpr:      { value: 1 },
          uOpacity:  { value: 0.85 },
          uColorA:   { value: new THREE.Color('#f4f1ea') },
          uColorB:   { value: new THREE.Color('#ff7a4d') },
        },
      });

      points = new THREE.Points(geo, mat);
      scene.add(points);

      const resize = () => {
        const w = mount.clientWidth || 1;
        const h = mount.clientHeight || 1;
        const dpr = Math.min(window.devicePixelRatio || 1, coarse ? 1.5 : 2);
        renderer.setPixelRatio(dpr);
        renderer.setSize(w, h, false);
        camera.aspect = w / h;
        camera.updateProjectionMatrix();
        mat.uniforms.uDpr.value = dpr;
        visibleH = 2 * Math.tan((camera.fov * Math.PI) / 360) * camera.position.z;
        visibleW = visibleH * camera.aspect;
      };
      resize();

      const ro = new ResizeObserver(resize);
      ro.observe(mount);

      // ---- input ----
      const setFromClient = (cx, cy) => {
        const r = mount.getBoundingClientRect();
        const nx = ((cx - r.left) / r.width) * 2 - 1;
        const ny = -(((cy - r.top) / r.height) * 2 - 1);
        ptr.tx = (nx * visibleW) / 2;
        ptr.ty = (ny * visibleH) / 2;
        ptr.active = true;
        idle = 0;
      };
      const onMove = (e) => setFromClient(e.clientX, e.clientY);
      const onTouch = (e) => {
        if (!e.touches || !e.touches.length) return;
        setFromClient(e.touches[0].clientX, e.touches[0].clientY);
      };
      const onTouchEnd = () => { ptr.active = false; };

      // gyroscope parallax on phones that expose it
      const onTilt = (e) => {
        if (ptr.active || e.gamma == null || e.beta == null) return;
        const g = Math.max(-45, Math.min(45, e.gamma)) / 45;
        const b = Math.max(-45, Math.min(45, e.beta - 45)) / 45;
        ptr.tx = (g * visibleW) / 3;
        ptr.ty = (-b * visibleH) / 3;
      };

      if (coarse) {
        window.addEventListener('touchstart', onTouch, { passive: true });
        window.addEventListener('touchmove', onTouch, { passive: true });
        window.addEventListener('touchend', onTouchEnd, { passive: true });
        window.addEventListener('deviceorientation', onTilt);
      } else {
        window.addEventListener('mousemove', onMove, { passive: true });
      }

      // ---- run only while the hero is on screen and the tab is visible ----
      let onScreen = true;
      let last = performance.now();
      let t = 0;

      io = new IntersectionObserver(([e]) => { onScreen = e.isIntersecting; }, { threshold: 0 });
      io.observe(mount);
      // returning to the tab shouldn't dump one huge dt into the sim
      const onVis = () => { if (!document.hidden) last = performance.now(); };
      document.addEventListener('visibilitychange', onVis);

      const tick = (now) => {
        raf = requestAnimationFrame(tick);
        const dt = Math.min((now - last) / 1000, 0.05);
        last = now;
        if (!onScreen || document.hidden) return;

        t += dt;

        // with no input, let the field breathe on its own
        if (!ptr.active) {
          idle += dt;
          if (idle > 1.2) {
            ptr.tx = Math.sin(t * 0.21) * visibleW * 0.22;
            ptr.ty = Math.cos(t * 0.17) * visibleH * 0.18;
          }
        }
        ptr.x += (ptr.tx - ptr.x) * Math.min(dt * 3.2, 1);
        ptr.y += (ptr.ty - ptr.y) * Math.min(dt * 3.2, 1);

        mat.uniforms.uTime.value = t;
        mat.uniforms.uPointer.value.set(ptr.x, ptr.y);

        // gentle counter-parallax for depth
        camera.position.x += ((ptr.x * 0.05) - camera.position.x) * Math.min(dt * 1.6, 1);
        camera.position.y += ((ptr.y * 0.05) - camera.position.y) * Math.min(dt * 1.6, 1);
        camera.lookAt(0, 0, 0);

        renderer.render(scene, camera);
      };
      raf = requestAnimationFrame(tick);
      setReady(true);

      // stash teardown for the cleanup below
      mount._teardown = () => {
        cancelAnimationFrame(raf);
        ro.disconnect();
        if (io) io.disconnect();
        document.removeEventListener('visibilitychange', onVis);
        window.removeEventListener('mousemove', onMove);
        window.removeEventListener('touchstart', onTouch);
        window.removeEventListener('touchmove', onTouch);
        window.removeEventListener('touchend', onTouchEnd);
        window.removeEventListener('deviceorientation', onTilt);
        geo.dispose();
        mat.dispose();
        renderer.dispose();
        if (renderer.domElement.parentNode) renderer.domElement.parentNode.removeChild(renderer.domElement);
      };
    }).catch(() => {
      // no WebGL / CDN blocked — the gradient wash behind is the fallback
    });

    return () => {
      disposed = true;
      cancelAnimationFrame(raf);
      if (mount._teardown) { mount._teardown(); mount._teardown = null; }
    };
  }, []);

  return <div ref={mountRef} className={`hero-canvas ${ready ? 'ready' : ''}`} aria-hidden="true"></div>;
}

Object.assign(window, { HeroCanvas });
