/**
 * PrismaticBurst.js — Vanilla WebGL2 port of @react-bits/PrismaticBurst
 * No dependencies — pure WebGL2 implementation.
 */

const PRISMATIC_VERT = `#version 300 es
in vec2 position;
in vec2 uv;
out vec2 vUv;
void main() {
    vUv = uv;
    gl_Position = vec4(position, 0.0, 1.0);
}`;

const PRISMATIC_FRAG = `#version 300 es
precision highp float;
precision highp int;

out vec4 fragColor;

uniform vec2  uResolution;
uniform float uTime;
uniform float uIntensity;
uniform float uSpeed;
uniform int   uAnimType;
uniform vec2  uMouse;
uniform int   uColorCount;
uniform float uDistort;
uniform vec2  uOffset;
uniform sampler2D uGradient;
uniform float uNoiseAmount;
uniform int   uRayCount;

float hash21(vec2 p){
    p = floor(p);
    float f = 52.9829189 * fract(dot(p, vec2(0.065, 0.005)));
    return fract(f);
}

mat2 rot30(){ return mat2(0.8, -0.5, 0.5, 0.8); }

float layeredNoise(vec2 fragPx){
    vec2 p = mod(fragPx + vec2(uTime * 30.0, -uTime * 21.0), 1024.0);
    vec2 q = rot30() * p;
    float n = 0.0;
    n += 0.40 * hash21(q);
    n += 0.25 * hash21(q * 2.0 + 17.0);
    n += 0.20 * hash21(q * 4.0 + 47.0);
    n += 0.10 * hash21(q * 8.0 + 113.0);
    n += 0.05 * hash21(q * 16.0 + 191.0);
    return n;
}

vec3 rayDir(vec2 frag, vec2 res, vec2 offset, float dist){
    float focal = res.y * max(dist, 1e-3);
    return normalize(vec3(2.0 * (frag - offset) - res, focal));
}

float edgeFade(vec2 frag, vec2 res, vec2 offset){
    vec2 toC = frag - 0.5 * res - offset;
    float r = length(toC) / (0.5 * min(res.x, res.y));
    float x = clamp(r, 0.0, 1.0);
    float q = x * x * x * (x * (x * 6.0 - 15.0) + 10.0);
    float s = q * 0.5;
    s = pow(s, 1.5);
    float tail = 1.0 - pow(1.0 - s, 2.0);
    s = mix(s, tail, 0.2);
    float dn = (layeredNoise(frag * 0.15) - 0.5) * 0.0015 * s;
    return clamp(s + dn, 0.0, 1.0);
}

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 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 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); }

vec3 sampleGradient(float t){
    t = clamp(t, 0.0, 1.0);
    return texture(uGradient, vec2(t, 0.5)).rgb;
}

vec2 rot2(vec2 v, float a){
    float s = sin(a), c = cos(a);
    return mat2(c, -s, s, c) * v;
}

float bendAngle(vec3 q, float t){
    float a = 0.8 * sin(q.x * 0.55 + t * 0.6)
            + 0.7 * sin(q.y * 0.50 - t * 0.5)
            + 0.6 * sin(q.z * 0.60 + t * 0.7);
    return a;
}

void main(){
    vec2 frag = gl_FragCoord.xy;
    float t = uTime * uSpeed;
    float jitterAmp = 0.1 * clamp(uNoiseAmount, 0.0, 1.0);
    vec3 dir = rayDir(frag, uResolution, uOffset, 1.0);
    float marchT = 0.0;
    vec3 col = vec3(0.0);
    float n = layeredNoise(frag);
    vec4 c = cos(t * 0.2 + vec4(0.0, 33.0, 11.0, 0.0));
    mat2 M2 = mat2(c.x, c.y, c.z, c.w);
    float amp = clamp(uDistort, 0.0, 50.0) * 0.15;

    mat3 rot3dMat = mat3(1.0);
    if(uAnimType == 1){
        vec3 ang = vec3(t * 0.31, t * 0.21, t * 0.17);
        rot3dMat = rotZ(ang.z) * rotY(ang.y) * rotX(ang.x);
    }
    mat3 hoverMat = mat3(1.0);
    if(uAnimType == 2){
        vec2 m = uMouse * 2.0 - 1.0;
        vec3 ang = vec3(m.y * 0.6, m.x * 0.6, 0.0);
        hoverMat = rotY(ang.y) * rotX(ang.x);
    }

    for (int i = 0; i < 44; ++i) {
        vec3 P = marchT * dir;
        P.z -= 2.0;
        float rad = length(P);
        vec3 Pl = P * (10.0 / max(rad, 1e-6));

        if(uAnimType == 0){
            Pl.xz *= M2;
        } else if(uAnimType == 1){
            Pl = rot3dMat * Pl;
        } else {
            Pl = hoverMat * Pl;
        }

        float stepLen = min(rad - 0.3, n * jitterAmp) + 0.1;

        float grow = smoothstep(0.35, 3.0, marchT);
        float a1 = amp * grow * bendAngle(Pl * 0.6, t);
        float a2 = 0.5 * amp * grow * bendAngle(Pl.zyx * 0.5 + 3.1, t * 0.9);
        vec3 Pb = Pl;
        Pb.xz = rot2(Pb.xz, a1);
        Pb.xy = rot2(Pb.xy, a2);

        float rayPattern = smoothstep(
            0.5, 0.7,
            sin(Pb.x + cos(Pb.y) * cos(Pb.z)) *
            sin(Pb.z + sin(Pb.y) * cos(Pb.x + t))
        );

        if (uRayCount > 0) {
            float ang = atan(Pb.y, Pb.x);
            float comb = 0.5 + 0.5 * cos(float(uRayCount) * ang);
            comb = pow(comb, 3.0);
            rayPattern *= smoothstep(0.15, 0.95, comb);
        }

        vec3 spectralDefault = 1.0 + vec3(
            cos(marchT * 3.0 + 0.0),
            cos(marchT * 3.0 + 1.0),
            cos(marchT * 3.0 + 2.0)
        );

        float saw = fract(marchT * 0.25);
        float tRay = saw * saw * (3.0 - 2.0 * saw);
        vec3 userGradient = 2.0 * sampleGradient(tRay);
        vec3 spectral = (uColorCount > 0) ? userGradient : spectralDefault;
        vec3 base = (0.05 / (0.4 + stepLen))
                  * smoothstep(5.0, 0.0, rad)
                  * spectral;

        col += base * rayPattern;
        marchT += stepLen;
    }

    col *= edgeFade(frag, uResolution, uOffset);
    col *= uIntensity;

    fragColor = vec4(clamp(col, 0.0, 1.0), 1.0);
}`;

