/* Phasor — GPU fluid sim (Pavel Dobryakov style, condensed)
   Velocity advection -> divergence -> jacobi pressure -> gradient subtract
   Dye field advection + splats + decay; cheap radial bloom on composite.
   Driven by a synthetic 4/4 + pseudo-random "notes". No audio.
*/

const FluidCanvas = ({ density = 1.0, intensity = 1.0, paused = false, className = "" }) => {
  const canvasRef = React.useRef(null);
  const stateRef = React.useRef(null);

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

    const gl = canvas.getContext('webgl2', { alpha: false, antialias: false, depth: false, stencil: false, preserveDrawingBuffer: false });
    if (!gl) {
      // fallback: gradient
      const ctx2d = canvas.getContext('2d');
      if (ctx2d) {
        const grad = ctx2d.createRadialGradient(canvas.width/2, canvas.height/2, 0, canvas.width/2, canvas.height/2, canvas.width/2);
        grad.addColorStop(0, '#1a0030'); grad.addColorStop(1, '#000');
        ctx2d.fillStyle = grad; ctx2d.fillRect(0,0,canvas.width,canvas.height);
      }
      return;
    }

    const ext = {
      halfFloat: gl.getExtension('EXT_color_buffer_float') || gl.getExtension('EXT_color_buffer_half_float'),
    };
    const internalRGBA = gl.RGBA16F;
    const internalRG = gl.RG16F;
    const internalR = gl.R16F;
    const texType = gl.HALF_FLOAT;

    // ----- shaders -----
    const vsSrc = `#version 300 es
      precision highp float;
      in vec2 aPos;
      out vec2 vUv;
      out vec2 vL; out vec2 vR; out vec2 vT; out vec2 vB;
      uniform vec2 texelSize;
      void main() {
        vUv = aPos * 0.5 + 0.5;
        vL = vUv - vec2(texelSize.x, 0.0);
        vR = vUv + vec2(texelSize.x, 0.0);
        vT = vUv + vec2(0.0, texelSize.y);
        vB = vUv - vec2(0.0, texelSize.y);
        gl_Position = vec4(aPos, 0.0, 1.0);
      }`;

    const fsClear = `#version 300 es
      precision highp float; in vec2 vUv;
      uniform sampler2D uTexture; uniform float uValue;
      out vec4 frag;
      void main() { frag = uValue * texture(uTexture, vUv); }`;

    const fsDisplay = `#version 300 es
      precision highp float; in vec2 vUv;
      uniform sampler2D uTexture;
      uniform float uTime;
      out vec4 frag;
      // cheap separable bloom by sampling at 5 offsets + threshold
      vec3 sampleBloom(vec2 uv) {
        vec3 c = vec3(0.0);
        float sum = 0.0;
        for (int i = -3; i <= 3; i++) {
          float w = exp(-float(i*i) * 0.18);
          vec2 off = vec2(float(i)) * 0.0035;
          c += w * texture(uTexture, uv + off).rgb;
          c += w * texture(uTexture, uv + off.yx).rgb;
          sum += 2.0 * w;
        }
        return c / sum;
      }
      void main() {
        vec3 base = texture(uTexture, vUv).rgb;
        vec3 bright = max(base - 0.35, 0.0);
        vec3 bloom = sampleBloom(vUv) * 0.55;
        bright = max(bloom - 0.2, 0.0);
        vec3 col = base + bloom * 0.9;
        // vignette + subtle background gradient bloom
        float d = distance(vUv, vec2(0.5));
        col *= smoothstep(1.0, 0.2, d * 1.4);
        // subtle dark bloom backdrop
        col += 0.04 * vec3(0.05, 0.02, 0.08) * (1.0 - d);
        frag = vec4(col, 1.0);
      }`;

    const fsSplat = `#version 300 es
      precision highp float; in vec2 vUv;
      uniform sampler2D uTarget;
      uniform float uAspect;
      uniform vec3 uColor;
      uniform vec2 uPoint;
      uniform float uRadius;
      out vec4 frag;
      void main() {
        vec2 p = vUv - uPoint;
        p.x *= uAspect;
        vec3 splat = exp(-dot(p,p) / uRadius) * uColor;
        vec3 base = texture(uTarget, vUv).xyz;
        frag = vec4(base + splat, 1.0);
      }`;

    const fsAdvection = `#version 300 es
      precision highp float; in vec2 vUv;
      uniform sampler2D uVelocity;
      uniform sampler2D uSource;
      uniform vec2 texelSize;
      uniform float dt;
      uniform float dissipation;
      out vec4 frag;
      void main() {
        vec2 coord = vUv - dt * texture(uVelocity, vUv).xy * texelSize;
        vec4 result = texture(uSource, coord);
        float decay = 1.0 + dissipation * dt;
        frag = result / decay;
      }`;

    const fsDivergence = `#version 300 es
      precision highp float;
      in vec2 vUv; in vec2 vL; in vec2 vR; in vec2 vT; in vec2 vB;
      uniform sampler2D uVelocity;
      out vec4 frag;
      void main() {
        float L = texture(uVelocity, vL).x;
        float R = texture(uVelocity, vR).x;
        float T = texture(uVelocity, vT).y;
        float B = texture(uVelocity, vB).y;
        vec2 C = texture(uVelocity, vUv).xy;
        if (vL.x < 0.0) L = -C.x;
        if (vR.x > 1.0) R = -C.x;
        if (vT.y > 1.0) T = -C.y;
        if (vB.y < 0.0) B = -C.y;
        float div = 0.5 * (R - L + T - B);
        frag = vec4(div, 0.0, 0.0, 1.0);
      }`;

    const fsPressure = `#version 300 es
      precision highp float;
      in vec2 vUv; in vec2 vL; in vec2 vR; in vec2 vT; in vec2 vB;
      uniform sampler2D uPressure;
      uniform sampler2D uDivergence;
      out vec4 frag;
      void main() {
        float L = texture(uPressure, vL).x;
        float R = texture(uPressure, vR).x;
        float T = texture(uPressure, vT).x;
        float B = texture(uPressure, vB).x;
        float divergence = texture(uDivergence, vUv).x;
        float pressure = (L + R + B + T - divergence) * 0.25;
        frag = vec4(pressure, 0.0, 0.0, 1.0);
      }`;

    const fsGradientSubtract = `#version 300 es
      precision highp float;
      in vec2 vUv; in vec2 vL; in vec2 vR; in vec2 vT; in vec2 vB;
      uniform sampler2D uPressure;
      uniform sampler2D uVelocity;
      out vec4 frag;
      void main() {
        float L = texture(uPressure, vL).x;
        float R = texture(uPressure, vR).x;
        float T = texture(uPressure, vT).x;
        float B = texture(uPressure, vB).x;
        vec2 v = texture(uVelocity, vUv).xy;
        v -= vec2(R - L, T - B);
        frag = vec4(v, 0.0, 1.0);
      }`;

    const fsCurl = `#version 300 es
      precision highp float;
      in vec2 vUv; in vec2 vL; in vec2 vR; in vec2 vT; in vec2 vB;
      uniform sampler2D uVelocity;
      out vec4 frag;
      void main() {
        float L = texture(uVelocity, vL).y;
        float R = texture(uVelocity, vR).y;
        float T = texture(uVelocity, vT).x;
        float B = texture(uVelocity, vB).x;
        float vorticity = R - L - T + B;
        frag = vec4(0.5 * vorticity, 0.0, 0.0, 1.0);
      }`;

    const fsVorticity = `#version 300 es
      precision highp float;
      in vec2 vUv; in vec2 vL; in vec2 vR; in vec2 vT; in vec2 vB;
      uniform sampler2D uVelocity;
      uniform sampler2D uCurl;
      uniform float curl;
      uniform float dt;
      out vec4 frag;
      void main() {
        float L = texture(uCurl, vL).x;
        float R = texture(uCurl, vR).x;
        float T = texture(uCurl, vT).x;
        float B = texture(uCurl, vB).x;
        float C = texture(uCurl, vUv).x;
        vec2 force = 0.5 * vec2(abs(T) - abs(B), abs(R) - abs(L));
        force /= length(force) + 0.0001;
        force *= curl * C;
        force.y *= -1.0;
        vec2 v = texture(uVelocity, vUv).xy;
        v += force * dt;
        v = clamp(v, -1000.0, 1000.0);
        frag = vec4(v, 0.0, 1.0);
      }`;

    function compile(type, src) {
      const s = gl.createShader(type);
      gl.shaderSource(s, src);
      gl.compileShader(s);
      if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) {
        console.error(gl.getShaderInfoLog(s), src);
        return null;
      }
      return s;
    }

    function program(vs, fs) {
      const p = gl.createProgram();
      gl.attachShader(p, compile(gl.VERTEX_SHADER, vs));
      gl.attachShader(p, compile(gl.FRAGMENT_SHADER, fs));
      gl.linkProgram(p);
      if (!gl.getProgramParameter(p, gl.LINK_STATUS)) {
        console.error(gl.getProgramInfoLog(p));
        return null;
      }
      // capture uniforms
      const uniforms = {};
      const n = gl.getProgramParameter(p, gl.ACTIVE_UNIFORMS);
      for (let i = 0; i < n; i++) {
        const u = gl.getActiveUniform(p, i);
        uniforms[u.name] = gl.getUniformLocation(p, u.name);
      }
      return { program: p, uniforms };
    }

    const progClear = program(vsSrc, fsClear);
    const progDisplay = program(vsSrc, fsDisplay);
    const progSplat = program(vsSrc, fsSplat);
    const progAdvection = program(vsSrc, fsAdvection);
    const progDivergence = program(vsSrc, fsDivergence);
    const progPressure = program(vsSrc, fsPressure);
    const progGradient = program(vsSrc, fsGradientSubtract);
    const progCurl = program(vsSrc, fsCurl);
    const progVorticity = program(vsSrc, fsVorticity);

    // fullscreen triangle
    const vbo = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1, 3,-1, -1,3]), gl.STATIC_DRAW);

    function bindAttribs(prog) {
      const loc = gl.getAttribLocation(prog, 'aPos');
      gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
      gl.enableVertexAttribArray(loc);
      gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);
    }

    function createFBO(w, h, internal, format, type) {
      const tex = gl.createTexture();
      gl.bindTexture(gl.TEXTURE_2D, tex);
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
      gl.texImage2D(gl.TEXTURE_2D, 0, internal, w, h, 0, format, type, null);
      const fbo = gl.createFramebuffer();
      gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
      gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0);
      gl.viewport(0,0,w,h);
      gl.clearColor(0,0,0,1); gl.clear(gl.COLOR_BUFFER_BIT);
      return { tex, fbo, w, h, texelSizeX: 1/w, texelSizeY: 1/h };
    }
    function createDouble(w, h, internal, format, type) {
      let a = createFBO(w,h,internal,format,type);
      let b = createFBO(w,h,internal,format,type);
      return {
        get read() { return a; },
        get write() { return b; },
        swap() { const t = a; a = b; b = t; },
        w, h,
      };
    }

    // Sim resolution scaled to canvas
    function getResolution(target) {
      const aspect = canvas.clientWidth / canvas.clientHeight;
      const min = Math.round(target);
      const max = Math.round(target * aspect);
      if (aspect < 1) return { w: min, h: max };
      return { w: max, h: min };
    }

    let dyeRes, simRes;
    let dye, velocity, divergence, curl, pressure;

    function init() {
      const dpr = Math.min(window.devicePixelRatio || 1, 1.5);
      canvas.width = Math.floor(canvas.clientWidth * dpr);
      canvas.height = Math.floor(canvas.clientHeight * dpr);

      const dyeR = getResolution(512 * density);
      const simR = getResolution(160);
      dyeRes = dyeR; simRes = simR;

      dye = createDouble(dyeR.w, dyeR.h, internalRGBA, gl.RGBA, texType);
      velocity = createDouble(simR.w, simR.h, internalRG, gl.RG, texType);
      divergence = createFBO(simR.w, simR.h, internalR, gl.RED, texType);
      curl = createFBO(simR.w, simR.h, internalR, gl.RED, texType);
      pressure = createDouble(simR.w, simR.h, internalR, gl.RED, texType);
    }
    init();
    const ro = new ResizeObserver(() => { init(); });
    ro.observe(canvas);

    function blit(target) {
      if (target == null) {
        gl.bindFramebuffer(gl.FRAMEBUFFER, null);
        gl.viewport(0,0, gl.drawingBufferWidth, gl.drawingBufferHeight);
      } else {
        gl.bindFramebuffer(gl.FRAMEBUFFER, target.fbo);
        gl.viewport(0,0, target.w, target.h);
      }
      gl.drawArrays(gl.TRIANGLES, 0, 3);
    }

    function splat(x, y, dx, dy, color) {
      // velocity splat
      gl.useProgram(progSplat.program); bindAttribs(progSplat.program);
      gl.uniform1i(progSplat.uniforms.uTarget, 0);
      gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, velocity.read.tex);
      gl.uniform1f(progSplat.uniforms.uAspect, canvas.width / canvas.height);
      gl.uniform2f(progSplat.uniforms.uPoint, x, y);
      gl.uniform3f(progSplat.uniforms.uColor, dx, dy, 0.0);
      gl.uniform1f(progSplat.uniforms.uRadius, 0.0008);
      blit(velocity.write); velocity.swap();

      // dye splat
      gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, dye.read.tex);
      gl.uniform3f(progSplat.uniforms.uColor, color[0], color[1], color[2]);
      gl.uniform1f(progSplat.uniforms.uRadius, 0.0014);
      blit(dye.write); dye.swap();
    }

    // ---- synthetic music driver ----
    const BPM = 124;
    const beatMs = 60000 / BPM;
    let lastBeat = performance.now();
    let beatIdx = 0;
    // Phasor palette: cyan / magenta / deep violet / acid green
    const palette = [
      [0.00, 0.55, 0.85],   // cyan
      [0.85, 0.10, 0.55],   // magenta
      [0.40, 0.10, 0.85],   // deep violet
      [0.45, 0.85, 0.10],   // acid green
    ];

    function rand(min, max) { return Math.random() * (max - min) + min; }

    function scheduleSplats(now) {
      // beat splats
      while (now - lastBeat >= beatMs) {
        lastBeat += beatMs;
        beatIdx++;
        const downbeat = beatIdx % 4 === 0;
        const accent = beatIdx % 8 === 0;
        const c = palette[(beatIdx + (downbeat?0:1)) % palette.length].map(v => v * intensity * (accent ? 1.8 : (downbeat ? 1.2 : 0.9)));
        const x = rand(0.15, 0.85);
        const y = rand(0.2, 0.8);
        const ang = rand(0, Math.PI * 2);
        const force = (downbeat ? 1500 : 800) * intensity;
        splat(x, y, Math.cos(ang) * force, Math.sin(ang) * force, c);
      }
      // off-beat micro notes
      if (Math.random() < 0.07) {
        const c = palette[Math.floor(Math.random() * palette.length)].map(v => v * 0.5 * intensity);
        splat(rand(0.1, 0.9), rand(0.1, 0.9), rand(-600,600), rand(-600,600), c);
      }
    }

    let raf = 0;
    let lastT = performance.now();
    let visible = true;
    let pausedRef = paused;
    stateRef.current = {
      setPaused(p) { pausedRef = p; },
      setVisible(v) { visible = v; },
      splatRandom() {
        const c = palette[Math.floor(Math.random() * palette.length)].map(v => v * 1.4 * intensity);
        splat(rand(0.2, 0.8), rand(0.2, 0.8), rand(-2000,2000), rand(-2000,2000), c);
      },
    };

    function step(now) {
      raf = requestAnimationFrame(step);
      if (pausedRef || !visible) { lastT = now; return; }
      let dt = Math.min(0.016, (now - lastT) / 1000);
      lastT = now;

      scheduleSplats(now);

      // CURL
      gl.useProgram(progCurl.program); bindAttribs(progCurl.program);
      gl.uniform2f(progCurl.uniforms.texelSize, 1/simRes.w, 1/simRes.h);
      gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, velocity.read.tex);
      gl.uniform1i(progCurl.uniforms.uVelocity, 0);
      blit(curl);

      // VORTICITY
      gl.useProgram(progVorticity.program); bindAttribs(progVorticity.program);
      gl.uniform2f(progVorticity.uniforms.texelSize, 1/simRes.w, 1/simRes.h);
      gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, velocity.read.tex);
      gl.uniform1i(progVorticity.uniforms.uVelocity, 0);
      gl.activeTexture(gl.TEXTURE1); gl.bindTexture(gl.TEXTURE_2D, curl.tex);
      gl.uniform1i(progVorticity.uniforms.uCurl, 1);
      gl.uniform1f(progVorticity.uniforms.curl, 30.0);
      gl.uniform1f(progVorticity.uniforms.dt, dt);
      blit(velocity.write); velocity.swap();

      // DIVERGENCE
      gl.useProgram(progDivergence.program); bindAttribs(progDivergence.program);
      gl.uniform2f(progDivergence.uniforms.texelSize, 1/simRes.w, 1/simRes.h);
      gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, velocity.read.tex);
      gl.uniform1i(progDivergence.uniforms.uVelocity, 0);
      blit(divergence);

      // PRESSURE jacobi
      gl.useProgram(progClear.program); bindAttribs(progClear.program);
      gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, pressure.read.tex);
      gl.uniform1i(progClear.uniforms.uTexture, 0);
      gl.uniform1f(progClear.uniforms.uValue, 0.8);
      blit(pressure.write); pressure.swap();

      gl.useProgram(progPressure.program); bindAttribs(progPressure.program);
      gl.uniform2f(progPressure.uniforms.texelSize, 1/simRes.w, 1/simRes.h);
      for (let i = 0; i < 18; i++) {
        gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, pressure.read.tex);
        gl.uniform1i(progPressure.uniforms.uPressure, 0);
        gl.activeTexture(gl.TEXTURE1); gl.bindTexture(gl.TEXTURE_2D, divergence.tex);
        gl.uniform1i(progPressure.uniforms.uDivergence, 1);
        blit(pressure.write); pressure.swap();
      }

      // GRADIENT SUBTRACT
      gl.useProgram(progGradient.program); bindAttribs(progGradient.program);
      gl.uniform2f(progGradient.uniforms.texelSize, 1/simRes.w, 1/simRes.h);
      gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, pressure.read.tex);
      gl.uniform1i(progGradient.uniforms.uPressure, 0);
      gl.activeTexture(gl.TEXTURE1); gl.bindTexture(gl.TEXTURE_2D, velocity.read.tex);
      gl.uniform1i(progGradient.uniforms.uVelocity, 1);
      blit(velocity.write); velocity.swap();

      // ADVECT velocity
      gl.useProgram(progAdvection.program); bindAttribs(progAdvection.program);
      gl.uniform2f(progAdvection.uniforms.texelSize, 1/simRes.w, 1/simRes.h);
      gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, velocity.read.tex);
      gl.uniform1i(progAdvection.uniforms.uVelocity, 0);
      gl.activeTexture(gl.TEXTURE1); gl.bindTexture(gl.TEXTURE_2D, velocity.read.tex);
      gl.uniform1i(progAdvection.uniforms.uSource, 1);
      gl.uniform1f(progAdvection.uniforms.dt, dt);
      gl.uniform1f(progAdvection.uniforms.dissipation, 0.2);
      blit(velocity.write); velocity.swap();

      // ADVECT dye
      gl.uniform2f(progAdvection.uniforms.texelSize, 1/dyeRes.w, 1/dyeRes.h);
      gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, velocity.read.tex);
      gl.uniform1i(progAdvection.uniforms.uVelocity, 0);
      gl.activeTexture(gl.TEXTURE1); gl.bindTexture(gl.TEXTURE_2D, dye.read.tex);
      gl.uniform1i(progAdvection.uniforms.uSource, 1);
      gl.uniform1f(progAdvection.uniforms.dt, dt);
      gl.uniform1f(progAdvection.uniforms.dissipation, 1.0);
      blit(dye.write); dye.swap();

      // DISPLAY
      gl.useProgram(progDisplay.program); bindAttribs(progDisplay.program);
      gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, dye.read.tex);
      gl.uniform1i(progDisplay.uniforms.uTexture, 0);
      gl.uniform1f(progDisplay.uniforms.uTime, now * 0.001);
      blit(null);
    }
    raf = requestAnimationFrame(step);

    // mouse interaction: drag to splat
    let dragging = false, lastX = 0, lastY = 0;
    const onDown = (e) => {
      dragging = true;
      const r = canvas.getBoundingClientRect();
      lastX = (e.clientX - r.left) / r.width;
      lastY = 1.0 - (e.clientY - r.top) / r.height;
    };
    const onMove = (e) => {
      if (!dragging) return;
      const r = canvas.getBoundingClientRect();
      const x = (e.clientX - r.left) / r.width;
      const y = 1.0 - (e.clientY - r.top) / r.height;
      const dx = (x - lastX) * 4000;
      const dy = (y - lastY) * 4000;
      const c = palette[Math.floor(Math.random() * palette.length)].map(v => v * 1.2);
      splat(x, y, dx, dy, c);
      lastX = x; lastY = y;
    };
    const onUp = () => { dragging = false; };
    canvas.addEventListener('pointerdown', onDown);
    window.addEventListener('pointermove', onMove);
    window.addEventListener('pointerup', onUp);

    return () => {
      cancelAnimationFrame(raf);
      ro.disconnect();
      canvas.removeEventListener('pointerdown', onDown);
      window.removeEventListener('pointermove', onMove);
      window.removeEventListener('pointerup', onUp);
    };
  }, [density, intensity]);

  // visibility pause via IntersectionObserver
  React.useEffect(() => {
    if (!canvasRef.current) return;
    const io = new IntersectionObserver((entries) => {
      for (const e of entries) {
        if (stateRef.current) stateRef.current.setVisible(e.isIntersecting);
      }
    }, { threshold: 0.01 });
    io.observe(canvasRef.current);
    return () => io.disconnect();
  }, []);

  React.useEffect(() => {
    if (stateRef.current) stateRef.current.setPaused(paused);
  }, [paused]);

  return <canvas ref={canvasRef} className={`block w-full h-full cursor-cross ${className}`} />;
};

window.FluidCanvas = FluidCanvas;
