/* Ported from ../../main.ts (Figma WGSL shader effect) to a WebGL2
   fragment/vertex pair. The noise/domain-warp/twist math below is a 1:1
   translation of that WGSL — same constants, same function structure.
   Two things were deliberately NOT ported:
     - the WebGPU host plumbing (manual MSAA textures, bind groups, UBO
       struct packing) — WebGL2's default canvas context does MSAA and
       depth via context-creation flags, so none of that is needed here.
     - the "material" lighting models (Satin/Glossy/Metallic/Iridescent)
       — out of scope per spec; only the material===0 (no lighting) path
       is implemented, which is what makes Height/Noise/Facing possible
       without ever needing surface normals except in Facing mode.
   Mesh is a 64-subdivision cube-sphere (vs. the source's 112) — a
   deliberate resolution cut for a decorative background element; the
   per-vertex FBM math is the expensive part and this quarters it. */

const { useRef, useEffect } = React;

const GRADIENT_METHODS = { height: 0, noise: 1, facing: 2 };

function resolveGradientMethod(value) {
  if (typeof value === "number") return value;
  const key = String(value || "noise").toLowerCase();
  return GRADIENT_METHODS[key] ?? 1;
}

function hexToRgb01(hex) {
  const m = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex || "");
  if (!m) return [1, 1, 1];
  return [
    parseInt(m[1], 16) / 255,
    parseInt(m[2], 16) / 255,
    parseInt(m[3], 16) / 255,
  ];
}

/* ---------------------------------------------------------------------
   Shared GLSL: uniforms + every helper function, included verbatim in
   all four shader programs (sphere vertex/fragment, backdrop
   vertex/fragment). A function unused by a given stage is harmless dead
   code to the compiler; this keeps every stage's uniform set identical
   so nothing can go stale between them.
   --------------------------------------------------------------------- */