function hexToRgb01(hex) {
  let h = (hex || '').trim();
  if (h.startsWith('#')) h = h.slice(1);
  if (h.length === 3) h = h[0]+h[0]+h[1]+h[1]+h[2]+h[2];
  const v = parseInt(h, 16);
  if (isNaN(v)) return [1, 1, 1];
  return [((v >> 16) & 255) / 255, ((v >> 8) & 255) / 255, (v & 255) / 255];
}

function compileShader(gl, type, src) {
  const sh = gl.createShader(type);
  gl.shaderSource(sh, src);
  gl.compileShader(sh);
  if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) {
    console.error('[PrismaticBurst] Shader error:', gl.getShaderInfoLog(sh));
    gl.deleteShader(sh);
    return null;
  }
  return sh;
}

function createProgram(gl, vsrc, fsrc) {
  const vs = compileShader(gl, gl.VERTEX_SHADER, vsrc);
  const fs = compileShader(gl, gl.FRAGMENT_SHADER, fsrc);
  if (!vs || !fs) return null;
  const prog = gl.createProgram();
  gl.attachShader(prog, vs);
  gl.attachShader(prog, fs);
  gl.bindAttribLocation(prog, 0, 'position');
  gl.bindAttribLocation(prog, 1, 'uv');
  gl.linkProgram(prog);
  gl.deleteShader(vs);
  gl.deleteShader(fs);
  if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
    console.error('[PrismaticBurst] Program link error:', gl.getProgramInfoLog(prog));
    return null;
  }
  return prog;
}

