import { Shader } from "./Shader";
import { mat4 } from "gl-matrix";

import { Cube } from "./cube";
import vertexSource from "./vertices.glsl";
import fragmentSource from "./fragments.glsl";

class RendererError extends Error {}

class Renderer {
    private canvas: HTMLCanvasElement;

    private analyser: AnalyserNode;
    private analyserData: Float32Array;

    private time: number;
    private buffers: {
        indices?: WebGLBuffer;
        positions?: WebGLBuffer;
        normals?: WebGLBuffer;
    };

    constructor(
        context: AudioContext,
        node: AudioNode,
        canvas: HTMLCanvasElement
    ) {
        const analyser = context.createAnalyser();
        analyser.fftSize = 2048;
        analyser.smoothingTimeConstant = 0.8;
        node.connect(analyser);

        this.canvas = canvas;
        this.analyser = analyser;
        this.analyserData = new Float32Array(analyser.frequencyBinCount);

        this.time = 0;
        this.buffers = { indices: undefined, positions: undefined };
    }

    resize() {
        this.canvas.width = this.canvas.parentElement!.clientWidth;
        this.canvas.height = this.canvas.parentElement!.clientHeight;
    }

    initializeScene() {
        this.resize();

        const gl = this.canvas.getContext("webgl2");
        if (gl === null) {
            throw new RendererError("WebGL (2) is unsupported on this browser");
        }

        const shader = Shader.builder(gl)
            .addShader(vertexSource, gl.VERTEX_SHADER)
            .addShader(fragmentSource, gl.FRAGMENT_SHADER)
            .addAttribute("aVertexPosition")
            .addAttribute("aVertexNormal")
            .addUniforms("uProjectionMatrix")
            .addUniforms("uModelViewMatrix")
            .addUniforms("uNormalMatrix")
            .build();

        this.initGL(gl, shader);
        this.initBuffers(gl);
        this.drawScene(gl, shader);
    }

    initBuffers(gl: WebGLRenderingContext) {
        // Position buffer
        const positionBuffer = gl.createBuffer();

        if (positionBuffer === null) {
            throw new Error("could not initialize position buffer");
        }

        gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
        gl.bufferData(gl.ARRAY_BUFFER, Cube.vertices, gl.STATIC_DRAW);
        this.buffers.positions = positionBuffer;

        // Index buffer
        const indexBuffer = gl.createBuffer();

        if (indexBuffer === null) {
            throw new Error("could not initialize index buffer");
        }

        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
        gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, Cube.indices, gl.STATIC_DRAW);
        this.buffers.indices = indexBuffer;

        // Surface normal buffer
        const normalBuffer = gl.createBuffer();

        if (normalBuffer === null) {
            throw new Error("could not initialize normal buffer");
        }

        gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);
        gl.bufferData(gl.ARRAY_BUFFER, Cube.normals, gl.STATIC_DRAW);
        this.buffers.normals = normalBuffer;
    }

    initGL(gl: WebGLRenderingContext, shader: Shader) {
        gl.useProgram(shader.program);
        gl.clearColor(0.0, 0.0, 0.0, 1.0);
        gl.clearDepth(1.0);
        gl.enable(gl.DEPTH_TEST);
        gl.depthFunc(gl.LEQUAL);
    }

    updateMatrices(gl: WebGLRenderingContext, shader: Shader) {
        const projectionMatrix = mat4.create();
        mat4.perspective(
            projectionMatrix,
            (45 * Math.PI) / 180,
            gl.canvas.clientWidth / gl.canvas.clientHeight,
            0.1,
            100.0
        );
        gl.uniformMatrix4fv(
            shader.getUniform("uProjectionMatrix"),
            false,
            projectionMatrix
        );

        const modelViewMatrix = mat4.create();
        mat4.translate(modelViewMatrix, modelViewMatrix, [0.0, 1.0, -8.0]);
        mat4.rotateY(modelViewMatrix, modelViewMatrix, this.time / 1000);
        mat4.rotateX(
            modelViewMatrix,
            modelViewMatrix,
            (this.time / 1000) * 0.7
        );
        gl.uniformMatrix4fv(
            shader.getUniform("uModelViewMatrix"),
            false,
            modelViewMatrix
        );

        const normalMatrix = mat4.create();
        mat4.invert(normalMatrix, modelViewMatrix);
        mat4.transpose(normalMatrix, normalMatrix);
        gl.uniformMatrix4fv(
            shader.getUniform("uNormalMatrix"),
            false,
            normalMatrix
        );
    }

    drawScene(gl: WebGL2RenderingContext, shader: Shader) {
        if (
            this.buffers.indices === undefined ||
            this.buffers.positions === undefined ||
            this.buffers.normals === undefined
        ) {
            throw new Error("failed to create buffers before rendering");
        }

        this.updateMatrices(gl, shader);

        // Update cube buffers
        gl.bindBuffer(gl.ARRAY_BUFFER, this.buffers.positions);
        gl.vertexAttribPointer(
            shader.getAttribute("aVertexPosition"),
            3,
            gl.FLOAT,
            false,
            0,
            0
        );
        gl.enableVertexAttribArray(shader.getAttribute("aVertexPosition"));

        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.buffers.indices);

        gl.bindBuffer(gl.ARRAY_BUFFER, this.buffers.normals);
        gl.vertexAttribPointer(
            shader.getAttribute("aVertexNormal"),
            3,
            gl.FLOAT,
            false,
            0,
            0
        );
        gl.enableVertexAttribArray(shader.getAttribute("aVertexNormal"));

        gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
        gl.drawElementsInstanced(gl.TRIANGLES, 36, gl.UNSIGNED_SHORT, 0, 2);

        requestAnimationFrame((time) => {
            this.time = time;
            this.drawScene(gl, shader);
        });
    }
}

export { Renderer, RendererError };