const GLSL_COMMON = `
uniform float u_detail;
uniform float u_time;
uniform float u_aspectRatio;
uniform float u_stopCount;
uniform vec4  u_colors[8];
uniform float u_stops[8];
uniform float u_zoom;
uniform float u_morphSpeed;
uniform float u_rotationSpeed;
uniform float u_gradientBalance;
uniform float u_warp;
uniform float u_intensity;
uniform float u_twist;
uniform float u_gradientMethod;

vec3 hash33(vec3 p) {
  vec3 q = vec3(
    dot(p, vec3(127.1, 311.7, 74.7)),
    dot(p, vec3(269.5, 183.3, 246.1)),
    dot(p, vec3(113.5, 271.9, 124.6))
  );
  return fract(sin(q) * 43758.5453) * 2.0 - 1.0;
}

vec3 smootherCurve(vec3 t) {
  return t * t * t * (t * (t * 6.0 - 15.0) + 10.0);
}

float gradDot(vec3 cell, vec3 offset, vec3 local) {
  return dot(hash33(cell + offset), local - offset);
}

float perlin3(vec3 p) {
  vec3 cell = floor(p);
  vec3 local = fract(p);
  vec3 w = smootherCurve(local);

  float n000 = gradDot(cell, vec3(0.0, 0.0, 0.0), local);
  float n100 = gradDot(cell, vec3(1.0, 0.0, 0.0), local);
  float n010 = gradDot(cell, vec3(0.0, 1.0, 0.0), local);
  float n110 = gradDot(cell, vec3(1.0, 1.0, 0.0), local);
  float n001 = gradDot(cell, vec3(0.0, 0.0, 1.0), local);
  float n101 = gradDot(cell, vec3(1.0, 0.0, 1.0), local);
  float n011 = gradDot(cell, vec3(0.0, 1.0, 1.0), local);
  float n111 = gradDot(cell, vec3(1.0, 1.0, 1.0), local);

  float nx00 = mix(n000, n100, w.x);
  float nx10 = mix(n010, n110, w.x);
  float nx01 = mix(n001, n101, w.x);
  float nx11 = mix(n011, n111, w.x);
  float nxy0 = mix(nx00, nx10, w.y);
  float nxy1 = mix(nx01, nx11, w.y);

  return mix(nxy0, nxy1, w.z) * 1.1547;
}

vec3 rotateOctave(vec3 p) {
  return vec3(
     0.00 * p.x + 0.80 * p.y + 0.60 * p.z,
    -0.80 * p.x + 0.36 * p.y - 0.48 * p.z,
    -0.60 * p.x - 0.48 * p.y + 0.64 * p.z
  );
}

float fbm(vec3 p) {
  vec3 q = p;
  float total = 0.0;
  float amplitude = 1.0;
  float weight = 0.0;

  for (int i = 0; i < 3; i++) {
    total += perlin3(q) * amplitude;
    weight += amplitude;
    q = rotateOctave(q) * 2.02 + vec3(3.7, 1.9, 6.3);
    amplitude *= 0.48;
  }

  return total / max(weight, 0.0001);
}

vec3 warpVector(vec3 p) {
  return vec3(
    perlin3(p),
    perlin3(p + vec3(5.2, 1.3, 2.8)),
    perlin3(p + vec3(1.7, 9.2, 4.4))
  );
}

float wrapPhase(float phase) {
  float tau = 6.28318530718;
  return phase - floor(phase / tau) * tau;
}

vec3 curvedDomainMotion(float morphTime, vec3 rates, vec3 phases) {
  return vec3(
    sin(wrapPhase(morphTime * rates.x + phases.x)),
    sin(wrapPhase(morphTime * rates.y + phases.y)),
    cos(wrapPhase(morphTime * rates.z + phases.z))
  );
}

vec3 primaryDomainMotion(float morphTime) {
  vec3 primaryDirection = normalize(vec3(0.73, -0.41, 0.55));
  vec3 secondaryDirection = normalize(vec3(-0.28, 0.91, 0.31));
  vec3 primaryDrift = primaryDirection * morphTime * 0.105;
  vec3 secondaryDrift = secondaryDirection * morphTime * 0.023;
  vec3 curve = curvedDomainMotion(
    morphTime,
    vec3(0.071, 0.043, 0.029),
    vec3(0.0, 1.73, 4.11)
  ) * 0.16;
  return primaryDrift + secondaryDrift + curve;
}

vec3 warpDomainMotion(float morphTime) {
  vec3 primaryDirection = normalize(vec3(-0.46, 0.38, 0.80));
  vec3 secondaryDirection = normalize(vec3(0.84, 0.51, -0.18));
  vec3 primaryDrift = primaryDirection * morphTime * 0.137;
  vec3 secondaryDrift = secondaryDirection * morphTime * 0.031;
  vec3 curve = curvedDomainMotion(
    morphTime,
    vec3(0.089, 0.053, 0.034),
    vec3(2.21, 5.07, 0.83)
  ) * 0.12;
  return primaryDrift + secondaryDrift + curve;
}

vec3 gradientDomainMotion(float morphTime) {
  vec3 primaryDirection = normalize(vec3(0.32, 0.76, -0.57));
  vec3 secondaryDirection = normalize(vec3(-0.88, 0.17, -0.44));
  vec3 primaryDrift = primaryDirection * morphTime * 0.079;
  vec3 secondaryDrift = secondaryDirection * morphTime * 0.019;
  vec3 curve = curvedDomainMotion(
    morphTime,
    vec3(0.061, 0.037, 0.023),
    vec3(4.37, 0.91, 2.68)
  ) * 0.19;
  return primaryDrift + secondaryDrift + curve;
}

float heightField(vec3 direction, float detail) {
  float detailLevel = clamp(detail / 5.0, 0.0, 1.0);
  float frequency = mix(1.05, 3.4, detailLevel);
  float morphTime = u_time * max(u_morphSpeed, 0.0);
  vec3 primaryMotion = primaryDomainMotion(morphTime);
  vec3 warpMotion = warpDomainMotion(morphTime);
  vec3 p = direction * frequency + vec3(1.7, 3.1, 5.3) + primaryMotion;
  vec3 warp = warpVector(
    p * 0.55 + warpMotion * 0.42 + vec3(0.7, -1.1, 0.4)
  ) * u_warp;
  return fbm(p + warp);
}

float displacementAmount(float detail) {
  float amount = clamp(detail, 0.0, 1.0);
  float easedAmount = amount * amount * (3.0 - 2.0 * amount);
  return easedAmount * 0.30 * clamp(u_intensity, 0.0, 5.0);
}

vec3 surfacePoint(vec3 direction, float detail) {
  float height = heightField(direction, detail);
  float displacedRadius = 1.0 + height * displacementAmount(detail);
  float safeRadius = max(displacedRadius, 0.72);
  return direction * safeRadius;
}

vec3 rotateAroundAxis(vec3 p, vec3 axis, float angle) {
  float c = cos(angle);
  float s = sin(angle);
  return p * c + cross(axis, p) * s + axis * dot(axis, p) * (1.0 - c);
}

vec3 twistAxis() {
  return normalize(vec3(-0.68, 0.54, 0.49));
}

float torsionAngle(vec3 direction) {
  vec3 axis = twistAxis();
  float axial = clamp(dot(direction, axis), -1.0, 1.0);
  float smoothAxial = axial * (1.5 - 0.5 * axial * axial);
  return smoothAxial * clamp(u_twist, 0.0, 7.0);
}

vec3 twistedSurfacePoint(vec3 direction, float detail) {
  vec3 axis = twistAxis();
  vec3 point = surfacePoint(direction, detail);
  return rotateAroundAxis(point, axis, torsionAngle(direction));
}

vec3 twistedFieldNormal(vec3 direction, float detail) {
  vec3 reference = vec3(0.0, 1.0, 0.0);
  if (abs(direction.y) > 0.9) {
    reference = vec3(1.0, 0.0, 0.0);
  }

  vec3 tangent = normalize(cross(reference, direction));
  vec3 bitangent = normalize(cross(direction, tangent));
  float epsilon = 0.02;

  vec3 tA = twistedSurfacePoint(normalize(direction - tangent * epsilon), detail);
  vec3 tB = twistedSurfacePoint(normalize(direction + tangent * epsilon), detail);
  vec3 bA = twistedSurfacePoint(normalize(direction - bitangent * epsilon), detail);
  vec3 bB = twistedSurfacePoint(normalize(direction + bitangent * epsilon), detail);

  vec3 normal = normalize(cross(tB - tA, bB - bA));
  vec3 outward = normalize(twistedSurfacePoint(direction, detail));
  if (dot(normal, outward) < 0.0) {
    normal = -normal;
  }
  return normal;
}

float spreadCoordinate(float raw) {
  float gain = 3.0;
  return clamp((raw - 0.5) * gain + 0.5, 0.0, 1.0);
}

float objectGradientCoordinate(int method, vec3 direction, float field, float morphTime) {
  if (method == 1) {
    vec3 gradientMotion = gradientDomainMotion(morphTime);
    vec3 gradientWarpMotion = warpDomainMotion(morphTime * 0.71);
    vec3 p = direction * 1.18 + vec3(-2.4, 4.1, 1.6) + gradientMotion;
    vec3 broadWarp = warpVector(
      p * 0.42 + vec3(3.2, -1.7, 2.5) + gradientWarpMotion * 0.19
    ) * 0.16;
    return spreadCoordinate(fbm(p + broadWarp) * 0.5 + 0.5);
  }
  return spreadCoordinate(field * 0.5 + 0.5);
}

vec3 srgbToLinear(vec3 c) {
  vec3 v = max(c, vec3(0.0));
  vec3 cutoff = step(vec3(0.04045), v);
  vec3 low = v / 12.92;
  vec3 high = pow((v + 0.055) / 1.055, vec3(2.4));
  return mix(low, high, cutoff);
}

vec3 linearToSrgb(vec3 c) {
  vec3 v = max(c, vec3(0.0));
  vec3 cutoff = step(vec3(0.0031308), v);
  vec3 low = v * 12.92;
  vec3 high = 1.055 * pow(v, vec3(1.0 / 2.4)) - 0.055;
  return mix(low, high, cutoff);
}

vec3 linearToOklab(vec3 c) {
  float l = 0.4122214708 * c.r + 0.5363325363 * c.g + 0.0514459929 * c.b;
  float m = 0.2119034982 * c.r + 0.6806995451 * c.g + 0.1073969566 * c.b;
  float s = 0.0883024619 * c.r + 0.2817188376 * c.g + 0.6299787005 * c.b;

  float lc = pow(max(l, 0.0), 1.0 / 3.0);
  float mc = pow(max(m, 0.0), 1.0 / 3.0);
  float sc = pow(max(s, 0.0), 1.0 / 3.0);

  return vec3(
    0.2104542553 * lc + 0.7936177850 * mc - 0.0040720468 * sc,
    1.9779984951 * lc - 2.4285922050 * mc + 0.4505937099 * sc,
    0.0259040371 * lc + 0.7827717662 * mc - 0.8086757660 * sc
  );
}

vec3 oklabToLinear(vec3 c) {
  float lc = c.x + 0.3963377774 * c.y + 0.2158037573 * c.z;
  float mc = c.x - 0.1055613458 * c.y - 0.0638541728 * c.z;
  float sc = c.x - 0.0894841775 * c.y - 1.2914855480 * c.z;

  float l = lc * lc * lc;
  float m = mc * mc * mc;
  float s = sc * sc * sc;

  return vec3(
     4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
    -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
    -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s
  );
}

float stopPosition(int index) {
  return u_stops[index];
}

vec4 gradientAt(float t) {
  int count = clamp(int(u_stopCount + 0.5), 1, 8);
  if (count == 1) {
    return u_colors[0];
  }

  int lowIndex = 0;
  for (int i = 0; i < 7; i++) {
    if (i >= count - 1) break;
    if (t >= stopPosition(i)) {
      lowIndex = i;
    }
  }
  int highIndex = lowIndex + 1;

  float start = stopPosition(lowIndex);
  float end = stopPosition(highIndex);
  float amount = clamp((t - start) / max(end - start, 0.0001), 0.0, 1.0);
  amount = amount * amount * (3.0 - 2.0 * amount);

  vec4 a = u_colors[lowIndex];
  vec4 b = u_colors[highIndex];

  vec3 labA = linearToOklab(srgbToLinear(a.rgb));
  vec3 labB = linearToOklab(srgbToLinear(b.rgb));
  vec3 blended = linearToSrgb(oklabToLinear(mix(labA, labB, amount)));

  return vec4(blended, mix(a.a, b.a, amount));
}

float balanceRemap(float coordinate) {
  float balance = clamp(u_gradientBalance, -1.0, 1.0);
  float balanceExponent = pow(4.0, balance);
  return pow(clamp(coordinate, 0.0, 1.0), 1.0 / balanceExponent);
}

vec3 rotateX(vec3 p, float angle) {
  float c = cos(angle);
  float s = sin(angle);
  return vec3(p.x, c * p.y - s * p.z, s * p.y + c * p.z);
}

vec3 animatedOrientation(vec3 p, float rotationTime) {
  vec3 axisA = normalize(vec3(0.36, 0.81, 0.46));
  vec3 axisB = normalize(vec3(-0.71, 0.29, 0.64));
  vec3 axisC = normalize(vec3(0.58, -0.69, 0.43));

  float angleA = wrapPhase(rotationTime * 0.287);
  float angleB = wrapPhase(rotationTime * 0.2236068);
  float angleC = wrapPhase(rotationTime * 0.1732051);

  vec3 oriented = rotateAroundAxis(p, axisA, angleA);
  oriented = rotateAroundAxis(oriented, axisB, angleB);
  oriented = rotateAroundAxis(oriented, axisC, angleC);
  return rotateX(oriented, -0.24);
}

float coverSphereScale(float aspect) {
  float safeRadius = 0.72;
  float cameraDistance = 3.0;
  float targetZoom = 4.0;
  float baseFocalLength = 1.73;
  float diagonal = sqrt(1.0 + aspect * aspect);
  float targetFocalLength = baseFocalLength * targetZoom;
  float requiredRadius = cameraDistance * diagonal /
    sqrt(targetFocalLength * targetFocalLength + diagonal * diagonal);
  return max(0.82, requiredRadius / safeRadius);
}
`;

