/**
 * Ambient background animation — WebGL shader + floating particles.
 *
 * Renders a smooth flowing noise field with accent-colored highlights
 * plus drifting glowing particles, all in a single GPU shader pass.
 */

const PARTICLE_COUNT = 40;

const VERT_SRC = `
attribute vec2 a_pos;
void main() { gl_Position = vec4(a_pos, 0.0, 1.0); }
`;

const FRAG_SRC = `
precision mediump float;
uniform float u_time;
uniform vec2  u_res;
uniform vec3  u_accent;
uniform vec3  u_bg;
uniform float u_light; // 0.0 = dark theme, 1.0 = light theme
uniform vec3  u_particles[${PARTICLE_COUNT}]; // xy = position (0-1), z = radius

// Simplex-style noise
vec3 mod289(vec3 x) { return x - floor(x * (1.0/289.0)) * 289.0; }
vec2 mod289(vec2 x) { return x - floor(x * (1.0/289.0)) * 289.0; }
vec3 permute(vec3 x) { return mod289(((x*34.0)+1.0)*x); }

float snoise(vec2 v) {
    const vec4 C = vec4(0.211324865405187, 0.366025403784439,
                        -0.577350269189626, 0.024390243902439);
    vec2 i = floor(v + dot(v, C.yy));
    vec2 x0 = v - i + dot(i, C.xx);
    vec2 i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
    vec4 x12 = x0.xyxy + C.xxzz;
    x12.xy -= i1;
    i = mod289(i);
    vec3 p = permute(permute(i.y + vec3(0.0, i1.y, 1.0)) + i.x + vec3(0.0, i1.x, 1.0));
    vec3 m = max(0.5 - vec3(dot(x0,x0), dot(x12.xy,x12.xy), dot(x12.zw,x12.zw)), 0.0);
    m = m*m; m = m*m;
    vec3 x = 2.0 * fract(p * C.www) - 1.0;
    vec3 h = abs(x) - 0.5;
    vec3 ox = floor(x + 0.5);
    vec3 a0 = x - ox;
    m *= 1.79284291400159 - 0.85373472095314 * (a0*a0 + h*h);
    vec3 g;
    g.x = a0.x * x0.x + h.x * x0.y;
    g.yz = a0.yz * x12.xz + h.yz * x12.yw;
    return 130.0 * dot(m, g);
}

void main() {
    vec2 uv = gl_FragCoord.xy / u_res;
    float aspect = u_res.x / u_res.y;
    vec2 p = vec2(uv.x * aspect, uv.y);

    float t = u_time * 0.08;

    // Layered noise at different scales and speeds
    float n1 = snoise(p * 1.5 + vec2(t * 0.3, t * 0.2));
    float n2 = snoise(p * 2.5 + vec2(-t * 0.2, t * 0.15));
    float n3 = snoise(p * 0.8 + vec2(t * 0.1, -t * 0.25));

    float n = n1 * 0.5 + n2 * 0.3 + n3 * 0.2;

    // Map noise to a soft glow
    float glow = smoothstep(0.05, 0.6, n);

    // Two accent-derived colors
    vec3 col1 = u_accent;
    vec3 col2 = u_accent.gbr;

    float colorMix = n2 * 0.5 + 0.5;
    vec3 color = mix(col1, col2, colorMix);

    float boost = 1.0 + u_light * 0.5; // 1.0 dark, 1.5 light

    // Noise background: additive glow on dark, tint toward accent on light
    vec3 noiseDark  = mix(u_bg, u_bg + color * 0.6, glow * 0.14);
    vec3 noiseLight = mix(u_bg, u_accent * 0.82, glow * 0.14 * boost);
    vec3 result = mix(noiseDark, noiseLight, u_light);

    // Floating particles — accumulate glow from each
    float particleGlow = 0.0;
    float particleColor = 0.0;
    for (int i = 0; i < ${PARTICLE_COUNT}; i++) {
        vec2 ppos = u_particles[i].xy;
        float pradius = u_particles[i].z;
        // Particle position in aspect-corrected space
        vec2 pp = vec2(ppos.x * aspect, ppos.y);
        float d = length(p - pp);
        float g = pradius / (d * d * 800.0 + pradius);
        particleGlow += g;
        // Alternate color based on index parity (use float comparison)
        particleColor += g * step(0.5, fract(float(i) * 0.5));
    }

    // Dark: additive white-ish accent glow; Light: blend toward accent color
    vec3 pCol = mix(u_accent, mix(u_accent, vec3(1.0), 0.5), particleColor / max(particleGlow, 0.001));
    float pI = particleGlow * 0.5;
    vec3 pDark  = result + pCol * pI;
    vec3 pLight = mix(result, u_accent * 0.7, pI * boost * 0.35);
    result = mix(pDark, pLight, u_light);

    gl_FragColor = vec4(result, 1.0);
}
`;

let _canvas: HTMLCanvasElement = undefined as any, _gl: WebGLRenderingContext | null = null, _prog: WebGLProgram | null = null;
let _uTime: WebGLUniformLocation | null, _uRes: WebGLUniformLocation | null, _uAccent: WebGLUniformLocation | null, _uBg: WebGLUniformLocation | null, _uLight: WebGLUniformLocation | null, _uParticlesBase: WebGLUniformLocation | null;
let _particleBuf: Float32Array | null = null; // pre-allocated Float32Array for uniform3fv
let _raf: number | null = null;
let _startTime = 0;
let _accent = [76 / 255, 175 / 255, 80 / 255];
let _bgColor = [26 / 255, 26 / 255, 26 / 255];
let _isLight = 0.0;

