"use strict";

const Engine = window.Engine || {};

Engine.Renderer = class {
    _canvas;
    _gl;
    
    _coordWidth = 240;           // Size in pixel coordinates = hardwired for now
    _coordHeight = 135;
    _aspectRatio = 16 / 9;  // Hardwired for now 


    _vertexBuffer;
    _indexBuffer;
    _uvBuffer;
    _frameBuffer; 

    constructor(canvas) {
        this._canvas = canvas;
        this._gl = canvas.getContext("experimental-webgl");

        this._initializeBuffers();
    }

    render(renderQueue, time) {
        const gl = this._gl;

        // Setup OpenGL
        gl.clearColor(0.5, 0.1, 0.5, 1.0); // If you see this purple you need to set a shader
        gl.disable(gl.DEPTH_TEST);  // No depth testing for now
        gl.clear(gl.COLOR_BUFFER_BIT);
        gl.viewport(0, 0, canvas.width, canvas.height);

        if (renderQueue.postShader)
        {
            // We are going to render to our framebuffer instead.
            if (!this._frameBuffer)
                this._frameBuffer = this._createFramebuffer(canvas.width, canvas.height);
            gl.bindFramebuffer(gl.FRAMEBUFFER, this._frameBuffer.buffer);
        }

        // BackgroundShader
        this._renderShader(renderQueue.backgroundShader, time, renderQueue.backgroundShaderParameters);

        // Sprites
        this._renderSprites(renderQueue, time)   

        if (renderQueue.postShader)
        {
            gl.bindFramebuffer(gl.FRAMEBUFFER, null);
            this._renderShader(renderQueue.postShader, time, renderQueue.postShaderParameters);

        }
    }


    // Returns the WebGLRenderingContext
    get GL() {
        return this._gl;
    }

    _renderShader(shader, time, parameters) {
        if (!shader)
            return;

        const gl = this._gl;

        // Setup Shader
        gl.useProgram(shader);
        gl.bindBuffer(gl.ARRAY_BUFFER, this._vertexBuffer);
        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this._indexBuffer);

        const coordinates = gl.getAttribLocation(shader, "vertexPosition");
        gl.vertexAttribPointer(coordinates, 3, gl.FLOAT, false, 0, 0);
        gl.enableVertexAttribArray(coordinates);

        const timeUniform = gl.getUniformLocation(shader, "iTime");
        gl.uniform1f(timeUniform, time);

        const resolutionUniform = gl.getUniformLocation(shader, "iResolution");
        gl.uniform2f(resolutionUniform, canvas.width, canvas.height);

        const mouseUniform = gl.getUniformLocation(shader, "iMouse");
        gl.uniform2f(mouseUniform, 0, 0);
        
        if (this._frameBuffer) {
            gl.activeTexture(gl.TEXTURE0);
            gl.bindTexture(gl.TEXTURE_2D, this._frameBuffer.texture);
            const framebufferUniform = gl.getUniformLocation(shader, "iChannel0");
            gl.uniform1i(framebufferUniform, 0)
        }

        if (parameters) {
            for (const parameterName in parameters) {
                const parameterUniform = gl.getUniformLocation(shader, parameterName);
                // TODO, PJ: Support multiple parameter types
                gl.uniform1f(parameterUniform, parameters[parameterName])
            }
        }

        // Draw triangles
        gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);
    }

    _renderSprites(renderQueue, time)
    {
        const gl = this._gl;

        for (const spriteItem of renderQueue.sprites) {
            this._renderSprite(spriteItem, renderQueue, time);
        }
    }

    _renderSprite(spriteItem, renderQueue, time) {
        const gl = this._gl;
        const sprite = spriteItem.sprite;

        const mat4 = glMatrix.mat4;
        const quat = glMatrix.quat;
        const vec3 = glMatrix.vec3;

        gl.enable(gl.BLEND)
     
        // Setup Shader
        const spriteShader = spriteItem.shader || renderQueue.spriteShader;
        gl.useProgram(spriteShader);
        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this._indexBuffer);

        const coordinates = gl.getAttribLocation(spriteShader, "vertexPosition");
        gl.bindBuffer(gl.ARRAY_BUFFER, this._vertexBuffer);
        gl.vertexAttribPointer(coordinates, 3, gl.FLOAT, false, 0, 0);
        gl.enableVertexAttribArray(coordinates);

        const textureCoordinates = gl.getAttribLocation(spriteShader, "textureCoord");
        gl.bindBuffer(gl.ARRAY_BUFFER, this._uvBuffer);
        gl.vertexAttribPointer(textureCoordinates, 2, gl.FLOAT, false, 0, 0);
        gl.enableVertexAttribArray(textureCoordinates);           

        // Blendmode (refa: pre-sorting in the renderqueue might be faster)
        if (spriteItem.blendMode === BlendMode.add)
            gl.blendFunc(gl.SRC_ALPHA, gl.ONE);
        else if (spriteItem.blendMode === BlendMode.subtract)
            gl.blendFunc(gl.SRC_ALPHA, gl.ONE);
        else 
            gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ZERO, gl.ONE);

        // gl.blendEquation( gl.FUNC_ADD ) 

        // Transform
        const modelViewMatrix = mat4.create();
        mat4.identity(modelViewMatrix); 
        // Position
        mat4.translate(modelViewMatrix, modelViewMatrix, vec3.fromValues( 
            (2 * spriteItem.position.x - this._coordWidth) / this._coordWidth,
            (this._coordHeight - 2 * spriteItem.position.y) / this._coordHeight,
            spriteItem.position.z
        ));
        // Scale
        const aspectRatio = sprite ? sprite.aspectRatio : 16/9;
        mat4.scale(modelViewMatrix, modelViewMatrix, vec3.fromValues( 
            spriteItem.width / this._coordWidth, 
            spriteItem.width / this._coordWidth * this._aspectRatio / aspectRatio, 
            1.0
        ));
        // Rotation
        let rotation = quat.create();
        if (spriteItem.rotation)
           quat.fromEuler(rotation, 0, 0, spriteItem.rotation);
        const rotationMatrix = mat4.create(); 
        mat4.fromQuat(rotationMatrix, rotation);
        mat4.multiply(modelViewMatrix, modelViewMatrix, rotationMatrix); 

        const modelViewMatrixUniform = gl.getUniformLocation(spriteShader, "modelViewMatrix");
        gl.uniformMatrix4fv(modelViewMatrixUniform, false, modelViewMatrix);        

        if (spriteItem.shader) {
            this._renderShader(spriteItem.shader, time, spriteItem.shaderParameters);
            return;
        }

        // Texture
        gl.activeTexture(gl.TEXTURE0);
        gl.bindTexture(gl.TEXTURE_2D, sprite.texture);
        const samplerUniform = gl.getUniformLocation(spriteShader, "sampler");
        gl.uniform1i(samplerUniform, 0);

        // Color
        const colorUniform = gl.getUniformLocation(spriteShader, "color");
        const color = spriteItem.color;
        gl.uniform4f(colorUniform, color.r, color.g, color.b, color.a );

        // Draw triangles
        gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);
    }

    _initializeBuffers() {
        const gl = this._gl;

        const vertices = [
            -1.0, 1.0, 0.0,
            -1.0, -1.0, 0.0,
             1.0, -1.0, 0.0,
             1.0,  1.0, 0.0
        ];
        const uvCoordinates = [
            0.0, 0.0,
            0.0, 1.0,
            1.0, 1.0,
            1.0, 0.0
        ];
        const indices = [0, 1, 2, 0, 2, 3];

        // Vertex buffer
        const vertexBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
        gl.bindBuffer(gl.ARRAY_BUFFER, null);
        this._vertexBuffer = vertexBuffer;

        // Index buffer
        const indexBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
        gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);
        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
        this._indexBuffer = indexBuffer;

        // Texture Coordinates
        const uvBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, uvBuffer);
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(uvCoordinates), gl.STATIC_DRAW);
        gl.bindBuffer(gl.ARRAY_BUFFER, null);        
        this._uvBuffer = uvBuffer;

    }

    _createFramebuffer(width, height) {
        const gl = this._gl;
        const framebuffer = new Framebuffer();

        framebuffer.texture = gl.createTexture();
        gl.bindTexture(gl.TEXTURE_2D, framebuffer.texture);
        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.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);

        framebuffer.buffer = gl.createFramebuffer();
        gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer.buffer);
        gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, framebuffer.texture, 0);

        return framebuffer;
    }
}
