/* ═══════════════════════════════════════════════════════════════
   DNAHelix - Premium Raymarched DNA Shader
   ═══════════════════════════════════════════════════════════════ */

const VERTEX = `
attribute vec2 position;
attribute vec2 uv;
varying vec2 vUv;
void main() {
    vUv = uv;
    gl_Position = vec4(position, 0.0, 1.0);
}
`;

const FRAGMENT = `
precision highp float;

uniform float uTime;
uniform float uScroll;
uniform vec2 uResolution;
uniform vec2 uMouse;
uniform vec3 uColor1;
uniform vec3 uColor2;
uniform float uOpacity;
uniform float uGlow;
uniform float uScale;

varying vec2 vUv;

#define MAX_STEPS 70
#define MAX_DIST 15.0
#define SURF_DIST 0.005

// 2D Rotation
mat2 rot(float a) {
    float s = sin(a);
    float c = cos(a);
    return mat2(c, -s, s, c);
}

// Capsule SDF
float sdCapsule(vec3 p, vec3 a, vec3 b, float r) {
    vec3 pa = p - a, ba = b - a;
    float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
    return length(pa - ba * h) - r;
}

// Smooth min
float smin(float a, float b, float k) {
    float h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0);
    return mix(b, a, h) - k * h * (1.0 - h);
}

// Scene Distance Field
float map(vec3 p) {
    // Basic scaling and scrolling
    p.y += uScroll * 0.003;
    p /= uScale;
    
    // Global subtle rotation (mouse + auto)
    p.yz *= rot(uMouse.y * 0.5);
    p.xz *= rot(uMouse.x * 0.5 + uTime * 0.15);
    p.xy *= rot(0.1 * sin(uTime * 0.3)); // Subtle breathing tilt
    
    vec3 q = p;
    
    // Twist deformation
    float twistAmt = 1.2;
    q.xz *= rot(q.y * twistAmt - uTime * 0.8);
    
    // Double Helix dimensions
    float radius = 0.65;
    float thickness = 0.06;
    
    // Two vertical strands
    float s1 = length(q.xz - vec2(radius, 0.0)) - thickness;
    float s2 = length(q.xz - vec2(-radius, 0.0)) - thickness;
    
    // Base pairs (Rungs)
    float spacing = 0.5;
    vec3 rp = q;
    rp.y = mod(rp.y + spacing * 0.5, spacing) - spacing * 0.5;
    
    float rung = sdCapsule(rp, vec3(radius, 0.0, 0.0), vec3(-radius, 0.0, 0.0), 0.015);
    
    // Organic blending
    float dna = smin(smin(s1, s2, 0.2), rung, 0.15);
    
    // Subtle organic pulsing
    dna += sin(p.y * 12.0 - uTime * 3.0) * 0.005;
    
    return dna * uScale;
}

// Normal Calculation
vec3 getNormal(vec3 p) {
    vec2 e = vec2(0.005, 0.0);
    return normalize(vec3(
        map(p + e.xyy) - map(p - e.xyy),
        map(p + e.yxy) - map(p - e.yxy),
        map(p + e.yyx) - map(p - e.yyx)
    ));
}

void main() {
    // Screen coordinates centered
    vec2 uv = (vUv - 0.5) * 2.0;
    uv.x *= uResolution.x / uResolution.y;
    
    // Ray setup
    vec3 ro = vec3(0.0, 0.0, 3.5);
    vec3 rd = normalize(vec3(uv, -1.0));
    
    float d0 = 0.0;
    vec3 p;
    float glow = 0.0;
    
    // Raymarching loop
    for(int i = 0; i < MAX_STEPS; i++) {
        p = ro + rd * d0;
        float dS = map(p);
        
        // Accumulate volumetric glow when close to surface
        if (dS < 0.15) {
            glow += (0.15 - dS) * 0.8; 
        }
        
        if(dS < SURF_DIST || d0 > MAX_DIST) break;
        d0 += dS * 0.7; // Fractional step for better glow sampling
    }
    
    vec3 col = vec3(0.0);
    float alpha = 0.0;
    
    if(d0 < MAX_DIST) {
        // Surface shading
        vec3 n = getNormal(p);
        vec3 lig = normalize(vec3(1.0, 2.0, 3.0));
        vec3 lig2 = normalize(vec3(-2.0, -1.0, -2.0));
        
        // Basic lighting
        float dif = max(dot(n, lig), 0.0);
        float dif2 = max(dot(n, lig2), 0.0) * 0.5;
        float amb = 0.15 + p.z * 0.02; // Depth ambient
        
        // Determine coloring based on untwisted space
        vec3 untwisted = p / uScale;
        untwisted.xz *= rot(-untwisted.y * 1.2 + uTime * 0.8);
        float colorBlend = smoothstep(-0.3, 0.3, untwisted.x);
        
        vec3 matCol = mix(uColor1, uColor2, colorBlend);
        
        // Fresnel rim light
        float fresnel = pow(1.0 - max(dot(n, -rd), 0.0), 3.0);
        
        col = matCol * (dif + dif2 + amb);
        col += matCol * fresnel * 0.8; // Rim light enhancement
        
        // Atmospheric depth fading
        float fog = smoothstep(2.5, 6.0, d0);
        col = mix(col, vec3(0.0), fog);
        
        alpha = 1.0 - fog;
    }
    
    // Add volumetric glow
    vec3 glowCol = mix(uColor1, uColor2, 0.5);
    float glowIntensity = glow * uGlow * 0.02;
    col += glowCol * glowIntensity;
    alpha += glowIntensity;
    
    // Final output with global opacity
    gl_FragColor = vec4(col, min(alpha * uOpacity, 1.0));
}
`;