const SPHERE_VERTEX_SRC = `#version 300 es
precision highp float;
layout(location = 0) in vec3 a_position;
out vec3 v_baseDirection;
out vec3 v_worldPosition;
${GLSL_COMMON}
void main() {
  float detail = clamp(u_detail, 0.0, 5.0);
  float rotationTime = u_time * max(u_rotationSpeed, 0.0);
  float aspect = max(u_aspectRatio, 0.001);
  float zoom = clamp(u_zoom, 0.5, 10.0);

  vec3 baseDirection = normalize(a_position);
  vec3 objectPosition = twistedSurfacePoint(baseDirection, detail);
  vec3 rotatedPosition = animatedOrientation(objectPosition, rotationTime);

  float sphereScale = coverSphereScale(aspect);
  vec3 worldPosition = rotatedPosition * sphereScale;
  float cameraDistance = 3.0;
  vec3 viewPosition = worldPosition + vec3(0.0, 0.0, -cameraDistance);

  float focalLength = 1.73 * zoom;
  float nearPlane = 0.1;
  float farPlane = 10.0;
  float depthA = farPlane / (nearPlane - farPlane);
  float depthB = nearPlane * farPlane / (nearPlane - farPlane);

  gl_Position = vec4(
    viewPosition.x * focalLength / aspect,
    viewPosition.y * focalLength,
    depthA * viewPosition.z + depthB,
    -viewPosition.z
  );
  v_baseDirection = baseDirection;
  v_worldPosition = worldPosition;
}
`;

