;(() => {
    const elemParent = document.querySelector('div.layout-background')
    const elemSprite = document.querySelector<HTMLLinkElement>('link[rel="texture"]')
    if (!elemParent || !elemSprite) throw 'Invalid Document'

    const FRAME_INTERVAL = 1000
    const FRAME_TIME_IDLE = 12
    const FRAME_TIME_ACTIVE = 60
    const CAM_ACCEL = 2
    const CAM_FRICTION = 0.85
    const PARTICLE_COUNT = 160
    const COLOR_PARTICLE = 0x484848
    const COLOR_FOREGROUND = 0x363636
    const COLOR_BACKGROUND = 0x000000

    let keysHeld = new Set()
    let frameTime = FRAME_INTERVAL / FRAME_TIME_IDLE
    let lastTime = 0
    let lastFrame = 0
    let time = 0
    let cameraMVP = mat4()
    let cameraRotX = 0
    let cameraRotY = 0
    let camVelRotX = 0
    let camVelRotY = 0
    let spriteTexture: WebGLTexture
    let spriteModel: Float32Array
    let particlePos: Float32Array
    let particleColor: Float32Array
    let particleData: {
        x: number
        y: number
        z: number
        vx: number
        vy: number
        vz: number
        life: number
        maxLife: number
    }[] = []
    let planeW: number
    let planeH: number
    let planeSegX: number
    let planeSegY: number
    let planeVerts: Float32Array
    let planeOrig: Float32Array
    let planeIdx

    const canvas = document.createElement('canvas')
    const gl = canvas.getContext('webgl')
    if (!gl) throw 'Failed to allocate WebGL Context'

    // --- Camera Controls ---
    document.addEventListener('keydown', (e) => {
        if (!e.shiftKey && !e.ctrlKey) return
        if (e.target instanceof HTMLTextAreaElement) return
        if (e.target instanceof HTMLInputElement) return
        if ((e as any).isContentEditable) return
        keysHeld.add(e.key)
    })
    document.addEventListener('keyup', (e) => {
        keysHeld.delete(e.key)
    })

    // --- Math Functions ---
    function mat4(): Float32Array {
        return new Float32Array(16)
    }
    function mat4Identity(m: Float32Array): Float32Array {
        m[0] = 1
        m[1] = 0
        m[2] = 0
        m[3] = 0
        m[4] = 0
        m[5] = 1
        m[6] = 0
        m[7] = 0
        m[8] = 0
        m[9] = 0
        m[10] = 1
        m[11] = 0
        m[12] = 0
        m[13] = 0
        m[14] = 0
        m[15] = 1
        return m
    }
    function mat4Multiply(out: Float32Array, a: Float32Array, b: Float32Array): Float32Array {
        for (let i = 0; i < 4; i++)
            for (let j = 0; j < 4; j++) {
                out[j * 4 + i] = 0
                for (let k = 0; k < 4; k++) out[j * 4 + i] += a[k * 4 + i] * b[j * 4 + k]
            }
        return out
    }
    function mat4Perspective(m: Float32Array, fovY: number, aspect: number, near: number, far: number): Float32Array {
        const f = 1.0 / Math.tan(fovY / 2)
        m[0] = f / aspect
        m[1] = 0
        m[2] = 0
        m[3] = 0
        m[4] = 0
        m[5] = f
        m[6] = 0
        m[7] = 0
        m[8] = 0
        m[9] = 0
        m[10] = (far + near) / (near - far)
        m[11] = -1
        m[12] = 0
        m[13] = 0
        m[14] = (2 * far * near) / (near - far)
        m[15] = 0
        return m
    }
    function mat4RotateX(m: Float32Array, angle: number): Float32Array {
        const c = Math.cos(angle)
        const s = Math.sin(angle)
        const t = mat4Identity(mat4())
        t[5] = c
        t[6] = s
        t[9] = -s
        t[10] = c
        return mat4Multiply(mat4(), t, m)
    }
    function mat4RotateY(m: Float32Array, angle: number): Float32Array {
        const c = Math.cos(angle)
        const s = Math.sin(angle)
        const t = mat4Identity(mat4())
        t[0] = c
        t[2] = -s
        t[8] = s
        t[10] = c
        return mat4Multiply(mat4(), t, m)
    }
    function mat4Translate(m: Float32Array, x: number, y: number, z: number): Float32Array {
        const t = mat4Identity(mat4())
        t[12] = x
        t[13] = y
        t[14] = z
        return mat4Multiply(mat4(), t, m)
    }
    function randFloat(lo: number, hi: number): number {
        return lo + Math.random() * (hi - lo)
    }
    function randFloatSpread(range: number): number {
        return randFloat(-range / 2, range / 2)
    }
    function degToRad(d: number): number {
        return (d * Math.PI) / 180
    }
    function intToRGB(i: number): [number, number, number] {
        return [((i >> 16) & 0xff) / 255, ((i >> 8) & 0xff) / 255, ((i >> 0) & 0xff) / 255]
    }

    // --- Prepare Shaders ---
    function createShader(type: number, src: string): WebGLShader {
        if (!gl) throw 'Missing Global GL Context'
        const s = gl.createShader(type)
        if (!s) throw 'Shader Compilation Failed'

        gl.shaderSource(s, src)
        gl.compileShader(s)
        return s
    }

    function createProgram(vert: string, frag: string): WebGLProgram {
        if (!gl) throw 'Missing Global GL Context'
        const p = gl.createProgram()
        gl.attachShader(p, createShader(gl.VERTEX_SHADER, vert))
        gl.attachShader(p, createShader(gl.FRAGMENT_SHADER, frag))
        gl.linkProgram(p)
        return p
    }

    const planeProg = createProgram(
        `attribute vec3 aPosition;
            uniform mat4 uMVP;
            uniform float uTime;
            varying float vDist;
            void main() {
                float dist = sqrt(aPosition.x * aPosition.x + aPosition.z * aPosition.z);
                float wave = sin(dist * 0.5 - uTime);
                float cave = -exp(-dist * 0.1) * 3.5;
                vec3 pos = vec3(aPosition.x, aPosition.y + wave + cave, aPosition.z);
                vDist = length((uMVP * vec4(pos, 1.0)).xyz);
                gl_Position = uMVP * vec4(pos, 1.0);
            }`,
        `precision mediump float;
            uniform vec3 uFogColor;
            uniform float uFogNear;
            uniform float uFogFar;
            varying float vDist;
            void main() {
                float fog = clamp((vDist - uFogNear) / (uFogFar - uFogNear), 0.0, 1.0);
                vec3 color = mix(vec3(${intToRGB(COLOR_FOREGROUND).join(',')}), uFogColor, fog);
                gl_FragColor = vec4(color, 1.0);
            }`,
    )

    const particleProg = createProgram(
        `attribute vec3 aPosition;
            attribute vec4 aColor;
            uniform mat4 uMVP;
            varying vec4 vColor;
            varying float vDist;
            void main() {
                vec4 pos = uMVP * vec4(aPosition, 1.0);
                vDist = length(pos.xyz);
                vColor = aColor;
                gl_PointSize = 3.0;
                gl_Position = pos;
            }`,
        `precision mediump float;
            uniform vec3 uFogColor;
            uniform float uFogNear;
            uniform float uFogFar;
            varying vec4 vColor;
            varying float vDist;
            void main() {
                float fog = clamp((vDist - uFogNear) / (uFogFar - uFogNear), 0.0, 1.0);
                float alpha = vColor.a * (1.0 - fog);
                gl_FragColor = vec4(mix(vColor.rgb, uFogColor, fog), alpha);
            }`,
    )

    const spriteProg = createProgram(
        `attribute vec2 aPosition;
            attribute vec2 aUV;
            uniform mat4 uMVP;
            varying vec2 vUV;
            void main() {
                vUV = aUV;
                gl_Position = uMVP * vec4(aPosition.x , 0, aPosition.y, 1);
            }`,
        `precision mediump float;
            uniform sampler2D uTex;
            varying vec2 vUV;
            void main() {
                gl_FragColor = texture2D(uTex, vUV);
            }`,
    )

    const planeBuf = gl.createBuffer()
    const planeOrigBuf = gl.createBuffer()
    const planeIdxBuf = gl.createBuffer()
    const uTimePlane = gl.getUniformLocation(planeProg, 'uTime')
    const uMVPPlane = gl.getUniformLocation(planeProg, 'uMVP')
    const uFogColorPlane = gl.getUniformLocation(planeProg, 'uFogColor')
    const uFogNearPlane = gl.getUniformLocation(planeProg, 'uFogNear')
    const uFogFarPlane = gl.getUniformLocation(planeProg, 'uFogFar')

    const particlePosBuf = gl.createBuffer()
    const particleColorBuf = gl.createBuffer()
    const uMVPParticle = gl.getUniformLocation(particleProg, 'uMVP')
    const uFogColorParticle = gl.getUniformLocation(particleProg, 'uFogColor')
    const uFogNearParticle = gl.getUniformLocation(particleProg, 'uFogNear')
    const uFogFarParticle = gl.getUniformLocation(particleProg, 'uFogFar')

    const spriteImage = new Image()
    spriteImage.src = elemSprite.href

    const spriteBuf = gl.createBuffer()
    const spriteIdxBuf = gl.createBuffer()
    const uMVPSprite = gl.getUniformLocation(spriteProg, 'uMVP')
    const uTexSprite = gl.getUniformLocation(spriteProg, 'uTex')

    function spawnParticle(i: number) {
        // Put them in the center because its out of frame anyways save some resources
        const x = randFloat(-planeW / 2, planeW / 2)
        const z = randFloat(-planeH / 2, planeH / 2)
        const l = randFloat(3.0, 6.0)

        particleData[i] = {
            x,
            y: 0,
            z,
            vx: randFloatSpread(0.05),
            vy: randFloat(0.02, 0.05),
            vz: randFloatSpread(0.05),
            life: l,
            maxLife: l,
        }
        const p = i * 3
        particlePos[p + 0] = x
        particlePos[p + 1] = 0
        particlePos[p + 2] = z

        const c = i * 4
        const [r, g, b] = intToRGB(COLOR_PARTICLE)
        particleColor[c + 0] = r
        particleColor[c + 1] = g
        particleColor[c + 2] = b
        particleColor[c + 3] = 0
    }

    function startup() {
        if (!gl) throw 'Missing Global GL Context'

        // Render Resolution
        canvas.width = Math.floor(window.innerWidth * 0.3)
        canvas.height = Math.floor(window.innerHeight * 0.3)
        canvas.style.width = window.innerWidth + 'px'
        canvas.style.height = window.innerHeight + 'px'
        canvas.style.imageRendering = 'pixelated'

        // Build Sprite
        spriteImage.onload = () => {
            gl.bindBuffer(gl.ARRAY_BUFFER, spriteBuf)
            gl.bufferData(
                gl.ARRAY_BUFFER,
                new Float32Array([-1, -1, 1, 1, 1, -1, 0, 1, 1, 1, 0, 0, -1, 1, 1, 0]),
                gl.STATIC_DRAW,
            )
            gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, spriteIdxBuf)
            gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array([0, 1, 2, 0, 2, 3]), gl.STATIC_DRAW)

            spriteModel = mat4Identity(mat4())
            spriteModel = mat4RotateY(spriteModel, degToRad(180))
            spriteModel = mat4Translate(spriteModel, 0, 4, 16)

            // Upload Texture
            spriteTexture = gl.createTexture()
            gl.bindTexture(gl.TEXTURE_2D, spriteTexture)
            gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, spriteImage)
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_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.useProgram(spriteProg)
            gl.uniform1i(uTexSprite, 0)
            gl.activeTexture(gl.TEXTURE0)
            gl.bindTexture(gl.TEXTURE_2D, spriteTexture)
            gl.bindBuffer(gl.ARRAY_BUFFER, spriteBuf)
        }

        // Build Plane
        gl.viewport(0, 0, canvas.width, canvas.height)
        gl.clearColor(...intToRGB(COLOR_BACKGROUND), 1)
        gl.enable(gl.BLEND)
        gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)

        const ratio = canvas.width / canvas.height
        const scale = 24
        planeSegX = Math.round(12 * ratio)
        planeSegY = 12
        planeW = scale * ratio
        planeH = scale

        const nx = planeSegX + 1
        const ny = planeSegY + 1
        planeVerts = new Float32Array(nx * ny * 3)
        planeOrig = new Float32Array(nx * ny * 3)

        let vi = 0
        for (let iy = 0; iy < ny; iy++) {
            for (let ix = 0; ix < nx; ix++) {
                const x = (ix / planeSegX - 0.5) * planeW
                const z = (iy / planeSegY - 0.5) * planeH
                planeVerts[vi] = x
                planeVerts[vi + 1] = 0
                planeVerts[vi + 2] = z
                planeOrig[vi] = x
                planeOrig[vi + 1] = 0
                planeOrig[vi + 2] = z
                vi += 3
            }
        }
        const lines = []
        for (let iy = 0; iy < ny; iy++) {
            for (let ix = 0; ix < nx; ix++) {
                const idx = iy * nx + ix
                // wireframe indices two triangles per quad
                if (ix < planeSegX) {
                    lines.push(idx, idx + 1)
                }
                if (iy < planeSegY) {
                    lines.push(idx, idx + nx)
                }
                if (ix < planeSegX && iy < planeSegY) {
                    lines.push(idx, idx + nx + 1)
                }
            }
        }
        planeIdx = new Uint16Array(lines)

        // Upload plane buffers
        gl.bindBuffer(gl.ARRAY_BUFFER, planeBuf)
        gl.bufferData(gl.ARRAY_BUFFER, planeVerts, gl.DYNAMIC_DRAW)
        gl.bindBuffer(gl.ARRAY_BUFFER, planeOrigBuf)
        gl.bufferData(gl.ARRAY_BUFFER, planeOrig, gl.STATIC_DRAW)
        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, planeIdxBuf)
        gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, planeIdx, gl.STATIC_DRAW)

        // Initialize particles if first time
        if (particleData.length === 0) {
            particlePos = new Float32Array(PARTICLE_COUNT * 3)
            particleColor = new Float32Array(PARTICLE_COUNT * 4)
            for (let i = 0; i < PARTICLE_COUNT; i++) {
                spawnParticle(i)
            }
        }

        gl.bindBuffer(gl.ARRAY_BUFFER, particlePosBuf)
        gl.bufferData(gl.ARRAY_BUFFER, particlePos, gl.DYNAMIC_DRAW)
        gl.bindBuffer(gl.ARRAY_BUFFER, particleColorBuf)
        gl.bufferData(gl.ARRAY_BUFFER, particleColor, gl.DYNAMIC_DRAW)

        // Initialize Fog
        gl.useProgram(planeProg)
        gl.uniform3f(uFogColorPlane, ...intToRGB(COLOR_BACKGROUND))
        gl.uniform1f(uFogNearPlane, 2)
        gl.uniform1f(uFogFarPlane, 22)

        gl.useProgram(particleProg)
        gl.uniformMatrix4fv(uMVPParticle, false, cameraMVP)
        gl.uniform3f(uFogColorParticle, ...intToRGB(COLOR_BACKGROUND))
        gl.uniform1f(uFogNearParticle, 6)
        gl.uniform1f(uFogFarParticle, 24)

        // Force dirty camera reset
        camVelRotY = 0.005
    }

    function animate(now: number) {
        requestAnimationFrame(animate)

        // Sleep (warning this sucks)
        if (now - lastFrame < frameTime) return
        const delta = (now - lastTime) * 0.00008 || 0
        lastFrame = now
        lastTime = now
        time += delta

        if (!gl) throw 'Missing Global GL Context'
        gl.clear(gl.COLOR_BUFFER_BIT)

        // Camera Movement
        if (keysHeld.has('ArrowLeft')) camVelRotY -= CAM_ACCEL
        if (keysHeld.has('ArrowRight')) camVelRotY += CAM_ACCEL
        if (keysHeld.has('ArrowUp')) camVelRotX -= CAM_ACCEL
        if (keysHeld.has('ArrowDown')) camVelRotX += CAM_ACCEL + 8

        camVelRotX *= CAM_FRICTION
        camVelRotY *= CAM_FRICTION
        const dirty = Math.abs(camVelRotX) + Math.abs(camVelRotY) > 0.001

        if (dirty) {
            cameraRotX += camVelRotX * delta * 1000
            cameraRotY += camVelRotY * delta * 1000

            // Update Camera
            const ratio = canvas.width / canvas.height
            const proj = mat4Perspective(mat4(), degToRad(70), ratio, 0.1, 100)

            let view = mat4Identity(mat4())
            view = mat4Translate(view, 0, -7.5, -15)
            view = mat4RotateX(view, degToRad(camVelRotX + 33.75))
            view = mat4RotateY(view, degToRad(camVelRotY))

            cameraMVP = mat4Multiply(mat4(), proj, view)
            gl.useProgram(planeProg)
            gl.uniformMatrix4fv(uMVPPlane, false, cameraMVP)
            gl.useProgram(particleProg)
            gl.uniformMatrix4fv(uMVPParticle, false, cameraMVP)

            frameTime = FRAME_INTERVAL / FRAME_TIME_ACTIVE
        } else {
            frameTime = FRAME_INTERVAL / FRAME_TIME_IDLE
        }

        // Update Sprite
        if (spriteTexture) {
            const spriteMVP = mat4Multiply(mat4(), cameraMVP, spriteModel)
            gl.useProgram(spriteProg)
            gl.uniformMatrix4fv(uMVPSprite, false, spriteMVP)
            gl.uniform1i(uTexSprite, 0)
            gl.activeTexture(gl.TEXTURE0)
            gl.bindTexture(gl.TEXTURE_2D, spriteTexture)

            gl.bindBuffer(gl.ARRAY_BUFFER, spriteBuf)
            const aPos = gl.getAttribLocation(spriteProg, 'aPosition')
            const aUV = gl.getAttribLocation(spriteProg, 'aUV')
            gl.enableVertexAttribArray(aPos)
            gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 16, 0)
            gl.enableVertexAttribArray(aUV)
            gl.vertexAttribPointer(aUV, 2, gl.FLOAT, false, 16, 8)

            gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, spriteIdxBuf)
            gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0)
        }

        {
            // Update Particles
            for (let i = 0; i < PARTICLE_COUNT; i++) {
                const p = particleData[i]

                // fade out
                p.life -= delta
                if (p.life <= 0) {
                    spawnParticle(i)
                    continue
                }
                particleColor[i * 4 + 3] = Math.min((p.life / p.maxLife) * 1.2, 1)

                // drift away
                p.x += p.vx * delta * 40
                p.y += p.vy * delta * 20
                p.z += p.vz * delta * 40
                const pi = i * 3
                particlePos[pi + 0] = p.x
                particlePos[pi + 1] = p.y
                particlePos[pi + 2] = p.z
            }
            gl.bindBuffer(gl.ARRAY_BUFFER, particlePosBuf)
            gl.bufferSubData(gl.ARRAY_BUFFER, 0, particlePos)
            gl.bindBuffer(gl.ARRAY_BUFFER, particleColorBuf)
            gl.bufferSubData(gl.ARRAY_BUFFER, 0, particleColor)

            // Draw Particles
            gl.useProgram(particleProg)
            const aPos = gl.getAttribLocation(particleProg, 'aPosition')
            const aCol = gl.getAttribLocation(particleProg, 'aColor')

            gl.bindBuffer(gl.ARRAY_BUFFER, particlePosBuf)
            gl.enableVertexAttribArray(aPos)
            gl.vertexAttribPointer(aPos, 3, gl.FLOAT, false, 0, 0)

            gl.bindBuffer(gl.ARRAY_BUFFER, particleColorBuf)
            gl.enableVertexAttribArray(aCol)
            gl.vertexAttribPointer(aCol, 4, gl.FLOAT, false, 0, 0)

            gl.drawArrays(gl.POINTS, 0, PARTICLE_COUNT)
        }

        {
            // Update Plane
            for (let i = 0; i < planeVerts.length; i += 3) {
                const x = planeOrig[i]
                const z = planeOrig[i + 2]
                const dist = Math.sqrt(x * x + z * z)
                planeVerts[i] = x
                planeVerts[i + 1] = Math.sin(dist * 0.5 - time) * 0.5 + -Math.exp(-dist * 0.1) * 3.5
                planeVerts[i + 2] = z
            }
            gl.bindBuffer(gl.ARRAY_BUFFER, planeBuf)
            gl.bufferSubData(gl.ARRAY_BUFFER, 0, planeVerts)

            // Draw Plane
            gl.useProgram(planeProg)
            gl.uniform1f(uTimePlane, time)

            const aPos = gl.getAttribLocation(planeProg, 'aPosition')
            gl.bindBuffer(gl.ARRAY_BUFFER, planeBuf)
            gl.enableVertexAttribArray(aPos)
            gl.vertexAttribPointer(aPos, 3, gl.FLOAT, false, 0, 0)

            gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, planeIdxBuf)
            gl.drawElements(gl.LINES, planeIdx.length, gl.UNSIGNED_SHORT, 0)
        }
    }

    // --- Prepare Canvas ---
    window.addEventListener('resize', startup)
    startup()
    animate(0)
    elemParent.append(canvas)
})()
