// Hero orb: a point-cloud sphere (the model) inside an orbiting ring (the guard),
// drawn with raw WebGL. It tilts toward the cursor, points near the cursor push
// outward and turn pink, and scrolling out of the hero disperses it.

const ORB_VERT = `
precision highp float;
attribute vec3 aPos;
attribute float aSeed;
attribute float aKind;           // 0 = sphere point, 1 = ring point
uniform float uTime;
uniform vec2 uMouse;             // NDC
uniform vec2 uTilt;
uniform float uScroll;
uniform float uAspect;
uniform float uDpr;
uniform float uOffsetX;
uniform float uOffsetY;
uniform float uScale;
varying float vPush;
varying float vDepth;
varying float vKind;
varying float vSeed;

vec3 mod289(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
vec4 mod289(vec4 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
vec4 permute(vec4 x) { return mod289(((x * 34.0) + 1.0) * x); }
vec4 taylorInvSqrt(vec4 r) { return 1.79284291400159 - 0.85373472095314 * r; }
float snoise(vec3 v) {
  const vec2 C = vec2(1.0 / 6.0, 1.0 / 3.0);
  const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);
  vec3 i = floor(v + dot(v, C.yyy));
  vec3 x0 = v - i + dot(i, C.xxx);
  vec3 g = step(x0.yzx, x0.xyz);
  vec3 l = 1.0 - g;
  vec3 i1 = min(g.xyz, l.zxy);
  vec3 i2 = max(g.xyz, l.zxy);
  vec3 x1 = x0 - i1 + C.xxx;
  vec3 x2 = x0 - i2 + C.yyy;
  vec3 x3 = x0 - D.yyy;
  i = mod289(i);
  vec4 p = permute(permute(permute(
    i.z + vec4(0.0, i1.z, i2.z, 1.0)) + i.y + vec4(0.0, i1.y, i2.y, 1.0)) + i.x + vec4(0.0, i1.x, i2.x, 1.0));
  float n_ = 0.142857142857;
  vec3 ns = n_ * D.wyz - D.xzx;
  vec4 j = p - 49.0 * floor(p * ns.z * ns.z);
  vec4 x_ = floor(j * ns.z);
  vec4 y_ = floor(j - 7.0 * x_);
  vec4 x = x_ * ns.x + ns.yyyy;
  vec4 y = y_ * ns.x + ns.yyyy;
  vec4 h = 1.0 - abs(x) - abs(y);
  vec4 b0 = vec4(x.xy, y.xy);
  vec4 b1 = vec4(x.zw, y.zw);
  vec4 s0 = floor(b0) * 2.0 + 1.0;
  vec4 s1 = floor(b1) * 2.0 + 1.0;
  vec4 sh = -step(h, vec4(0.0));
  vec4 a0 = b0.xzyw + s0.xzyw * sh.xxyy;
  vec4 a1 = b1.xzyw + s1.xzyw * sh.zzww;
  vec3 p0 = vec3(a0.xy, h.x);
  vec3 p1 = vec3(a0.zw, h.y);
  vec3 p2 = vec3(a1.xy, h.z);
  vec3 p3 = vec3(a1.zw, h.w);
  vec4 norm = taylorInvSqrt(vec4(dot(p0, p0), dot(p1, p1), dot(p2, p2), dot(p3, p3)));
  p0 *= norm.x; p1 *= norm.y; p2 *= norm.z; p3 *= norm.w;
  vec4 m = max(0.6 - vec4(dot(x0, x0), dot(x1, x1), dot(x2, x2), dot(x3, x3)), 0.0);
  m = m * m;
  return 42.0 * dot(m * m, vec4(dot(p0, x0), dot(p1, x1), dot(p2, x2), dot(p3, x3)));
}

mat3 rotY(float a) { float c = cos(a), s = sin(a); return mat3(c, 0.0, -s, 0.0, 1.0, 0.0, s, 0.0, c); }
mat3 rotX(float a) { float c = cos(a), s = sin(a); return mat3(1.0, 0.0, 0.0, 0.0, c, s, 0.0, -s, c); }
mat3 rotZ(float a) { float c = cos(a), s = sin(a); return mat3(c, s, 0.0, -s, c, 0.0, 0.0, 0.0, 1.0); }

void main() {
  vec3 p = aPos;
  if (aKind < 0.5) {
    float n = snoise(p * 1.5 + vec3(0.0, uTime * 0.18, uTime * 0.07));
    p *= 1.0 + n * 0.16;
    p *= 1.0 + uScroll * (0.6 + aSeed * 2.2);
    p = rotY(uTime * 0.07) * p;
  } else {
    p = rotZ(0.42) * rotX(1.18) * rotY(uTime * 0.35) * p;
    p *= 1.0 + uScroll * 1.4;
  }
  p = rotX(uTilt.y) * rotY(uTilt.x) * p;
  p *= uScale;

  vec3 view = p + vec3(uOffsetX, uOffsetY, -3.4);
  float f = 1.9;
  vec4 pos = vec4(view.x * f / uAspect, view.y * f, 0.0, -view.z);

  vec2 ndc = pos.xy / pos.w;
  vec2 d = (ndc - uMouse) * vec2(uAspect, 1.0);
  float dist = length(d);
  float push = smoothstep(0.42, 0.0, dist);
  pos.xy += normalize(d + 0.0001) / vec2(uAspect, 1.0) * push * 0.22 * pos.w;

  gl_Position = pos;
  float size = aKind < 0.5 ? (1.4 + aSeed * 2.4) : (1.2 + aSeed * 1.2);
  gl_PointSize = size * uDpr * (3.4 / pos.w) * (1.0 + push * 1.4);
  vPush = push;
  vDepth = clamp((-view.z - 2.2) / 2.4, 0.0, 1.0);
  vKind = aKind;
  vSeed = aSeed;
}
`;