const SPHERE_FRAGMENT_SRC = `#version 300 es
precision highp float;
in vec3 v_baseDirection;
in vec3 v_worldPosition;
out vec4 fragColor;
${GLSL_COMMON}
void main() {
  float detail = clamp(u_detail, 0.0, 5.0);
  float rotationTime = u_time * max(u_rotationSpeed, 0.0);
  float morphTime = u_time * max(u_morphSpeed, 0.0);
  int gradientMethod = clamp(int(u_gradientMethod + 0.5), 0, 2);

  vec3 direction = normalize(v_baseDirection);
  float field = 0.0;
  if (gradientMethod == 0) {
    field = heightField(direction, detail);
  }

  float gradientCoordinate = 0.0;
  if (gradientMethod == 2) {
    vec3 objectNormal = twistedFieldNormal(direction, detail);
    vec3 normal = normalize(animatedOrientation(objectNormal, rotationTime));
    vec3 viewDirection = normalize(vec3(0.0, 0.0, 3.0) - v_worldPosition);
    float facing = max(dot(normal, viewDirection), 0.0);
    gradientCoordinate = 1.0 - facing;
  } else {
    gradientCoordinate = objectGradientCoordinate(gradientMethod, direction, field, morphTime);
  }

  float t = balanceRemap(gradientCoordinate);
  /* material is always "None" here (lighting models intentionally not
     ported) so the shader's own material===0 branch — return baseColor
     untouched — is the only path implemented. */
  fragColor = gradientAt(t);
}
`;

