createProgram() {
        // set program id and type
        id++;
        this.id = id;

        // we need to create a new shader program
        this.program = this.gl.createProgram();

        // if shaders are valid, go on
        this.gl.attachShader(this.program, this.vertexShader);
        this.gl.attachShader(this.program, this.fragmentShader);
        this.gl.linkProgram(this.program);

        // TODO getProgramParameter even in production to avoid errors?
        // check the shader program creation status only when not in production mode
        if(!this.renderer.production) {
            if(!this.gl.getProgramParameter(this.program, this.gl.LINK_STATUS)) {
                throwWarning(this.type + ": Unable to initialize the shader program: " + this.gl.getProgramInfoLog(this.program));

                // use default shaders, recompile and recreate program
                throwWarning(this.type + ": Will use default vertex and fragment shaders");
                this.vertexShader = this.createShader(this.defaultVsCode, this.gl.VERTEX_SHADER);
                this.fragmentShader = this.createShader(this.defaultFsCode, this.gl.FRAGMENT_SHADER);
                this.createProgram();

                return;
            }
        }

        // free the shaders handles
        this.gl.deleteShader(this.vertexShader);
        this.gl.deleteShader(this.fragmentShader);

        // store active textures (those that are used in the shaders) to avoid binding unused textures
        if(!this.activeUniforms || !this.activeAttributes) {
            this.activeUniforms = {
                textures: [],
                textureMatrices: [],
            };

            // check for program active textures
            const numUniforms = this.gl.getProgramParameter(this.program, this.gl.ACTIVE_UNIFORMS);
            for(let i = 0; i < numUniforms; i++) {
                const activeUniform = this.gl.getActiveUniform(this.program, i);

                if(activeUniform.type === this.gl.SAMPLER_2D) {
                    // if it's a texture add it to our activeUniforms textures array
                    this.activeUniforms.textures.push(activeUniform.name);
                }
                if(activeUniform.type === this.gl.FLOAT_MAT4 && activeUniform.name !== "uMVMatrix" && activeUniform.name !== "uPMatrix") {
                    // if it's a texture matrix add it to our activeUniforms textureMatrices array
                    this.activeUniforms.textureMatrices.push(activeUniform.name);
                }
            }

            this.activeAttributes = [];

            // check for program active attributes to avoid binding attribute buffer if attribute is unused
            const numAttributes = this.gl.getProgramParameter(this.program, this.gl.ACTIVE_ATTRIBUTES);
            for(let i = 0; i < numAttributes; i++) {
                const activeAttribute = this.gl.getActiveAttrib(this.program, i);
                // push attribute name
                this.activeAttributes.push(activeAttribute.name);
            }
        }
    }
Used internally to set up program based on the created shaders and attach them to the program
     Sets a list of active textures that are actually used by the shaders to avoid binding unused textures during draw calls
     Add the program to the cache
*