const DNAHelix = ({
  primaryColor   = '#CBF696',
  secondaryColor = '#64B400',
  speed          = 0.6,
  scale          = 1.0,
  opacity        = 0.95,
  glow           = 0.8,
  mouseInteractive = true,
  className,
  style,
}) => {
  const containerRef = React.useRef(null);
  const oglRef       = React.useRef(null);
  const mouseRef     = React.useRef([0, 0]);
  const targetMouseRef = React.useRef([0, 0]);

  const hexToRgb = (hex) => {
    let c = hex.replace(/^#/, '');
    if (c.length === 3) c = c.split('').map(x => x + x).join('');
    const num = parseInt(c, 16);
    return [((num >> 16) & 255) / 255, ((num >> 8) & 255) / 255, (num & 255) / 255];
  };

  React.useEffect(() => {
    const ogl = window.ogl;
    if (!ogl) {
      console.warn('DNAHelix: OGL not found on window.');
      return;
    }
    const { Renderer, Program, Mesh, Triangle, Color } = ogl;

    const el = containerRef.current;
    if (!el) return;

    if (oglRef.current) {
      const p = oglRef.current;
      p.ro.disconnect();
      cancelAnimationFrame(p.raf);
      if (p.renderer.gl.canvas.parentElement === el) {
        el.removeChild(p.renderer.gl.canvas);
      }
      oglRef.current = null;
    }

    // This is a 70-step raymarch running over every pixel of the canvas, so the
    // render target's area is the whole cost of the shader. At dpr 2 on a retina
    // display that is ~14 megapixels a frame, which starves the compositor that
    // is driving the scroll. The SDFs are already smooth-min'd and the whole
    // thing sits at 0.5 opacity behind text, so neither the extra device pixels
    // nor MSAA are visible — dropping both is most of the frame budget back.
    const renderer = new Renderer({ alpha: true, antialias: false, dpr: Math.min(window.devicePixelRatio || 1, 1.25) });
    const gl = renderer.gl;
    el.appendChild(gl.canvas);
    gl.canvas.style.width = '100%';
    gl.canvas.style.height = '100%';
    gl.canvas.style.display = 'block';

    const geometry = new Triangle(gl);

    const program = new Program(gl, {
      vertex: VERTEX,
      fragment: FRAGMENT,
      uniforms: {
        uTime:       { value: 0 },
        uScroll:     { value: 0 },
        uResolution: { value: [1, 1] },
        uMouse:      { value: [0, 0] },
        uColor1:     { value: new Color(...hexToRgb(primaryColor)) },
        uColor2:     { value: new Color(...hexToRgb(secondaryColor)) },
        uOpacity:    { value: opacity },
        uGlow:       { value: glow },
        uScale:      { value: scale },
      },
      transparent: true,
      depthTest: false,
    });

    const mesh = new Mesh(gl, { geometry, program });

    const resize = () => {
      const w = el.clientWidth || 1;
      const h = el.clientHeight || 1;
      renderer.setSize(w, h);
      program.uniforms.uResolution.value = [w, h];
    };
    resize();
    const ro = new ResizeObserver(resize);
    ro.observe(el);

    const onMouseMove = (e) => {
      if (!mouseInteractive) return;
      // Normalize mouse to -1 to 1
      targetMouseRef.current[0] = (e.clientX / window.innerWidth) * 2 - 1;
      targetMouseRef.current[1] = -(e.clientY / window.innerHeight) * 2 + 1;
    };
    window.addEventListener('mousemove', onMouseMove, { passive: true });

    let raf = 0;
    let running = false;
    // Wall-clock time is paused along with the loop, so the helix picks up from
    // where it was rather than jumping forward by however long it was parked.
    let clock = 0;
    let lastT = 0;

    const loop = (t) => {
      raf = requestAnimationFrame(loop);
      clock += Math.min(t - lastT, 50); // a long stall must not snap the animation
      lastT = t;

      program.uniforms.uTime.value = clock * 0.001 * speed;
      // Read the scroll here rather than from a scroll listener: it is only ever
      // consumed by a frame we are actually drawing, and window.scrollY inside
      // rAF costs nothing (layout is already clean at that point).
      program.uniforms.uScroll.value = window.scrollY;

      // Smoothly interpolate mouse
      mouseRef.current[0] += (targetMouseRef.current[0] - mouseRef.current[0]) * 0.05;
      mouseRef.current[1] += (targetMouseRef.current[1] - mouseRef.current[1]) * 0.05;
      program.uniforms.uMouse.value = mouseRef.current;

      renderer.render({ scene: mesh });
    };

    const start = () => {
      if (running) return;
      running = true;
      lastT = performance.now();
      raf = requestAnimationFrame(loop);
    };
    const stop = () => {
      if (!running) return;
      running = false;
      cancelAnimationFrame(raf);
      raf = 0;
    };

    // The hero is the first screen of a long page, so for most of a visit this
    // shader is off-screen. Left running it keeps the GPU saturated and the
    // scroll compositing behind it — which is why the page felt heavy well past
    // the hero. Only draw while some of the canvas is actually on screen.
    //
    // Both triggers re-derive the answer rather than sharing a cached flag. A
    // page opened in a background tab gets no IntersectionObserver callbacks at
    // all — observer delivery rides the frame loop, which is frozen — so a flag
    // set only by the observer is still false when the tab is finally revealed,
    // and the shader would never start. Measuring costs one rect on a tab
    // switch or a viewport crossing, which is nothing.
    const MARGIN = 100;
    const shouldRun = () => {
      if (document.hidden) return false;
      const r = el.getBoundingClientRect();
      return r.bottom > -MARGIN && r.top < window.innerHeight + MARGIN;
    };
    const sync = () => (shouldRun() ? start() : stop());

    const io = new IntersectionObserver(sync, { rootMargin: `${MARGIN}px` });
    io.observe(el);
    document.addEventListener('visibilitychange', sync);

    // The observer's first delivery is a frame away, and on a tab that loads
    // hidden it never arrives; the hero is on screen at load, so just start.
    sync();

    oglRef.current = { renderer, ro, raf };

    return () => {
      ro.disconnect();
      io.disconnect();
      stop();
      document.removeEventListener('visibilitychange', sync);
      window.removeEventListener('mousemove', onMouseMove);
      if (gl.canvas.parentElement === el) {
        el.removeChild(gl.canvas);
      }
      oglRef.current = null;
    };
  }, [primaryColor, secondaryColor, speed, scale, opacity, glow, mouseInteractive]);

  return (
    <div
      ref={containerRef}
      className={`dna-helix-container ${className || ''}`}
      style={style}
      aria-label="Premium Raymarched 3D DNA animation"
    />
  );
};

window.DNAHelix = DNAHelix;