const BACKDROP_VERTEX_SRC = `#version 300 es
precision highp float;
out vec2 v_screenUv;
void main() {
  vec2 positions[3] = vec2[3](
    vec2(-1.0, -3.0),
    vec2(-1.0, 1.0),
    vec2(3.0, 1.0)
  );
  vec2 p = positions[gl_VertexID];
  gl_Position = vec4(p, 0.5, 1.0);
  v_screenUv = vec2(p.x * 0.5 + 0.5, 0.5 - p.y * 0.5);
}
`;

const BACKDROP_FRAGMENT_SRC = `#version 300 es
precision highp float;
in vec2 v_screenUv;
out vec4 fragColor;
${GLSL_COMMON}
void main() {
  float aspect = max(u_aspectRatio, 0.001);
  vec2 axis = normalize(vec2(0.62 * aspect, 0.78));
  vec2 centered = vec2(
    (v_screenUv.x - 0.5) * aspect,
    v_screenUv.y - 0.5
  );
  float extent = abs(axis.x) * aspect * 0.5 + abs(axis.y) * 0.5;
  float raw = dot(centered, axis) / max(extent * 2.0, 0.0001) + 0.5;
  fragColor = gradientAt(balanceRemap(raw));
}
`;

/* ---------------------------------------------------------------------
   Cube-sphere mesh generation — ported verbatim from main.ts (plain JS
   there too, no WebGPU-specific calls in the geometry math), just with
   a lower default resolution.
   --------------------------------------------------------------------- */
function buildSphereMesh(resolution) {
  const faces = [
    { right: [0, 0, -1], up: [0, 1, 0] },
    { right: [0, 0, 1], up: [0, 1, 0] },
    { right: [1, 0, 0], up: [0, 0, -1] },
    { right: [1, 0, 0], up: [0, 0, 1] },
    { right: [1, 0, 0], up: [0, 1, 0] },
    { right: [-1, 0, 0], up: [0, 1, 0] },
  ];

  const verticesPerFace = (resolution + 1) * (resolution + 1);
  const vertexCount = faces.length * verticesPerFace;
  const triangleCount = faces.length * resolution * resolution * 2;
  const vertices = new Float32Array(vertexCount * 3);
  const indices = new Uint32Array(triangleCount * 3);
  let vertexOffset = 0;
  let indexOffset = 0;

  for (let f = 0; f < faces.length; f += 1) {
    const right = faces[f].right;
    const up = faces[f].up;
    const forward = [
      right[1] * up[2] - right[2] * up[1],
      right[2] * up[0] - right[0] * up[2],
      right[0] * up[1] - right[1] * up[0],
    ];
    const baseIndex = vertexOffset / 3;

    for (let row = 0; row <= resolution; row += 1) {
      const v = (row / resolution) * 2 - 1;
      for (let column = 0; column <= resolution; column += 1) {
        const u = (column / resolution) * 2 - 1;
        const x = forward[0] + right[0] * u + up[0] * v;
        const y = forward[1] + right[1] * u + up[1] * v;
        const z = forward[2] + right[2] * u + up[2] * v;
        const x2 = x * x;
        const y2 = y * y;
        const z2 = z * z;
        vertices[vertexOffset] = x * Math.sqrt(1 - y2 / 2 - z2 / 2 + (y2 * z2) / 3);
        vertices[vertexOffset + 1] = y * Math.sqrt(1 - z2 / 2 - x2 / 2 + (z2 * x2) / 3);
        vertices[vertexOffset + 2] = z * Math.sqrt(1 - x2 / 2 - y2 / 2 + (x2 * y2) / 3);
        vertexOffset += 3;
      }
    }

    const rowSize = resolution + 1;
    for (let row = 0; row < resolution; row += 1) {
      for (let column = 0; column < resolution; column += 1) {
        const a = baseIndex + row * rowSize + column;
        const b = a + 1;
        const c = a + rowSize + 1;
        const d = a + rowSize;
        indices[indexOffset] = a;
        indices[indexOffset + 1] = b;
        indices[indexOffset + 2] = c;
        indices[indexOffset + 3] = a;
        indices[indexOffset + 4] = c;
        indices[indexOffset + 5] = d;
        indexOffset += 6;
      }
    }
  }

  return { vertices, indices };
}

