/**
 * Shader-based background effects engine.
 *
 * Renders various full-screen WebGL shader effects on a shared canvas.
 * Each effect is a fragment shader that receives common uniforms
 * (time, resolution, accent color, theme brightness).
 *
 * The engine reuses the same <canvas id="bg-effect-canvas"> element
 * and rebuilds the GL program when switching shaders.
 */

// ─── Shader library ──────────────────────────────────────────

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

/** Aurora — flowing northern-lights bands */
const FRAG_AURORA = `
precision mediump float;
uniform float u_time;
uniform vec2  u_res;
uniform vec3  u_accent;
uniform vec3  u_bg;
uniform float u_light;

vec3 mod289(vec3 x){return x-floor(x*(1./289.))*289.;}
vec2 mod289(vec2 x){return x-floor(x*(1./289.))*289.;}
vec3 permute(vec3 x){return mod289(((x*34.)+1.)*x);}

float snoise(vec2 v){
    const vec4 C=vec4(.211324865,.366025404,-.577350269,.024390244);
    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):vec2(0,1);
    vec4 x12=x0.xyxy+C.xxzz; x12.xy-=i1;
    i=mod289(i);
    vec3 p=permute(permute(i.y+vec3(0,i1.y,1))+i.x+vec3(0,i1.x,1));
    vec3 m=max(.5-vec3(dot(x0,x0),dot(x12.xy,x12.xy),dot(x12.zw,x12.zw)),0.);
    m=m*m; m=m*m;
    vec3 x=2.*fract(p*C.www)-1.;
    vec3 h=abs(x)-.5;
    vec3 ox=floor(x+.5);
    vec3 a0=x-ox;
    m*=1.79284291-.85373472*(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.*dot(m,g);
}

void main(){
    vec2 uv=gl_FragCoord.xy/u_res;
    float t=u_time*.06;

    // Vertical bands that sway horizontally
    float wave1=sin(uv.x*3.+t*1.2+snoise(vec2(uv.x*2.,t*.3))*.8)*.5+.5;
    float wave2=sin(uv.x*5.-t*.9+snoise(vec2(uv.x*1.5,t*.2+2.))*.6)*.5+.5;
    float wave3=sin(uv.x*2.+t*.7+snoise(vec2(uv.x*3.,t*.25+5.))*.7)*.5+.5;

    // Fade toward the top of the screen
    float yFade=smoothstep(0.,.7,uv.y)*smoothstep(1.,.4,uv.y);

    // Combine bands
    float band1=smoothstep(.35,.65,wave1)*yFade;
    float band2=smoothstep(.4,.7,wave2)*yFade*.7;
    float band3=smoothstep(.3,.6,wave3)*yFade*.5;

    vec3 col1=u_accent;
    vec3 col2=u_accent.gbr;
    vec3 col3=mix(u_accent,vec3(1),0.3);

    float intensity=band1*0.12+band2*0.08+band3*0.06;
    vec3 color=band1*col1+band2*col2+band3*col3;
    color=color/(band1+band2+band3+.001);

    float boost=1.+u_light*.4;
    vec3 result=mix(u_bg,u_bg+color*.6,intensity*boost);

    gl_FragColor=vec4(result,1.);
}
`;

/** Plasma — classic color-cycling plasma */
const FRAG_PLASMA = `
precision mediump float;
uniform float u_time;
uniform vec2  u_res;
uniform vec3  u_accent;
uniform vec3  u_bg;
uniform float u_light;

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*.15;

    float v1=sin(p.x*4.+t);
    float v2=sin(p.y*4.+t*.7);
    float v3=sin((p.x+p.y)*3.+t*1.1);
    float v4=sin(length(p-vec2(aspect*.5,.5))*6.-t*.8);

    float v=(v1+v2+v3+v4)*.25; // -1..1

    vec3 col1=u_accent;
    vec3 col2=u_accent.brg;
    vec3 col3=u_accent.gbr;

    float m1=sin(v*3.14159)*.5+.5;
    float m2=sin(v*3.14159+2.094)*.5+.5;
    vec3 color=col1*(1.-m1)+col2*m1;
    color=color*(1.-m2*.4)+col3*m2*.4;

    float boost=1.+u_light*.3;
    float intensity=smoothstep(-.2,.8,v)*.22*boost;
    vec3 result=mix(u_bg,u_bg+color*.7,intensity);

    gl_FragColor=vec4(result,1.);
}
`;

