class OBJGeometry {
  /**
   * 
   */
  constructor(gl, data="", mat_data="", material=new PhongWireframeMaterial(gl), transform=identity()) {
    this.data = data;
    this.material = material;
    this.transform = transform;

    // Se parsea el archivo de materiales que usa el .obj
    this.getMaterialParams(mat_data);


    // se divide la información del .obj en un arreglo de líneas de texto
    let lines = data.split("\n");
    let initial_vertices = [];
    let initial_normals = [];
    let faces = [];
    this.group_faces = [];

    // Se almacenan los valores de las coordenadas de vertices y normales
    for (const line of lines) {
      // vertices
      if (line.startsWith("v ")) {
        data = line.split(" ");
        data.shift();
        initial_vertices.push(data);
      }

      // normales
      else if (line.startsWith("vn ")) {
        data = line.split(" ");
        data.shift();
        initial_normals.push(data);
      }

      // caras
      else if (line.startsWith("f ")) {
        data = line.split(" ");
        data.shift();

        // cara definida como un triángulo
        if (data.length === 3) {
          faces.push(data);
        }
        // cara definida como un cuadrilátero
        // se agregan dos triángulos
        else if (data.length === 4) {
          faces.push([ data[0], data[1], data[2] ]);
          faces.push([ data[0], data[2], data[3] ]);
        }
      }

      // El grupo de elementos que usan un material particular
      else if (line.startsWith("usemtl ")) {
        this.group_faces.push({
          mat_name: line.split(" ")[1],
          initial_index: faces.length
        });
      }
    }

    // se agrega al grupo de caras el número de elementos
    for (let i=0; i<this.group_faces.length; i++) {
      // los elementos anteriores al último
      if (i+1 < this.group_faces.length) {
        this.group_faces[i].num_elements = this.group_faces[i+1].initial_index - this.group_faces[i].initial_index;
      }
      // el último elemento
      else {
        this.group_faces[i].num_elements = faces.length - this.group_faces[i].initial_index;
      }
    }

    // Se construye la información para los buffers de datos correspondientes
    let vertices = [];
    let normals = [];
    let tmp1, tmp2, tmp3;

    for(let i=0,l=faces.length; i<l; i++) {
      tmp1 = faces[i][0].split("/");
      tmp2 = faces[i][1].split("/");
      tmp3 = faces[i][2].split("/");

      // Coordenadas de los vértices
      vertices.push(
        // coordenadas del primer vértice
        initial_vertices[parseInt(tmp1[0])-1][0], // x
        initial_vertices[parseInt(tmp1[0])-1][1], // y
        initial_vertices[parseInt(tmp1[0])-1][2], // z
        // coordenadas del segundo vértice
        initial_vertices[parseInt(tmp2[0])-1][0], // x
        initial_vertices[parseInt(tmp2[0])-1][1], // y
        initial_vertices[parseInt(tmp2[0])-1][2], // z
        // coordenadas del tercer vértice
        initial_vertices[parseInt(tmp3[0])-1][0], // x
        initial_vertices[parseInt(tmp3[0])-1][1], // y
        initial_vertices[parseInt(tmp3[0])-1][2], // z
      );

      // Coordenadas de las normales
      if (tmp1[2]) {
        normals.push(
          // coordenadas de la normal del primer vértice
          initial_normals[parseInt(tmp1[2])-1][0], // x
          initial_normals[parseInt(tmp1[2])-1][1], // y
          initial_normals[parseInt(tmp1[2])-1][2], // z
          // coordenadas de la normal del segundo vértice
          initial_normals[parseInt(tmp2[2])-1][0], // x
          initial_normals[parseInt(tmp2[2])-1][1], // y
          initial_normals[parseInt(tmp2[2])-1][2], // z
          // coordenadas de la normal del tercer vértice
          initial_normals[parseInt(tmp3[2])-1][0], // x
          initial_normals[parseInt(tmp3[2])-1][1], // y
          initial_normals[parseInt(tmp3[2])-1][2], // z
        );
      }
    }


    let positionAttributeLocation = gl.getAttribLocation(this.material.program, "a_position");
    let normalAttributeLocation = gl.getAttribLocation(this.material.program, "a_normal");

    // Se crea el Vertex Array Object
    this.geometryVAO = gl.createVertexArray();
    gl.bindVertexArray(this.geometryVAO);

    // Se crea el buffer de datos para las posiciones
    this.positionBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, this.positionBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
    gl.enableVertexAttribArray(positionAttributeLocation);
    gl.vertexAttribPointer(positionAttributeLocation, 3, gl.FLOAT, false, 0, 0);


    // Si el .obj tiene información de las normales se crea el buffer correspondiente
    if (normals.length > 0) {
      this.normalsBuffer = gl.createBuffer();
      gl.bindBuffer(gl.ARRAY_BUFFER, this.normalsBuffer);
      gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(normals), gl.STATIC_DRAW);
      gl.enableVertexAttribArray(normalAttributeLocation);
      gl.vertexAttribPointer(normalAttributeLocation, 3, gl.FLOAT, false, 0, 0);
    }