/* ---------------------------------------------------------------------
   WebGL boilerplate
   --------------------------------------------------------------------- */
function compileShader(gl, type, source) {
  const shader = gl.createShader(type);
  gl.shaderSource(shader, source);
  gl.compileShader(shader);
  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
    const info = gl.getShaderInfoLog(shader);
    gl.deleteShader(shader);
    throw new Error("Shader compile error: " + info);
  }
  return shader;
}

function createProgram(gl, vsSource, fsSource) {
  const vs = compileShader(gl, gl.VERTEX_SHADER, vsSource);
  const fs = compileShader(gl, gl.FRAGMENT_SHADER, fsSource);
  const program = gl.createProgram();
  gl.attachShader(program, vs);
  gl.attachShader(program, fs);
  gl.linkProgram(program);
  gl.deleteShader(vs);
  gl.deleteShader(fs);
  if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
    const info = gl.getProgramInfoLog(program);
    gl.deleteProgram(program);
    throw new Error("Program link error: " + info);
  }
  return program;
}

const UNIFORM_NAMES = [
  "u_detail", "u_time", "u_aspectRatio", "u_stopCount", "u_zoom",
  "u_morphSpeed", "u_rotationSpeed", "u_gradientBalance", "u_warp",
  "u_intensity", "u_twist", "u_gradientMethod",
];

function getUniformLocations(gl, program) {
  const locs = {};
  for (const name of UNIFORM_NAMES) {
    locs[name] = gl.getUniformLocation(program, name);
  }
  locs.u_colors = gl.getUniformLocation(program, "u_colors[0]");
  locs.u_stops = gl.getUniformLocation(program, "u_stops[0]");
  return locs;
}

const MAX_ZOOM = 10;
const MIN_ZOOM_FLOOR = 0.5;
const SAFE_RADIUS = 0.72;
const CAMERA_DISTANCE = 3;
const BASE_FOCAL_LENGTH = 1.73;

/* Host-side mirror of main.ts's zoom-percent -> zoom-units remap. Pure
   JS math, ported as-is (no WGSL involved on this side in the source
   either). Depends on aspect ratio, so it's recomputed on resize. */
function computeMinimumZoom(aspect) {
  const diagonal = Math.sqrt(1 + aspect * aspect);
  const targetCoverZoom = 4;
  const targetFocalLength = BASE_FOCAL_LENGTH * targetCoverZoom;
  const requiredRadius =
    (CAMERA_DISTANCE * diagonal) /
    Math.sqrt(targetFocalLength * targetFocalLength + diagonal * diagonal);
  const sphereScale = Math.max(0.82, requiredRadius / SAFE_RADIUS);
  const conservativeRadius = Math.min(CAMERA_DISTANCE - 0.001, sphereScale * SAFE_RADIUS);
  const perspectiveDepth = Math.sqrt(
    Math.max(0.0001, CAMERA_DISTANCE * CAMERA_DISTANCE - conservativeRadius * conservativeRadius)
  );
  const unclampedMinimumCoverZoom =
    ((diagonal * perspectiveDepth) / (BASE_FOCAL_LENGTH * conservativeRadius)) * 1.12;
  const minimumCoverZoom = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM_FLOOR, unclampedMinimumCoverZoom));
  const zoomOutFactor = 0.65;
  return Math.min(MAX_ZOOM, Math.max(MIN_ZOOM_FLOOR, minimumCoverZoom * zoomOutFactor));
}