const ORB_FRAG = `
precision highp float;
uniform float uScroll;
varying float vPush;
varying float vDepth;
varying float vKind;
varying float vSeed;
void main() {
  float r = length(gl_PointCoord - 0.5);
  float a = smoothstep(0.5, 0.05, r);
  vec3 blue = vec3(0.239, 0.353, 0.996);
  vec3 ice = vec3(0.91, 0.925, 1.0);
  vec3 pink = vec3(1.0, 0.239, 0.604);
  vec3 col = mix(ice, blue, vDepth * 0.9);
  if (vKind > 0.5) col = mix(ice, vec3(0.5, 0.58, 1.0), vSeed);
  col = mix(col, pink, vPush);
  float alpha = a * mix(0.95, 0.25, vDepth) * (1.0 - uScroll * 0.85);
  gl_FragColor = vec4(col * alpha, alpha);
}
`;

function buildOrbGeometry(sphereCount, ringCount) {
  const total = sphereCount + ringCount;
  const pos = new Float32Array(total * 3);
  const seed = new Float32Array(total);
  const kind = new Float32Array(total);
  const golden = Math.PI * (3 - Math.sqrt(5));
  for (let i = 0; i < sphereCount; i++) {
    const y = 1 - (i / (sphereCount - 1)) * 2;
    const r = Math.sqrt(1 - y * y);
    const t = golden * i;
    pos.set([Math.cos(t) * r, y, Math.sin(t) * r], i * 3);
    seed[i] = Math.random();
  }
  for (let j = 0; j < ringCount; j++) {
    const i = sphereCount + j;
    const t = Math.random() * Math.PI * 2;
    const rad = 1.55 + (Math.random() - 0.5) * 0.08;
    pos.set([Math.cos(t) * rad, (Math.random() - 0.5) * 0.03, Math.sin(t) * rad], i * 3);
    seed[i] = Math.random();
    kind[i] = 1;
  }
  return { pos, seed, kind, total };
}