// Particle state (CPU-side, positions in 0..1 UV space)
interface Particle { x: number; y: number; vx: number; vy: number; r: number; }
const _particles: Particle[] = [];

function _initParticles(): void {
    _particles.length = 0;
    for (let i = 0; i < PARTICLE_COUNT; i++) {
        _particles.push({
            x: Math.random(),
            y: Math.random(),
            vx: (Math.random() - 0.5) * 0.0002,
            vy: 0.0001 + Math.random() * 0.0003,
            r: 0.003 + Math.random() * 0.006,
        });
    }
}

function _updateParticles(): void {
    for (const p of _particles) {
        p.x += p.vx;
        p.y += p.vy;
        // Wrap around
        if (p.y > 1.05) { p.y = -0.05; p.x = Math.random(); }
        if (p.x < -0.05) p.x = 1.05;
        if (p.x > 1.05) p.x = -0.05;
    }
}

function _compile(gl: WebGLRenderingContext, type: number, src: string): WebGLShader | null {
    const s = gl.createShader(type);
    if (!s) return null;
    gl.shaderSource(s, src);
    gl.compileShader(s);
    if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) {
        console.error('Shader compile:', gl.getShaderInfoLog(s));
        return null;
    }
    return s;
}

function _initGL(): boolean {
    _gl = _canvas.getContext('webgl', { alpha: false, antialias: false, depth: false });
    if (!_gl) return false;
    const gl = _gl;

    const vs = _compile(gl, gl.VERTEX_SHADER, VERT_SRC);
    const fs = _compile(gl, gl.FRAGMENT_SHADER, FRAG_SRC);
    if (!vs || !fs) return false;

    _prog = gl.createProgram()!;
    gl.attachShader(_prog!, vs);
    gl.attachShader(_prog!, fs);
    gl.linkProgram(_prog);
    if (!gl.getProgramParameter(_prog, gl.LINK_STATUS)) {
        console.error('Program link:', gl.getProgramInfoLog(_prog));
        return false;
    }
    gl.useProgram(_prog);

    // Full-screen quad
    const buf = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, buf);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1, 1,-1, -1,1, 1,1]), gl.STATIC_DRAW);
    const aPos = gl.getAttribLocation(_prog, 'a_pos');
    gl.enableVertexAttribArray(aPos);
    gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);

    _uTime = gl.getUniformLocation(_prog, 'u_time');
    _uRes = gl.getUniformLocation(_prog, 'u_res');
    _uAccent = gl.getUniformLocation(_prog, 'u_accent');
    _uBg = gl.getUniformLocation(_prog, 'u_bg');
    _uLight = gl.getUniformLocation(_prog, 'u_light');
    _uParticlesBase = gl.getUniformLocation(_prog, 'u_particles[0]');
    _particleBuf = new Float32Array(PARTICLE_COUNT * 3);

    return true;
}

function _resize(): void {
    const w = Math.round(window.innerWidth * 0.5);
    const h = Math.round(window.innerHeight * 0.5);
    _canvas.width = w;
    _canvas.height = h;
    if (_gl) _gl.viewport(0, 0, w, h);
}

function _draw(time: number): void {
    _raf = requestAnimationFrame(_draw);
    const gl = _gl;
    if (!gl) return;

    _updateParticles();

    const t = (time - _startTime) * 0.001;
    gl.uniform1f(_uTime, t);
    gl.uniform2f(_uRes, _canvas.width, _canvas.height);
    gl.uniform3f(_uAccent, _accent[0], _accent[1], _accent[2]);
    gl.uniform3f(_uBg, _bgColor[0], _bgColor[1], _bgColor[2]);
    gl.uniform1f(_uLight, _isLight);

    if (_particleBuf) {
        for (let i = 0; i < PARTICLE_COUNT; i++) {
            const p = _particles[i];
            const off = i * 3;
            _particleBuf[off]     = p.x;
            _particleBuf[off + 1] = p.y;
            _particleBuf[off + 2] = p.r;
        }
        gl.uniform3fv(_uParticlesBase, _particleBuf);
    }

    gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
}

function _start(): void {
    if (_raf) return;
    if (!_gl && !_initGL()) return;
    _resize();
    _initParticles();
    _startTime = performance.now();
    _raf = requestAnimationFrame(_draw);
}

function _stop(): void {
    if (_raf) {
        cancelAnimationFrame(_raf);
        _raf = null;
    }
}

function hexToNorm(hex: string): number[] {
    return [
        parseInt(hex.slice(1, 3), 16) / 255,
        parseInt(hex.slice(3, 5), 16) / 255,
        parseInt(hex.slice(5, 7), 16) / 255,
    ];
}

export function updateBgAnimAccent(hex: string): void {
    _accent = hexToNorm(hex);
}

export function updateBgAnimTheme(isDark: boolean): void {
    _bgColor = isDark ? [26 / 255, 26 / 255, 26 / 255] : [245 / 255, 245 / 255, 245 / 255];
    _isLight = isDark ? 0.0 : 1.0;
}

export function initBgAnim(): void {
    const canvasEl = document.getElementById('bg-anim-canvas') as HTMLCanvasElement | null;
    if (!canvasEl) return;
    _canvas = canvasEl;

    const observer = new MutationObserver(() => {
        const on = document.documentElement.getAttribute('data-bg-anim') === 'on';
        if (on) _start(); else _stop();
    });
    observer.observe(document.documentElement, { attributes: true, attributeFilter: ['data-bg-anim'] });

    window.addEventListener('resize', () => { if (_raf) _resize(); });

    if (document.documentElement.getAttribute('data-bg-anim') === 'on') _start();
}