function computeZoom(zoomPercent, minimumZoom) {
  const clamped = Math.min(100, Math.max(0, zoomPercent));
  const progress = clamped / 100;
  const unclamped = minimumZoom * Math.pow(MAX_ZOOM / minimumZoom, progress);
  return Math.min(MAX_ZOOM, Math.max(minimumZoom, unclamped));
}

function computeRotationSpeed(rotationSpeedPercent) {
  const clamped = Math.min(100, Math.max(0, rotationSpeedPercent));
  return Math.min(0.25, Math.max(0, (clamped / 100) * 0.25));
}

function MovingGradientSphere({
  detail = 1.8,
  intensity = 2.2,
  twist = 0.04,
  warp = 0.25,
  zoom = 50,
  rotationSpeed = 8,
  morphSpeed = 2,
  gradientMethod = "noise",
  colors = ["#0B1220", "#2563EB", "#60A5FA"],
  stops = [0, 0.5, 1],
  resolution = 64,
  className,
}) {
  const canvasRef = useRef(null);

  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return undefined;

    let gl;
    try {
      gl = canvas.getContext("webgl2", { antialias: true, alpha: true, depth: true });
    } catch (err) {
      gl = null;
    }
    if (!gl) {
      console.warn("MovingGradientSphere: WebGL2 unavailable, falling back to CSS glow.");
      return undefined;
    }

    let sphereProgram;
    let backdropProgram;
    let sphereLocs;
    let backdropLocs;
    let vertexBuffer;
    let indexBuffer;
    let indexCount = 0;
    let emptyVao;
    let sphereVao;

    try {
      sphereProgram = createProgram(gl, SPHERE_VERTEX_SRC, SPHERE_FRAGMENT_SRC);
      backdropProgram = createProgram(gl, BACKDROP_VERTEX_SRC, BACKDROP_FRAGMENT_SRC);
      sphereLocs = getUniformLocations(gl, sphereProgram);
      backdropLocs = getUniformLocations(gl, backdropProgram);

      const mesh = buildSphereMesh(resolution);
      indexCount = mesh.indices.length;

      vertexBuffer = gl.createBuffer();
      gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
      gl.bufferData(gl.ARRAY_BUFFER, mesh.vertices, gl.STATIC_DRAW);

      indexBuffer = gl.createBuffer();
      gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
      gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, mesh.indices, gl.STATIC_DRAW);

      sphereVao = gl.createVertexArray();
      gl.bindVertexArray(sphereVao);
      gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
      gl.enableVertexAttribArray(0);
      gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 12, 0);
      gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
      gl.bindVertexArray(null);

      // WebGL2 requires a bound VAO for any draw call, even the
      // attribute-less full-screen-triangle backdrop pass.
      emptyVao = gl.createVertexArray();
    } catch (err) {
      console.error("MovingGradientSphere: shader setup failed, falling back to CSS glow.", err);
      return undefined;
    }

    const colorData = new Float32Array(32);
    const stopData = new Float32Array(8);
    const stopCount = Math.min(8, colors.length);
    for (let i = 0; i < stopCount; i += 1) {
      const [r, g, b] = hexToRgb01(colors[i]);
      colorData[i * 4] = r;
      colorData[i * 4 + 1] = g;
      colorData[i * 4 + 2] = b;
      colorData[i * 4 + 3] = 1;
      stopData[i] = stops[i] ?? 0;
    }

    const gradientMethodValue = resolveGradientMethod(gradientMethod);
    const rotationSpeedValue = computeRotationSpeed(rotationSpeed);

    let minimumZoom = MIN_ZOOM_FLOOR;
    let lastAspect = NaN;
    let dpr = Math.min(window.devicePixelRatio || 1, 2);

    function resize() {
      const rect = canvas.getBoundingClientRect();
      const width = Math.max(1, Math.round(rect.width * dpr));
      const height = Math.max(1, Math.round(rect.height * dpr));
      if (canvas.width !== width || canvas.height !== height) {
        canvas.width = width;
        canvas.height = height;
        gl.viewport(0, 0, width, height);
      }
      const aspect = width / height;
      if (aspect !== lastAspect) {
        minimumZoom = computeMinimumZoom(aspect);
        lastAspect = aspect;
      }
    }

    function setCommonUniforms(locs, time, aspect) {
      gl.uniform1f(locs.u_detail, detail);
      gl.uniform1f(locs.u_time, time);
      gl.uniform1f(locs.u_aspectRatio, aspect);
      gl.uniform1f(locs.u_stopCount, stopCount);
      gl.uniform4fv(locs.u_colors, colorData);
      gl.uniform1fv(locs.u_stops, stopData);
      gl.uniform1f(locs.u_zoom, computeZoom(zoom, minimumZoom));
      gl.uniform1f(locs.u_morphSpeed, morphSpeed);
      gl.uniform1f(locs.u_rotationSpeed, rotationSpeedValue);
      gl.uniform1f(locs.u_gradientBalance, 0);
      gl.uniform1f(locs.u_warp, warp);
      gl.uniform1f(locs.u_intensity, intensity);
      gl.uniform1f(locs.u_twist, twist);
      gl.uniform1f(locs.u_gradientMethod, gradientMethodValue);
    }

    function drawFrame(time) {
      resize();
      const aspect = canvas.width / Math.max(canvas.height, 1);

      gl.clearColor(0, 0, 0, 0);
      gl.clearDepth(1);
      gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

      gl.disable(gl.DEPTH_TEST);
      gl.disable(gl.CULL_FACE);
      gl.useProgram(backdropProgram);
      setCommonUniforms(backdropLocs, time, aspect);
      gl.bindVertexArray(emptyVao);
      gl.drawArrays(gl.TRIANGLES, 0, 3);

      gl.enable(gl.DEPTH_TEST);
      gl.depthFunc(gl.LESS);
      gl.enable(gl.CULL_FACE);
      gl.cullFace(gl.BACK);
      gl.frontFace(gl.CCW);
      gl.useProgram(sphereProgram);
      setCommonUniforms(sphereLocs, time, aspect);
      gl.bindVertexArray(sphereVao);
      gl.drawElements(gl.TRIANGLES, indexCount, gl.UNSIGNED_INT, 0);
      gl.bindVertexArray(null);
    }

    const startTime = performance.now();
    let rafId = null;
    let isVisible = false;
    let hasRenderedOnce = false;

    const reducedMotionQuery = window.matchMedia("(prefers-reduced-motion: reduce)");

    function frame(now) {
      drawFrame((now - startTime) * 0.001);
      hasRenderedOnce = true;
      if (isVisible && !reducedMotionQuery.matches) {
        rafId = requestAnimationFrame(frame);
      } else {
        rafId = null;
      }
    }

    function startLoop() {
      if (rafId !== null) return;
      if (reducedMotionQuery.matches) {
        if (!hasRenderedOnce) {
          drawFrame(0);
          hasRenderedOnce = true;
        }
        return;
      }
      rafId = requestAnimationFrame(frame);
    }

    function stopLoop() {
      if (rafId !== null) {
        cancelAnimationFrame(rafId);
        rafId = null;
      }
    }

    const io = new IntersectionObserver(
      (entries) => {
        isVisible = entries[0] && entries[0].isIntersecting;
        if (isVisible) startLoop();
        else stopLoop();
      },
      { threshold: 0.01 }
    );
    io.observe(canvas);

    const ro = new ResizeObserver(() => {
      resize();
      if (!isVisible || reducedMotionQuery.matches) {
        drawFrame(hasRenderedOnce ? (performance.now() - startTime) * 0.001 : 0);
        hasRenderedOnce = true;
      }
    });
    ro.observe(canvas);

    function handleReducedMotionChange() {
      if (reducedMotionQuery.matches) {
        stopLoop();
        drawFrame((performance.now() - startTime) * 0.001);
        hasRenderedOnce = true;
      } else if (isVisible) {
        startLoop();
      }
    }
    reducedMotionQuery.addEventListener("change", handleReducedMotionChange);

    resize();
    drawFrame(0);
    hasRenderedOnce = true;
    if (canvas.parentElement) canvas.parentElement.classList.add("glow-canvas-ready");

    return () => {
      stopLoop();
      io.disconnect();
      ro.disconnect();
      reducedMotionQuery.removeEventListener("change", handleReducedMotionChange);
      if (canvas.parentElement) canvas.parentElement.classList.remove("glow-canvas-ready");
      gl.deleteVertexArray(sphereVao);
      gl.deleteVertexArray(emptyVao);
      gl.deleteBuffer(vertexBuffer);
      gl.deleteBuffer(indexBuffer);
      gl.deleteProgram(sphereProgram);
      gl.deleteProgram(backdropProgram);
    };
    // Config is treated as fixed for the lifetime of the instance;
    // Hero/Contact both mount this once with static props.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

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

window.MovingGradientSphere = MovingGradientSphere;
