/**
 * Clase auxiliar para simplificar creación y carda de WebGL, 
 * así como para la simplificación del uso y carga de atributos y 
 * uniforms en los VAOs.
 * @author Melissa Méndez Servín.
 */
class WebGLUtils{
    constructor(){
        this.programs = {};
    }
    /**
     * Inicializa el contexto de renderizado WebGL2 para el canvas.
     * @param {*} canvas el canvas sobre el que se aplicará el contexto.
     */  
    init(canvas, att={}){
        var gl = null;
        gl = canvas.getContext("webgl2", att);
        if(!gl){
            let container = document.getElementById("container");
            var err = "Tu navegador no soporta WebGL2";
            container.innerHTML = "<div class=\"err\">" + err + "</div>";
            throw err;
        }
            
        return gl;
    }
    /**
     * Compila y crea un WebGLProgram dados los shaders.
     * @param {WebGLRenderingContext} gl el WebGLRenderingContext.
     * @param {String} vshSource el vertex shader.
     * @param {String} fshSource el fragment shader.
     * @return {WebGLProgram} el programa.
     */
    createProgram(gl, vshSource, fshSource, name = "program"){
        var vsh = this.createShader(gl, vshSource, 0);
        var fsh = this.createShader(gl, fshSource, 1);
        var program = gl.createProgram();
        gl.attachShader(program,vsh);
        gl.attachShader(program,fsh);
        gl.linkProgram(program);
        if(!gl.getProgramParameter(program, gl.LINK_STATUS)){
            console.log("Link error in program " + gl.getProgramInfoLog(program));
            gl.deleteProgram(program);
            return null;
        }
        WebGL.programs[name] = program;
        return program;
    }
    /**
     * Crea y compila un shader.
     * @param {WebGLRenderingContext} gl el WebGLRenderingContext.
     * @param {String} shaderSource el shader.
     * @param {Number} type el tipo del shader, 0 para indicar que se trata de un
     * shader de vértices, 1 para shader de fragmentos.
     */
    createShader(gl,shaderSource,type){
        var sh = (type) ?  gl.createShader(gl.FRAGMENT_SHADER) : gl.createShader(gl.VERTEX_SHADER);
        gl.shaderSource(sh, shaderSource);
        gl.compileShader(sh);
       
        if(!gl.getShaderParameter(sh, gl.COMPILE_STATUS)){
            var err_type = (type)? "fragment shader " : "vertex shader ";
            console.log("Error compiling " + err_type + gl.getShaderInfoLog(sh));
            gl.deleteShader(sh);
            return null;
        }
        return sh;
    }
    /**
     * Crea el VAO, agrega los atributos y respectivos búfers definidos 
     * por un objeto dado, donde se toma el nombre de las llaves como 
     * nombre por defecto de la variable en el shader, agregando el prefijo
     * 'a_'.
     * Ejemplo:
     * var attributes = {  position : { numComponents: 2, 
     *                                  data: [ -100,  100,
    *                                           -100, -100, 
    *                                            100, -100]},
     *                        color : { numComponents: 4,  
     *                                  type: gl.UNSIGNED_BYTE,
     *                                  dataType: Uint8Array, 
     *                                  normalize: true, 
     *                                  typData: [ 22, 177, 176, 255,
     *                                             22, 164, 133, 255,
     *                                             129, 90, 134, 255 ])}
     *                  };
     * Los objetos deben definir al menos el número de componentes y el arreglo 
     * con el que se rellenará el búfer, por atributo. Esto es dependiendo 
     * de la configuración del atributo.
     *
     * @param {WebGLRenderingContext} gl el WebGLRenderingContext.
     * @param {WebGLProgram} program el programa al que se le agregaran los atributos.
     * @param {Object} attributes los atributos y sus configuraciones.
     * @return {} el vao o en caso de definir un arreglo de índices 
     *            un objeto con el vao y el búfer de índices.
     */
    setVAOAndAttributes(gl, program, attributes, indices=[], prefix=false){     
        var vao = gl.createVertexArray();
        gl.bindVertexArray(vao);   
        
        const numAttribs = gl.getProgramParameter(program, gl.ACTIVE_ATTRIBUTES);
        for (let i = 0; i < numAttribs; ++i) {
            const info = gl.getActiveAttrib(program, i); 
            
            const name = (prefix) ? info.name : info.name.substr(2,info.name.length); 
            const att = attributes[name];
            
            const location = gl.getAttribLocation(program, info.name);
            var buffer = gl.createBuffer();    
            
            const numComponents = att.numComponents;
            const DataType = att.dataType || Float32Array;
            const data = att.data;
            const type = att.type || gl.FLOAT;
            const normalize = att.normalize || false;
            const offset = att.offset || 0;
            const stride = att.stride || 0;

            gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
            gl.bufferData(gl.ARRAY_BUFFER, new DataType(data), gl.STATIC_DRAW);
            gl.enableVertexAttribArray(location);
            gl.vertexAttribPointer(location, numComponents, type, normalize, stride, offset);
        }

        if(indices.length > 0){
            let indexBuffer = gl.createBuffer();
            gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
            gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);
            
            //unbind vao
            gl.bindVertexArray(null); 
            return { vao: vao, indexBuffer: indexBuffer};
        }

