// Grainient — ported from React Bits (reactbits.dev) to plain JSX. Needs ogl, which is
// ESM-only, so every page loads it as a module and hands it over on window.OGL, the same
// arrangement SideRays uses.
//
// It is rendered once per page as a fixed backdrop behind everything (see PageBackdrop in
// components.jsx), not once per section. One WebGL context for the whole page rather than
// one per band: the "below" sections are simply transparent and let it show through, and
// the "above" sections are opaque slabs on top. Interleaving them costs nothing.
//
// Deviations from upstream, all deliberate:
//   * Colours come from the site's own tokens and are re-read when the theme flips.
//   * It renders nothing at all under prefers-reduced-motion — an animated gradient behind
//     every page is exactly what that setting is asking us not to do.
//   * It waits for the ps:ogl event rather than assuming the module has landed; module
//     scripts are deferred, so React usually mounts first.
const GRAINIENT_VERTEX = `#version 300 es
in vec2 position;
void main() { gl_Position = vec4(position, 0.0, 1.0); }
`;

const GRAINIENT_FRAGMENT = `#version 300 es
precision highp float;
uniform vec2 iResolution;
uniform float iTime;
uniform float uTimeSpeed;
uniform float uColorBalance;
uniform float uWarpStrength;
uniform float uWarpFrequency;
uniform float uWarpSpeed;
uniform float uWarpAmplitude;
uniform float uBlendAngle;
uniform float uBlendSoftness;
uniform float uRotationAmount;
uniform float uNoiseScale;
uniform float uGrainAmount;
uniform float uGrainScale;
uniform float uGrainAnimated;
uniform float uContrast;
uniform float uGamma;
uniform float uSaturation;
uniform vec2 uCenterOffset;
uniform float uZoom;
uniform vec3 uColor1;
uniform vec3 uColor2;
uniform vec3 uColor3;
out vec4 fragColor;
#define S(a,b,t) smoothstep(a,b,t)
mat2 Rot(float a){float s=sin(a),c=cos(a);return mat2(c,-s,s,c);}
vec2 hash(vec2 p){p=vec2(dot(p,vec2(2127.1,81.17)),dot(p,vec2(1269.5,283.37)));return fract(sin(p)*43758.5453);}
float noise(vec2 p){vec2 i=floor(p),f=fract(p),u=f*f*(3.0-2.0*f);float n=mix(mix(dot(-1.0+2.0*hash(i+vec2(0.0,0.0)),f-vec2(0.0,0.0)),dot(-1.0+2.0*hash(i+vec2(1.0,0.0)),f-vec2(1.0,0.0)),u.x),mix(dot(-1.0+2.0*hash(i+vec2(0.0,1.0)),f-vec2(0.0,1.0)),dot(-1.0+2.0*hash(i+vec2(1.0,1.0)),f-vec2(1.0,1.0)),u.x),u.y);return 0.5+0.5*n;}
void mainImage(out vec4 o, vec2 C){
  float t=iTime*uTimeSpeed;
  vec2 uv=C/iResolution.xy;
  float ratio=iResolution.x/iResolution.y;
  vec2 tuv=uv-0.5+uCenterOffset;
  tuv/=max(uZoom,0.001);
  float degree=noise(vec2(t*0.1,tuv.x*tuv.y)*uNoiseScale);
  tuv.y*=1.0/ratio;
  tuv*=Rot(radians((degree-0.5)*uRotationAmount+180.0));
  tuv.y*=ratio;
  float frequency=uWarpFrequency;
  float ws=max(uWarpStrength,0.001);
  float amplitude=uWarpAmplitude/ws;
  float warpTime=t*uWarpSpeed;
  tuv.x+=sin(tuv.y*frequency+warpTime)/amplitude;
  tuv.y+=sin(tuv.x*(frequency*1.5)+warpTime)/(amplitude*0.5);
  vec3 colLav=uColor1;
  vec3 colOrg=uColor2;
  vec3 colDark=uColor3;
  float b=uColorBalance;
  float s=max(uBlendSoftness,0.0);
  mat2 blendRot=Rot(radians(uBlendAngle));
  float blendX=(tuv*blendRot).x;
  float edge0=-0.3-b-s;
  float edge1=0.2-b+s;
  float v0=0.5-b+s;
  float v1=-0.3-b-s;
  vec3 layer1=mix(colDark,colOrg,S(edge0,edge1,blendX));
  vec3 layer2=mix(colOrg,colLav,S(edge0,edge1,blendX));
  vec3 col=mix(layer1,layer2,S(v0,v1,tuv.y));
  vec2 grainUv=uv*max(uGrainScale,0.001);
  if(uGrainAnimated>0.5){grainUv+=vec2(iTime*0.05);}
  float grain=fract(sin(dot(grainUv,vec2(12.9898,78.233)))*43758.5453);
  col+=(grain-0.5)*uGrainAmount;
  col=(col-0.5)*uContrast+0.5;
  float luma=dot(col,vec3(0.2126,0.7152,0.0722));
  col=mix(vec3(luma),col,uSaturation);
  col=pow(max(col,0.0),vec3(1.0/max(uGamma,0.001)));
  col=clamp(col,0.0,1.0);
  o=vec4(col,1.0);
}
void main(){ vec4 o=vec4(0.0); mainImage(o,gl_FragCoord.xy); fragColor=o; }
`;

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