function Orb({ scrollTarget }) {
  const canvasRef = React.useRef(null);

  React.useEffect(() => {
    const canvas = canvasRef.current;
    const gl = canvas.getContext('webgl', { antialias: false, alpha: true, premultipliedAlpha: true });
    if (!gl) { canvas.hidden = true; return; }

    const compile = (type, src) => {
      const s = gl.createShader(type);
      gl.shaderSource(s, src);
      gl.compileShader(s);
      if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(s));
      return s;
    };
    let program;
    try {
      program = gl.createProgram();
      gl.attachShader(program, compile(gl.VERTEX_SHADER, ORB_VERT));
      gl.attachShader(program, compile(gl.FRAGMENT_SHADER, ORB_FRAG));
      gl.linkProgram(program);
      if (!gl.getProgramParameter(program, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(program));
    } catch (err) {
      console.warn('Orb disabled:', err);
      canvas.hidden = true;
      return;
    }
    gl.useProgram(program);

    const small = window.matchMedia('(max-width: 900px)').matches;
    const geo = buildOrbGeometry(small ? 3200 : 7000, small ? 700 : 1400);
    const attr = (name, data, size) => {
      const buf = gl.createBuffer();
      gl.bindBuffer(gl.ARRAY_BUFFER, buf);
      gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
      const loc = gl.getAttribLocation(program, name);
      gl.enableVertexAttribArray(loc);
      gl.vertexAttribPointer(loc, size, gl.FLOAT, false, 0, 0);
    };
    attr('aPos', geo.pos, 3);
    attr('aSeed', geo.seed, 1);
    attr('aKind', geo.kind, 1);

    const u = {};
    ['uTime', 'uMouse', 'uTilt', 'uScroll', 'uAspect', 'uDpr', 'uOffsetX', 'uOffsetY', 'uScale']
      .forEach(n => { u[n] = gl.getUniformLocation(program, n); });

    gl.enable(gl.BLEND);
    gl.blendFunc(gl.ONE, gl.ONE);
    gl.clearColor(0, 0, 0, 0);

    const dpr = Math.min(window.devicePixelRatio || 1, 2);
    let aspect = 1, offsetX = 0, offsetY = 0, scale = 1;
    const resize = () => {
      const r = canvas.getBoundingClientRect();
      canvas.width = Math.round(r.width * dpr);
      canvas.height = Math.round(r.height * dpr);
      gl.viewport(0, 0, canvas.width, canvas.height);
      aspect = r.width / Math.max(1, r.height);
      const narrow = r.width < 900;
      // on narrow screens: shrink to fit the width and sit lower, behind the name
      offsetX = narrow ? 0 : Math.min(1.35, aspect * 0.62);
      offsetY = narrow ? -0.55 : 0;
      scale = narrow ? Math.min(1, aspect * 1.35) : 1;
    };
    resize();
    window.addEventListener('resize', resize);

    // mouse: target vs. eased current
    const mouse = { x: 5, y: 5, tx: 5, ty: 5 };
    const tilt = { x: 0, y: 0, tx: 0, ty: 0 };
    const onMove = (e) => {
      const r = canvas.getBoundingClientRect();
      mouse.tx = ((e.clientX - r.left) / r.width) * 2 - 1;
      mouse.ty = -(((e.clientY - r.top) / r.height) * 2 - 1);
      tilt.tx = mouse.tx * 0.5;
      tilt.ty = -mouse.ty * 0.35;
    };
    const onLeave = () => { mouse.tx = 5; mouse.ty = 5; tilt.tx = 0; tilt.ty = 0; };
    window.addEventListener('pointermove', onMove, { passive: true });
    document.addEventListener('pointerleave', onLeave);

    let scroll = 0;
    const gsap = getGsap();
    let trigger = null;
    if (gsap && window.ScrollTrigger && scrollTarget && scrollTarget.current) {
      trigger = window.ScrollTrigger.create({
        trigger: scrollTarget.current,
        start: 'top top',
        end: 'bottom top',
        scrub: true,
        onUpdate: self => { scroll = self.progress; }
      });
    }

    const reduced = prefersReducedMotion();
    let visible = true;
    const io = new IntersectionObserver(([e]) => { visible = e.isIntersecting; });
    io.observe(canvas);

    let raf = 0;
    const start = performance.now();
    const draw = (now) => {
      const t = reduced ? 12 : (now - start) / 1000;
      mouse.x += (mouse.tx - mouse.x) * 0.12;
      mouse.y += (mouse.ty - mouse.y) * 0.12;
      tilt.x += (tilt.tx - tilt.x) * 0.05;
      tilt.y += (tilt.ty - tilt.y) * 0.05;
      gl.clear(gl.COLOR_BUFFER_BIT);
      gl.uniform1f(u.uTime, t);
      gl.uniform2f(u.uMouse, mouse.x, mouse.y);
      gl.uniform2f(u.uTilt, tilt.x, tilt.y);
      gl.uniform1f(u.uScroll, scroll);
      gl.uniform1f(u.uAspect, aspect);
      gl.uniform1f(u.uDpr, dpr);
      gl.uniform1f(u.uOffsetX, offsetX);
      gl.uniform1f(u.uOffsetY, offsetY);
      gl.uniform1f(u.uScale, scale);
      gl.drawArrays(gl.POINTS, 0, geo.total);
    };
    const loop = (now) => {
      if (visible) draw(now);
      raf = requestAnimationFrame(loop);
    };
    raf = requestAnimationFrame(loop);

    return () => {
      cancelAnimationFrame(raf);
      io.disconnect();
      if (trigger) trigger.kill();
      window.removeEventListener('resize', resize);
      window.removeEventListener('pointermove', onMove);
      document.removeEventListener('pointerleave', onLeave);
      const ext = gl.getExtension('WEBGL_lose_context');
      if (ext) ext.loseContext();
    };
  }, []);

  return <canvas ref={canvasRef} className="orb" aria-hidden="true"></canvas>;
}

Object.assign(window, { Orb });