    let barycentric = this.getBarycentric(vertices);
    let barycentricBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, barycentricBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(barycentric), gl.STATIC_DRAW);
    gl.enableVertexAttribArray(this.material.getAttribute("a_barycentric"));
    gl.vertexAttribPointer(this.material.getAttribute("a_barycentric"), 3, gl.FLOAT, false, 0, 0);

    gl.bindVertexArray(null);
    gl.bindBuffer(gl.ARRAY_BUFFER, null);
  }
  
  /**
   * 
   */
  getMaterialParams(material_txt) {
    // Objeto para almacenar la información de los materiales
    this.material_library = {};

    // Nombre del material actual
    let name;
    let tmp;

    // Se divide el contenido del archivo en líneas
    let lines = material_txt.split("\n");

    // Se itera sobre cada línea de texto
    for (const line of lines) {
      // Se obtiene el nombre del material
      if (line.startsWith("newmtl ")) {
        name = line.split(" ")[1];
        // Se crea un objeto temporal para almacenar los parámetros del material
        this.material_library[name] = {};
      }

      // Componente ambiental
      else if (line.startsWith("Ka ")) {
        tmp = line.split(" ");
        this.material_library[name].Ka = [
          tmp[1], // R
          tmp[2], // G
          tmp[3]  // B
        ];
      }

      // Componente difuso
      else if (line.startsWith("Kd ")) {
        tmp = line.split(" ");
        this.material_library[name].Kd = [
          tmp[1], // R
          tmp[2], // G
          tmp[3]  // B
        ];
      }

      // Componente especular
      else if (line.startsWith("Ks ")) {
        tmp = line.split(" ");
        this.material_library[name].Ks = [
          tmp[1], // R
          tmp[2], // G
          tmp[3]  // B
        ];
      }

      // Exponente especular (shininess)
      else if (line.startsWith("Ns ")) {
        tmp = line.split(" ");
        this.material_library[name].shininess = tmp[1];
      }
    }
  }

  /**
   */
  draw(gl, projectionMatrix, viewMatrix, light) {
    let viewModelMatrix = multiply(viewMatrix, this.transform);
    let projectionViewModelMatrix = multiply(projectionMatrix, viewModelMatrix);

    gl.useProgram(this.material.program);

    // u_VM_matrix
    if (this.material.getUniform("u_VM_matrix") != undefined) {
      gl.uniformMatrix4fv(this.material.getUniform("u_VM_matrix"), true, viewModelMatrix);
    }

    // u_PVM_matrix
    if (this.material.getUniform("u_PVM_matrix") != undefined) {
      gl.uniformMatrix4fv(this.material.getUniform("u_PVM_matrix"), true, projectionViewModelMatrix);
    }

    ////////////////////////////////////////////////////////////
    // Componentes de la luz
    ////////////////////////////////////////////////////////////
    // u_light.position
    if (this.material.getUniform("u_light.position") != undefined) {
      gl.uniform3fv(this.material.getUniform("u_light.position"), light.getPosition());
    }
    // u_light.La
    if (this.material.getUniform("u_light.La") != undefined) {
      gl.uniform3fv(this.material.getUniform("u_light.La"), light.ambient);
    }
    // u_light.Ld
    if (this.material.getUniform("u_light.Ld") != undefined) {
      gl.uniform3fv(this.material.getUniform("u_light.Ld"), light.diffuse);
    }
    // u_light.Ls
    if (this.material.getUniform("u_light.Ls") != undefined) {
      gl.uniform3fv(this.material.getUniform("u_light.Ls"), light.especular);
    }
    ////////////////////////////////////////////////////////////

    let tmp_material;

    // Se dibuja el objeto por grupos para enviar los valores correctos del material asociado
    for (let i=0, l=this.group_faces.length; i<l; i++) {
      tmp_material = this.material_library[this.group_faces[i].mat_name];

      // Si no existen el material en el objeto material_library, se usa el material asignado cuando se construye el modelo
      if (tmp_material == undefined) {
        tmp_material = this.material;
      }

      ////////////////////////////////////////////////////////////
      // Componentes del material
      ////////////////////////////////////////////////////////////
      // u_material.Ka
      if (this.material.getUniform("u_material.Ka") != undefined) {
        gl.uniform3fv(this.material.getUniform("u_material.Ka"), tmp_material.Ka);
      }
      // u_material.Kd
      if (this.material.getUniform("u_material.Kd") != undefined) {
        gl.uniform3fv(this.material.getUniform("u_material.Kd"), tmp_material.Kd);
      }
      // u_material.Ks
      if (this.material.getUniform("u_material.Ks") != undefined) {
        gl.uniform3fv(this.material.getUniform("u_material.Ks"), tmp_material.Ks);
      }
      // u_material.shininess
      if (this.material.getUniform("u_material.shininess") != undefined) {
        gl.uniform1f(this.material.getUniform("u_material.shininess"), tmp_material.shininess);
      }
      // u_material.color
      if (this.material.getUniform("u_material.color") != undefined) {
        gl.uniform4fv(this.material.getUniform("u_material.color"), this.material.color);
      }
      ////////////////////////////////////////////////////////////

      gl.bindVertexArray(this.geometryVAO);

      // Solo se dibujan los elementos desde el vértice inicial, hasta el número de elementos de cada grupo
      gl.drawArrays(
        gl.TRIANGLES, 
        this.group_faces[i].initial_index*3, 
        this.group_faces[i].num_elements*3
      );
      gl.bindVertexArray(null);
    }
  }

  getBarycentric(position) {
    const count = position.length / 9;
    const barycentric = [];
  
    for (let i=0; i < count; i++) {
      barycentric.push(
        0, 0, 1, 
        0, 1, 0, 
        1, 0, 0
      );
    }
  
    return barycentric;
  }
}