const Grainient = ({
  timeSpeed = 0.12, colorBalance = 0, warpStrength = 1, warpFrequency = 4,
  warpSpeed = 1.4, warpAmplitude = 60, blendAngle = 0, blendSoftness = 0.22,
  rotationAmount = 260, noiseScale = 1.6, grainAmount = 0.07, grainScale = 2,
  grainAnimated = false, contrast = 1.05, gamma = 1, saturation = 0.9,
  centerX = 0, centerY = 0, zoom = 0.9,
  color1 = "#FF9FFC", color2 = "#5227FF", color3 = "#B497CF",
  className = "",
}) => {
  const { useRef, useEffect, useState } = React;
  const hostRef = useRef(null);
  const ctxRef = useRef(null);
  const [oglReady, setOglReady] = useState(() => !!window.OGL);

  useEffect(() => {
    if (window.OGL) { setOglReady(true); return; }
    const onReady = () => setOglReady(true);
    window.addEventListener("ps:ogl", onReady);
    // Module scripts are deferred and the event may already have fired, so poll briefly
    // as well rather than waiting forever on a listener that will never be called.
    const poll = setInterval(() => { if (window.OGL) { setOglReady(true); clearInterval(poll); } }, 150);
    return () => { window.removeEventListener("ps:ogl", onReady); clearInterval(poll); };
  }, []);

  useEffect(() => {
    const host = hostRef.current;
    if (!host || !oglReady) return;
    const { Renderer, Program, Mesh, Triangle } = window.OGL;

    const renderer = new Renderer({
      webgl: 2, alpha: false, antialias: false,
      dpr: Math.min(window.devicePixelRatio || 1, 1.5),
    });
    const gl = renderer.gl;
    const canvas = gl.canvas;
    canvas.style.cssText = "width:100%;height:100%;display:block";
    host.appendChild(canvas);

    const program = new Program(gl, {
      vertex: GRAINIENT_VERTEX,
      fragment: GRAINIENT_FRAGMENT,
      uniforms: {
        iTime: { value: 0 },
        iResolution: { value: new Float32Array([1, 1]) },
        uTimeSpeed: { value: 0 }, uColorBalance: { value: 0 },
        uWarpStrength: { value: 1 }, uWarpFrequency: { value: 4 },
        uWarpSpeed: { value: 1 }, uWarpAmplitude: { value: 60 },
        uBlendAngle: { value: 0 }, uBlendSoftness: { value: 0.2 },
        uRotationAmount: { value: 260 }, uNoiseScale: { value: 1.6 },
        uGrainAmount: { value: 0.07 }, uGrainScale: { value: 2 },
        uGrainAnimated: { value: 0 }, uContrast: { value: 1 },
        uGamma: { value: 1 }, uSaturation: { value: 1 },
        uCenterOffset: { value: new Float32Array([0, 0]) }, uZoom: { value: 0.9 },
        uColor1: { value: new Float32Array([1, 1, 1]) },
        uColor2: { value: new Float32Array([1, 1, 1]) },
        uColor3: { value: new Float32Array([1, 1, 1]) },
      },
    });
    const mesh = new Mesh(gl, { geometry: new Triangle(gl), program });
    ctxRef.current = { renderer, program, mesh };

    const setSize = () => {
      const r = host.getBoundingClientRect();
      renderer.setSize(Math.max(1, Math.floor(r.width)), Math.max(1, Math.floor(r.height)));
      const res = program.uniforms.iResolution.value;
      res[0] = gl.drawingBufferWidth;
      res[1] = gl.drawingBufferHeight;
      renderer.render({ scene: mesh });
    };
    const ro = new ResizeObserver(setSize);
    ro.observe(host);
    setSize();

    let raf = 0;
    let pageVisible = !document.hidden;
    const t0 = performance.now();
    const loop = (t) => {
      program.uniforms.iTime.value = (t - t0) * 0.001;
      renderer.render({ scene: mesh });
      raf = requestAnimationFrame(loop);
    };
    const start = () => { if (pageVisible && raf === 0) raf = requestAnimationFrame(loop); };
    const stop = () => { if (raf !== 0) { cancelAnimationFrame(raf); raf = 0; } };
    const onVis = () => { pageVisible = !document.hidden; pageVisible ? start() : stop(); };
    document.addEventListener("visibilitychange", onVis);
    start();

    return () => {
      stop();
      ro.disconnect();
      document.removeEventListener("visibilitychange", onVis);
      ctxRef.current = null;
      try { host.removeChild(canvas); } catch (e) { /* already gone */ }
    };
  }, [oglReady]);

  // Uniforms are synced separately so a theme flip never rebuilds the GL context.
  useEffect(() => {
    const ctx = ctxRef.current;
    if (!ctx) return;
    const u = ctx.program.uniforms;
    u.uTimeSpeed.value = timeSpeed;
    u.uColorBalance.value = colorBalance;
    u.uWarpStrength.value = warpStrength;
    u.uWarpFrequency.value = warpFrequency;
    u.uWarpSpeed.value = warpSpeed;
    u.uWarpAmplitude.value = warpAmplitude;
    u.uBlendAngle.value = blendAngle;
    u.uBlendSoftness.value = blendSoftness;
    u.uRotationAmount.value = rotationAmount;
    u.uNoiseScale.value = noiseScale;
    u.uGrainAmount.value = grainAmount;
    u.uGrainScale.value = grainScale;
    u.uGrainAnimated.value = grainAnimated ? 1 : 0;
    u.uContrast.value = contrast;
    u.uGamma.value = gamma;
    u.uSaturation.value = saturation;
    u.uCenterOffset.value = new Float32Array([centerX, centerY]);
    u.uZoom.value = zoom;
    u.uColor1.value = new Float32Array(grainientHexToRgb(color1));
    u.uColor2.value = new Float32Array(grainientHexToRgb(color2));
    u.uColor3.value = new Float32Array(grainientHexToRgb(color3));
  }, [oglReady, timeSpeed, colorBalance, warpStrength, warpFrequency, warpSpeed,
      warpAmplitude, blendAngle, blendSoftness, rotationAmount, noiseScale, grainAmount,
      grainScale, grainAnimated, contrast, gamma, saturation, centerX, centerY, zoom,
      color1, color2, color3]);

  return <div ref={hostRef} className={`grainient-container ${className}`.trim()} aria-hidden="true" />;
};
window.Grainient = Grainient;