const ANIM_MAP = { rotate: 0, rotate3d: 1, hover: 2 };

class PrismaticBurst {
  constructor(canvas, options = {}) {
    this.canvas = canvas;
    this.opts = Object.assign({
      intensity: 2,
      speed: 0.5,
      animationType: 'rotate3d',
      colors: ['#7B2FFF', '#E040FB', '#00D4FF'],
      distort: 0,
      offset: { x: 0, y: 0 },
      hoverDampness: 0.25,
      rayCount: 0,
      mixBlendMode: 'lighten',
      paused: false,
    }, options);

    this.gl = null;
    this.program = null;
    this.locs = {};
    this.vao = null;
    this.gradTex = null;
    this.accumTime = 0;
    this.lastTime = performance.now();
    this.paused = this.opts.paused;
    this.mouseTarget = [0.5, 0.5];
    this.mouseSmooth = [0.5, 0.5];
    this.raf = 0;
    this.destroyed = false;

    this._init();
  }

  _init() {
    const canvas = this.canvas;
    canvas.style.mixBlendMode = this.opts.mixBlendMode || '';

    const gl = canvas.getContext('webgl2', {
      alpha: false,
      antialias: false,
      premultipliedAlpha: false,
    });
    if (!gl) { console.error('[PrismaticBurst] WebGL2 not supported'); return; }
    this.gl = gl;

    // Create program
    const prog = createProgram(gl, PRISMATIC_VERT, PRISMATIC_FRAG);
    if (!prog) return;
    this.program = prog;
    gl.useProgram(prog);

    // Cache uniform locations
    const unifNames = [
      'uResolution','uTime','uIntensity','uSpeed','uAnimType',
      'uMouse','uColorCount','uDistort','uOffset','uGradient',
      'uNoiseAmount','uRayCount'
    ];
    unifNames.forEach(n => { this.locs[n] = gl.getUniformLocation(prog, n); });

    // Set static uniforms
    gl.uniform1f(this.locs.uNoiseAmount, 0.8);
    gl.uniform1i(this.locs.uGradient, 0);

    // Full-screen triangle (OGL Triangle equivalent)
    //  3 vertices that cover the full NDC space
    const posData = new Float32Array([-1, -1,  3, -1,  -1,  3]);
    const uvData  = new Float32Array([ 0,  0,  2,  0,   0,  2]);

    this.vao = gl.createVertexArray();
    gl.bindVertexArray(this.vao);

    const posBuf = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, posBuf);
    gl.bufferData(gl.ARRAY_BUFFER, posData, gl.STATIC_DRAW);
    gl.enableVertexAttribArray(0);
    gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);

    const uvBuf = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, uvBuf);
    gl.bufferData(gl.ARRAY_BUFFER, uvData, gl.STATIC_DRAW);
    gl.enableVertexAttribArray(1);
    gl.vertexAttribPointer(1, 2, gl.FLOAT, false, 0, 0);

    gl.bindVertexArray(null);

    // Gradient texture
    this.gradTex = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_2D, this.gradTex);
    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);
    this._updateGradient(this.opts.colors);

    // Event listeners
    const container = canvas.parentElement;
    this._onPointer = (e) => {
      const rect = container.getBoundingClientRect();
      this.mouseTarget = [
        Math.min(Math.max((e.clientX - rect.left) / Math.max(rect.width, 1), 0), 1),
        Math.min(Math.max((e.clientY - rect.top)  / Math.max(rect.height, 1), 0), 1),
      ];
    };
    container.addEventListener('pointermove', this._onPointer, { passive: true });

    this._onResize = () => this._resize();
    const ro = new ResizeObserver(this._onResize);
    ro.observe(canvas.parentElement);
    this._ro = ro;
    this._resize();

    // Start loop
    this._loop();
  }

  _resize() {
    const gl = this.gl;
    if (!gl) return;
    const container = this.canvas.parentElement;
    const dpr = Math.min(window.devicePixelRatio || 1, 2);
    const w = container.clientWidth  || window.innerWidth;
    const h = container.clientHeight || window.innerHeight;
    this.canvas.width  = Math.round(w * dpr);
    this.canvas.height = Math.round(h * dpr);
    gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
    gl.useProgram(this.program);
    gl.uniform2f(this.locs.uResolution, gl.drawingBufferWidth, gl.drawingBufferHeight);
  }

  _updateGradient(colors) {
    const gl = this.gl;
    if (!gl) return;
    const cols = (colors || []).slice(0, 64);
    if (cols.length === 0) {
      gl.bindTexture(gl.TEXTURE_2D, this.gradTex);
      const px = new Uint8Array([255,255,255,255]);
      gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, px);
      gl.useProgram(this.program);
      gl.uniform1i(this.locs.uColorCount, 0);
      return;
    }
    const data = new Uint8Array(cols.length * 4);
    cols.forEach((hex, i) => {
      const [r, g, b] = hexToRgb01(hex);
      data[i*4]   = Math.round(r * 255);
      data[i*4+1] = Math.round(g * 255);
      data[i*4+2] = Math.round(b * 255);
      data[i*4+3] = 255;
    });
    gl.bindTexture(gl.TEXTURE_2D, this.gradTex);
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, cols.length, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, data);
    gl.useProgram(this.program);
    gl.uniform1i(this.locs.uColorCount, cols.length);
  }

  _loop() {
    if (this.destroyed) return;
    this.raf = requestAnimationFrame((now) => {
      const elapsed = now - this.lastTime;
      
      // Limitar para ~30 FPS (1000ms / 30 = 33.33ms)
      if (elapsed < 33) {
        this._loop();
        return;
      }

      const dt = Math.min(elapsed * 0.001, 0.05);
      this.lastTime = now;

      // Se estiver pausado pelo ScrollTrigger, ignora a renderização e o tempo
      if (this.paused) {
        this._loop();
        return;
      }

      this.accumTime += dt;

      // Smooth mouse
      const damp = this.opts.hoverDampness;
      const tau = 0.02 + Math.max(0, Math.min(1, damp)) * 0.5;
      const alpha = 1 - Math.exp(-dt / tau);
      this.mouseSmooth[0] += (this.mouseTarget[0] - this.mouseSmooth[0]) * alpha;
      this.mouseSmooth[1] += (this.mouseTarget[1] - this.mouseSmooth[1]) * alpha;

      this._render();
      this._loop();
    });
  }

  _render() {
    const gl = this.gl;
    const prog = this.program;
    if (!gl || !prog) return;

    gl.useProgram(prog);
    gl.bindVertexArray(this.vao);

    gl.activeTexture(gl.TEXTURE0);
    gl.bindTexture(gl.TEXTURE_2D, this.gradTex);

    // Update per-frame uniforms
    gl.uniform1f(this.locs.uTime, this.accumTime);
    gl.uniform2fv(this.locs.uMouse, this.mouseSmooth);

    // Config uniforms
    gl.uniform1f(this.locs.uIntensity, this.opts.intensity);
    gl.uniform1f(this.locs.uSpeed, this.opts.speed);
    gl.uniform1i(this.locs.uAnimType, ANIM_MAP[this.opts.animationType] ?? 1);
    gl.uniform1f(this.locs.uDistort, this.opts.distort);
    gl.uniform2f(this.locs.uOffset, this.opts.offset.x || 0, this.opts.offset.y || 0);
    gl.uniform1i(this.locs.uRayCount, Math.max(0, Math.floor(this.opts.rayCount || 0)));

    gl.drawArrays(gl.TRIANGLES, 0, 3);
    gl.bindVertexArray(null);
  }

  destroy() {
    this.destroyed = true;
    cancelAnimationFrame(this.raf);
    if (this._ro) this._ro.disconnect();
    const container = this.canvas?.parentElement;
    if (container && this._onPointer) {
      container.removeEventListener('pointermove', this._onPointer);
    }
    const gl = this.gl;
    if (gl) {
      gl.deleteProgram(this.program);
      gl.deleteTexture(this.gradTex);
      gl.deleteVertexArray(this.vao);
    }
  }
}

window.PrismaticBurst = PrismaticBurst;