        //unbind vao
        gl.bindVertexArray(null); 
        return vao;
    }
    /**
     * Agrega los uniforms.
     * Configura el tipo de función asociada a las variables uniformes activas
     * del programa, y finalmente devuelve la función que agrega las variables
     * activas de acuerdo con el tipo de función definida primero.
     * Ésta última, asigna los valores de las variables dado un objeto, permitiendo
     * modificar los valores de las variables en el redibujado.
     * Ejemplo:
     * 
     * (Antes del dibujado, uniforms que no cambian). 
     * let uniforms = { unit: unit, 
     *                  axis: axis};
     * let setUniforms = setUniforms(gl, program);
     * 
     * (En el dibujado, actualizamos valores de las variables).
     * uniforms.u_matrix = projectionMatrix;
     * uniforms.slice = [lines.nX, lines.pX, lines.nY];
     * uniforms.numPointsForLines = lines.totalPoints;
     * setUniforms(uniforms);
     * 
     * Solo acepta arreglos con a los más 10 elementos y 
     * constructores con un solo nivel de profundidad. Por ejemplo:
     *    u_spot_lights[2].la
     *    u_light.la
     * 
     * 
     * @param {WebGLRenderingContext} gl el WebGLRenderingContext.
     * @param {WebGLProgram} program el programa al que se le agregaran las variables uniformes.
     * @param {Object} locations las variables uniformes a crear.
     */
    setUniforms(gl, program){
        var uniformSetters = {};

        const numUniforms = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS);
        for (let i = 0; i < numUniforms; ++i) {
            const info = gl.getActiveUniform(program, i);
            let setter = this.getUniformSetter(gl, info.type);
            uniformSetters[info.name] = setter;;
            
        }
        function setUniforms(uniforms){
            const numUniforms = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS);
            for (let i = 0; i < numUniforms; ++i) {
                const info = gl.getActiveUniform(program, i);
                let splitName = info.name.split('.');
                let uniformValue;
                if(splitName.length == 1)
                    uniformValue = uniforms[info.name];
                else{//Para arreglos
                    if(splitName[0].indexOf('[') > 0){
                        let index = splitName[0].indexOf('[') + 1;
                        let name_list = splitName[0].substr(0, index-1);
                        uniformValue = uniforms[name_list][splitName[0][index]][splitName[1]];
                    }else//Para diccionarios 
                    uniformValue = uniforms[splitName[0]][splitName[1]];
                }
                const location = gl.getUniformLocation(program, info.name);
                let setter = uniformSetters[info.name];
                if(info.type == gl.SAMPLER_2D || info.type == gl.SAMPLER_CUBE){
                    const textureId = uniformValue.id;
                    const texture = uniformValue.texture;
                    setter(location, textureId, texture);
                }
                else
                    setter(location, uniformValue);
                
            }
        }
        return setUniforms;
    }
    /**
     * Devuelve la función asociada a la tipo de variable uniforme dada.
     * 
     * @param {*} gl 
     * @param {*} type 
     */
    getUniformSetter(gl, type){
        let f;
        if(type == gl.FLOAT){
            return function(location, value) { 
                gl.uniform1f(location, value);
            };
        }
        if( type == gl.FLOAT_VEC2){
            return function(location, value) { 
                gl.uniform2fv(location, value);
            };
        }
        if( type == gl.FLOAT_VEC3){
            return function(location, value) { 
                gl.uniform3fv(location, value);
            };
        }
        if( type == gl.FLOAT_VEC4){
            return function(location, value) { 
                gl.uniform4fv(location, value);
            };
        }
        if( type == gl.INT || type == gl.BOOL){
            return function(location, value) { 
                gl.uniform1i(location, value);
            };
        }
        if( type == gl.INT_VEC2 || type == gl.BOOL_VEC2){
            return function(location, value) { 
                gl.uniform2iv(location, value);
            };
        }
        if( type == gl.INT_VEC3 || type == gl.BOOL_VEC3){
            return function(location, value) { 
                gl.uniform3iv(location, value);
            };
        }
        if( type == gl.INT_VEC4 || type == gl.BOOL_VEC4){
            return function(location, value) { 
                gl.uniform4iv(location, value);
            };
        }
        if( type == gl.FLOAT_MAT2){
            return function(location, value) { 
                gl.uniformMatrix2fv(location, false, value);
            };
        }
        if( type == gl.FLOAT_MAT3){
            return function(location, value) { 
                gl.uniformMatrix3fv(location, false, value);
            };
        }
        if( type == gl.FLOAT_MAT4){
            return function(location, value) { 
                gl.uniformMatrix4fv(location, false, value);
            };
        }
        if( type == gl.SAMPLER_2D){
            return function(location, id, texture){
                gl.uniform1i(location, id);
                gl.activeTexture(gl.TEXTURE0 + id);
                gl.bindTexture(gl.TEXTURE_2D, texture);
            }
        }
        if( type == gl.SAMPLER_CUBE){
            return function(location, id, texture){
                gl.uniform1i(location, id);
                gl.activeTexture(gl.TEXTURE0 + id);
                gl.bindTexture(gl.TEXTURE_CUBE_MAP, texture);
            }
        }
        throw "Tipo no implementado";
    }
    /**
     * Carga una única tectura.
     * @param {*} gl 
     * @param {*} url 
     * @param {*} draw_callback 
     * @param {*} filters 
     * @returns 
     */
    loadImageTexture(gl, url, draw_callback, filters={}) {
        const MIN_FILTER = (filters.min != undefined) ? filters.min : gl.LINEAR_MIPMAP_LINEAR;
        const MAG_FILTER = (filters.mag != undefined) ? filters.mag : gl.LINEAR;
        const WRAP_S = (filters.wrap_s != undefined) ? filters.wrap_s : gl.CLAMP_TO_EDGE;
        const WRAP_T = (filters.wrap_t != undefined) ? filters.wrap_t : gl.CLAMP_TO_EDGE;
        // Create a texture.
        var texture = gl.createTexture();
        
        gl.bindTexture(gl.TEXTURE_2D, texture);
        // Fill the texture with a 1x1 lavender pixel.
        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE,
                        new Uint8Array([230, 230, 250, 255]));
        
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, WRAP_S);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, WRAP_T);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, MIN_FILTER);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, MAG_FILTER);
        // Asynchronously load an image
        const image = new Image();
        image.src = url;
        image.addEventListener('load', function() {
            // Now that the image has loaded make copy it to the texture.
            gl.bindTexture(gl.TEXTURE_2D, texture);
            gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
            // assumes this texture is a power of 2
            gl.generateMipmap(gl.TEXTURE_2D);
            draw_callback(texture);
        });
        return texture;
      }
    /**
     * Carga una única textura sin crear mipmaps.
     * @param {*} gl 
     * @param {*} url 
     * @param {*} draw_callback 
     * @returns 
     */
    loadImageTextureNoMipmap(gl, url, draw_callback) {
        // Create a texture.
        var texture = gl.createTexture();
        
        gl.bindTexture(gl.TEXTURE_2D, texture);
        // Fill the texture with a 1x1 lavender pixel.
        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE,
                        new Uint8Array([230, 230, 250, 255]));
        
        // Asynchronously load an image
        const image = new Image();
        
        image.src = url;
        image.addEventListener('load', function() {
            // Now that the image has loaded make copy it to the texture.
            gl.bindTexture(gl.TEXTURE_2D, texture);
            gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
            draw_callback();
        });
        
        return texture;
    }  
    /**
     * Carga un arreglo de texturas.
     * @param {*} urls 
     * @param {*} draw_callback 
     */
    loadImages(urls, draw_callback) {
        let images = {};
        var imagesToLoad = urls.length;
        urls.forEach( url => {
            let image = new Image();
            image.src = url;
            image.addEventListener("load", function() {
                images[url] = image;
                --imagesToLoad;
                
                if(imagesToLoad == 0){
                    draw_callback(images);
                }
            });
            image.addEventListener("error", (evt) => {
                throw `Error loading image: ${url}`;
            });
        });

    }
    /**
     * Crea las texturas de un arreglo de imágenes previamente cargado,
     * y se devuelven en la función de retorno.
     * @param {*} gl 
     * @param {*} images 
     * @param {*} urls 
     * @param {*} draw_callback 
     * @param {*} filters 
     */
    createTextures(gl, images, urls, draw_callback, filters={}){
        const MIN_FILTER = (filters.min != undefined) ? filters.min : gl.LINEAR;
        const MAG_FILTER = (filters.mag != undefined) ? filters.mag : gl.LINEAR;
        const WRAP_S = (filters.wrap_s != undefined) ? filters.wrap_s : gl.REPEAT;
        const WRAP_T = (filters.wrap_t != undefined) ? filters.wrap_t : gl.REPEAT;
        let textures = [];
        for(var i = 0; i < urls.length; i++){
            let texture = gl.createTexture();
            gl.activeTexture(gl.TEXTURE0 + i);
            gl.bindTexture(gl.TEXTURE_2D, texture);
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, WRAP_S);
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, WRAP_T);
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, MIN_FILTER);
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, MAG_FILTER);
            gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA,gl.UNSIGNED_BYTE, images[urls[i]]);
            gl.generateMipmap(gl.TEXTURE_2D);
            textures.push(texture);
        }
        draw_callback(textures);
    }
    /**
     * Crea una textura cube map.
     * En la info del cube map se indica los urls de la imágenes a utilizar 
     * y su tipo, el tipo seberá ser alguno de los siguientes:
     * gl.TEXTURE_CUBE_MAP_POSITIVE_X, gl.TEXTURE_CUBE_MAP_NEGATIVE_X, 
     * gl.TEXTURE_CUBE_MAP_POSITIVE_Y, gl.TEXTURE_CUBE_MAP_NEGATIVE_Y, 
     * gl.TEXTURE_CUBE_MAP_POSITIVE_Z, and gl.TEXTURE_CUBE_MAP_NEGATIVE_Z.
     * Ej:
     *     cube_map_info = { url: /url, type: gl.TEXTURE_CUBE_MAP_POSITIVE_X}
     * @param {*} gl 
     * @param {*} images las imágenes que conforman el cube map
     * @param {Object} cube_map_info la información para construir el cube map, 
     *                               descrita previamente.
     * @param {*} draw_callback la función de retorno.
     */
    createCubeMap(gl, images, cube_map_info, draw_callback){      
        var texture = gl.createTexture();
        gl.bindTexture(gl.TEXTURE_CUBE_MAP, texture);
        cube_map_info.forEach(info => {
            var {url, type} = info;
            gl.texImage2D(type, 0, gl.RGBA, gl.RGBA,gl.UNSIGNED_BYTE, images[url]);
        });
        gl.generateMipmap(gl.TEXTURE_CUBE_MAP);
        gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
        gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
        gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);          
        draw_callback(texture);
    }
}

let WebGL = new WebGLUtils();

export default WebGL;