/** Digital Rain — matrix-style falling columns */
const FRAG_RAIN = `
precision mediump float;
uniform float u_time;
uniform vec2  u_res;
uniform vec3  u_accent;
uniform vec3  u_bg;
uniform float u_light;

float hash(vec2 p){
    return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453);
}

void main(){
    vec2 uv=gl_FragCoord.xy/u_res;
    float aspect=u_res.x/u_res.y;
    float t=u_time*.12;

    // Many thin columns
    float cols=80.*aspect;
    float col_x=floor(uv.x*cols);

    // Multiple drops per column at staggered phases
    float totalGlow=0.;
    for(int d=0;d<3;d++){
        float seed=float(d)*37.;
        float speed=.3+hash(vec2(col_x,seed))*1.0;
        float phase=hash(vec2(col_x,seed+1.))*6.28;
        float drop_y=1.-fract(t*speed+phase);

        // Trail: fades downward from the drop head
        float dist=drop_y-uv.y;
        if(dist<0.) dist+=1.;
        float trailLen=.15+hash(vec2(col_x,seed+2.))*.25;
        float trail=smoothstep(trailLen,0.,dist);
        trail*=trail;

        // Cell flicker within the trail
        float rows=80.;
        float row_y=floor(uv.y*rows);
        float flicker=step(.35,hash(vec2(col_x+seed,row_y+floor(t*4.))));
        totalGlow+=trail*flicker*.12;

        // Bright head
        float headDist=abs(uv.y-drop_y);
        if(headDist>.5) headDist=1.-headDist;
        totalGlow+=smoothstep(.006,0.,headDist)*.15;
    }

    totalGlow=min(totalGlow,.35);
    float boost=1.+u_light*.25;
    vec3 result=mix(u_bg,u_bg+u_accent,totalGlow*boost);

    gl_FragColor=vec4(result,1.);
}
`;

/** Starfield — parallax depth star field */
const FRAG_STARS = `
precision mediump float;
uniform float u_time;
uniform vec2  u_res;
uniform vec3  u_accent;
uniform vec3  u_bg;
uniform float u_light;

float hash(vec2 p){
    return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453);
}

void main(){
    vec2 uv=(gl_FragCoord.xy-.5*u_res)/u_res.y;
    float t=u_time*.02;

    vec3 result=u_bg;

    // 4 layers of stars at different depths
    for(int layer=0;layer<4;layer++){
        float fl=float(layer);
        float depth=1.+fl*.6;
        float speed=.05*depth;

        vec2 st=uv*20.*depth;
        st.y+=t*speed*15.;

        vec2 cell=floor(st);
        vec2 f=fract(st);

        // Check this cell and its 8 neighbours for stars
        for(int dy=-1;dy<=1;dy++){
            for(int dx=-1;dx<=1;dx++){
                vec2 neighbor=vec2(float(dx),float(dy));
                vec2 c=cell+neighbor;
                float h=hash(c+fl*137.);

                // Only ~40% of cells have a star
                if(h>.6) continue;

                vec2 starPos=vec2(hash(c*1.17+fl*53.),hash(c*2.31+fl*79.));
                float d=length(f-neighbor-starPos);

                float starSize=.04+h*.06;
                float brightness=smoothstep(starSize,starSize*.1,d);

                // Soft glow halo
                float glow=smoothstep(starSize*4.,0.,d)*.3;

                // Twinkle
                float twinkle=sin(t*8.+h*43.)*.25+.75;
                float total=(brightness+glow)*twinkle;

                // Color: white or accent-tinted
                vec3 starColor=mix(vec3(1.),u_accent,step(.3,h));
                float boost=1.+u_light*.2;
                result+=starColor*total*.12*boost/depth;
            }
        }
    }

    gl_FragColor=vec4(result,1.);
}
`;

/** Warp — smooth tunnel/vortex speed effect */
const FRAG_WARP = `
precision mediump float;
uniform float u_time;
uniform vec2  u_res;
uniform vec3  u_accent;
uniform vec3  u_bg;
uniform float u_light;

void main(){
    vec2 uv=(gl_FragCoord.xy-.5*u_res)/u_res.y;
    float t=u_time*.1;

    float r=length(uv);
    float a=atan(uv.y,uv.x);

    // Logarithmic tunnel mapping for smooth depth
    float tunnel=log(r+.001)*-2.;

    // Twist increases with depth (further from center = more twist)
    float twist=a/3.14159+tunnel*.15+t;

    // Overlapping band patterns at different scales
    float band1=sin(tunnel*4.-t*2.)*.5+.5;
    float band2=sin(tunnel*7.+t*1.5)*.5+.5;
    float rays=sin(twist*6.)*.5+.5;

    float pattern=mix(band1,band2,.5)*rays;

    // Smooth radial fade — no harsh edges
    float fade=smoothstep(0.,.03,r)*smoothstep(.9,.15,r);

    // Gentle center glow
    float centerGlow=smoothstep(.3,0.,r)*.08;

    vec3 col1=u_accent;
    vec3 col2=u_accent.gbr;
    vec3 color=mix(col1,col2,rays);

    float intensity=pattern*fade*.15+centerGlow;
    float boost=1.+u_light*.3;
    vec3 result=mix(u_bg,u_bg+color*.6,intensity*boost);

    gl_FragColor=vec4(result,1.);
}
`;

// ─── Shader registry ─────────────────────────────────────────

export type ShaderEffectId = 'aurora' | 'plasma' | 'rain' | 'stars' | 'warp';

const SHADER_MAP: Record<ShaderEffectId, string> = {
    aurora: FRAG_AURORA,
    plasma: FRAG_PLASMA,
    rain:   FRAG_RAIN,
    stars:  FRAG_STARS,
    warp:   FRAG_WARP,
};

// ─── Engine state ────────────────────────────────────────────

let _canvas: HTMLCanvasElement | null = null;
let _gl: WebGLRenderingContext | null = null;
let _prog: WebGLProgram | null = null;
let _raf: number | null = null;
let _startTime = 0;
let _activeShader: ShaderEffectId | null = null;

// Uniforms
let _uTime: WebGLUniformLocation | null = null;
let _uRes: WebGLUniformLocation | null = null;
let _uAccent: WebGLUniformLocation | null = null;
let _uBg: WebGLUniformLocation | null = null;
let _uLight: WebGLUniformLocation | null = null;

let _accent = [76 / 255, 175 / 255, 80 / 255];
let _bgColor = [26 / 255, 26 / 255, 26 / 255];
let _isLight = 0.0;

// ─── GL helpers ──────────────────────────────────────────────

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('bg-shaders compile:', gl.getShaderInfoLog(s));
        return null;
    }
    return s;
}

function _buildProgram(gl: WebGLRenderingContext, fragSrc: string): WebGLProgram | null {
    const vs = _compile(gl, gl.VERTEX_SHADER, VERT);
    const fs = _compile(gl, gl.FRAGMENT_SHADER, fragSrc);
    if (!vs || !fs) return null;

    const prog = gl.createProgram()!;
    gl.attachShader(prog, vs);
    gl.attachShader(prog, fs);
    gl.linkProgram(prog);
    if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
        console.error('bg-shaders link:', gl.getProgramInfoLog(prog));
        return null;
    }

    // Clean up individual shaders
    gl.deleteShader(vs);
    gl.deleteShader(fs);

    return prog;
}

function _ensureCanvas(): boolean {
    if (_canvas) return true;
    _canvas = document.getElementById('bg-effect-canvas') as HTMLCanvasElement | null;
    return !!_canvas;
}

function _ensureGL(): boolean {
    if (_gl) return true;
    if (!_ensureCanvas()) return false;
    _gl = _canvas!.getContext('webgl', { alpha: false, antialias: false, depth: false });
    if (!_gl) return false;

    // Full-screen quad (shared across all shaders)
    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);
    return true;
}

function _switchProgram(shaderId: ShaderEffectId): boolean {
    if (!_ensureGL()) return false;
    const gl = _gl!;

    // Delete old program
    if (_prog) {
        gl.deleteProgram(_prog);
        _prog = null;
    }

    const fragSrc = SHADER_MAP[shaderId];
    if (!fragSrc) return false;

    _prog = _buildProgram(gl, fragSrc);
    if (!_prog) return false;

    gl.useProgram(_prog);

    // Re-bind vertex attrib
    const aPos = gl.getAttribLocation(_prog, 'a_pos');
    gl.enableVertexAttribArray(aPos);
    gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);

    // Cache uniform locations
    _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');

    return true;
}

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

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

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

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

// ─── Public API ──────────────────────────────────────────────

/** Start a shader effect. Stops any currently running shader. */
export function startShaderEffect(id: ShaderEffectId): void {
    stopShaderEffect();

    if (!_switchProgram(id)) return;

    _activeShader = id;
    _resize();
    _startTime = performance.now();
    _raf = requestAnimationFrame(_draw);

    // Show canvas
    if (_canvas) _canvas.style.display = 'block';
}

/** Stop the currently running shader effect. */
export function stopShaderEffect(): void {
    if (_raf) {
        cancelAnimationFrame(_raf);
        _raf = null;
    }
    _activeShader = null;
    if (_canvas) _canvas.style.display = 'none';
}

/** Update accent color (called when user changes accent). */
export function updateShaderAccent(hex: string): void {
    _accent = [
        parseInt(hex.slice(1, 3), 16) / 255,
        parseInt(hex.slice(3, 5), 16) / 255,
        parseInt(hex.slice(5, 7), 16) / 255,
    ];
}

/** Update theme brightness (called on theme toggle). */
export function updateShaderTheme(isDark: boolean): void {
    _bgColor = isDark ? [26 / 255, 26 / 255, 26 / 255] : [245 / 255, 245 / 255, 245 / 255];
    _isLight = isDark ? 0.0 : 1.0;
}

/** Get the currently active shader ID (or null). */
export function getActiveShader(): ShaderEffectId | null {
    return _activeShader;
}

/** Check if a given ID is a valid shader effect. */
export function isShaderEffect(id: string): id is ShaderEffectId {
    return id in SHADER_MAP;
}

/** Initialize resize listener. */
export function initBgShaders(): void {
    window.addEventListener('resize', () => { if (_raf) _resize(); });
}