var merge = require('merge');
var geom = require('pex-geom')
var Context = require('./Context');
var RenderableGeometry = require('./RenderableGeometry');
var Vec3 = geom.Vec3
var Quat = geom.Quat
var Mat4 = geom.Mat4
var BoundingBox = geom.BoundingBox;
function Mesh(geometry, material, options) {
  this.gl = Context.currentContext;
  this.geometry = merge(geometry, RenderableGeometry);
  this.material = material;
  options = options || {};
  this.primitiveType = options.primitiveType;
  if (this.primitiveType == null) {
    this.primitiveType = this.gl.TRIANGLES;
  }
  if (options.lines) {
    this.primitiveType = this.gl.LINES;
  }
  if (options.triangles) {
    this.primitiveType = this.gl.TRIANGLES;
  }
  if (options.points) {
    this.primitiveType = this.gl.POINTS;
  }
  this.position = Vec3.create(0, 0, 0);
  this.rotation = Quat.create();
  this.scale = Vec3.create(1, 1, 1);
  this.projectionMatrix = Mat4.create();
  this.viewMatrix = Mat4.create();
  this.invViewMatrix = Mat4.create();
  this.modelWorldMatrix = Mat4.create();
  this.modelViewMatrix = Mat4.create();
  this.rotationMatrix = Mat4.create();
  this.normalMatrix = Mat4.create();
}
Mesh.extensions = {};
Mesh.prototype.draw = function(camera) {
  if (this.geometry.isDirty()) {
    this.geometry.compile();
  }
  if (camera) {
    this.updateMatrices(camera);
    this.updateMatricesUniforms(this.material);
  }
  this.material.use();
  var numInstances = this.bindAttribs();
  if (numInstances > 0) {
    var drawElementsInstanced;
    if (this.gl.drawElementsInstanced) {
      drawElementsInstanced = this.gl.drawElementsInstanced.bind(this.gl);
    }
    if (!drawElementsInstanced) {
      if (!Mesh.extensions.instancedArrays) {
        Mesh.extensions.instancedArrays = this.gl.getExtension("ANGLE_instanced_arrays");
        if (!Mesh.extensions.instancedArrays) {
          throw 'Mesh has instanced geometry but ANGLE_instanced_arrays is not available';
        }
      }
      drawElementsInstanced = Mesh.extensions.instancedArrays.drawElementsInstancedANGLE.bind(Mesh.extensions.instancedArrays);
    }
    if (this.geometry.faces && this.geometry.faces.length > 0 && this.primitiveType !== this.gl.LINES && this.primitiveType !== this.gl.POINTS) {
      this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, this.geometry.faces.buffer.handle);
      drawElementsInstanced(this.primitiveType, this.geometry.faces.buffer.dataBuf.length, this.gl.UNSIGNED_SHORT, 0, numInstances);
    }
    else if (this.geometry.edges && this.geometry.edges.length > 0 && this.primitiveType === this.gl.LINES) {
      this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, this.geometry.edges.buffer.handle);
      drawElementsInstanced(this.primitiveType, this.geometry.edges.buffer.dataBuf.length, this.gl.UNSIGNED_SHORT, 0, numInstances);
    }
  }
  else {
    if (this.geometry.faces && this.geometry.faces.length > 0 && this.primitiveType !== this.gl.LINES && this.primitiveType !== this.gl.POINTS) {
      this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, this.geometry.faces.buffer.handle);
      this.gl.drawElements(this.primitiveType, this.geometry.faces.buffer.dataBuf.length, this.gl.UNSIGNED_SHORT, 0);
    }
    else if (this.geometry.edges && this.geometry.edges.length > 0 && this.primitiveType === this.gl.LINES) {
      this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, this.geometry.edges.buffer.handle);
      this.gl.drawElements(this.primitiveType, this.geometry.edges.buffer.dataBuf.length, this.gl.UNSIGNED_SHORT, 0);
    }
    else if (this.geometry.vertices) {
      var num = this.geometry.vertices.length;
      this.gl.drawArrays(this.primitiveType, 0, num);
    }
  }
  this.unbindAttribs();
};
Mesh.prototype.drawInstances = function(camera, instances) {
  if (this.geometry.isDirty()) {
    this.geometry.compile();
  }
  if (camera) {
    this.updateMatrices(camera);
    this.updateMatricesUniforms(this.material);
  }
  this.material.use();
  this.bindAttribs();
  if (this.geometry.faces && this.geometry.faces.length > 0 && !this.useEdges) {
    this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, this.geometry.faces.buffer.handle);
    for (var i = 0; i < instances.length; i++) {
      var instance = instances[i];
      if (camera) {
        this.updateMatrices(camera, instance);
        this.updateMatricesUniforms(this.material);
        this.updateUniforms(this.material, instance);
        this.material.use();
      }
      this.gl.drawElements(this.primitiveType, this.geometry.faces.buffer.dataBuf.length, this.gl.UNSIGNED_SHORT, 0);
    }
  }
  else if (this.geometry.edges && this.useEdges) {
    this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, this.geometry.edges.buffer.handle);
    for (var i = 0; i < instances.length; i++) {
      var instance = instances[i];
      if (camera) {
        this.updateMatrices(camera, instance);
        this.updateMatricesUniforms(this.material);
        this.updateUniforms(this.material, instance);
        this.material.use();
      }
      this.gl.drawElements(this.primitiveType, this.geometry.edges.buffer.dataBuf.length, this.gl.UNSIGNED_SHORT, 0);
    }
  }
  else if (this.geometry.vertices) {
    var num = this.geometry.vertices.length;
    for (var i = 0; i < instances.length; i++) {
      var instance = instances[i];
      if (camera) {
        this.updateMatrices(camera, instance);
        this.updateMatricesUniforms(this.material);
        this.updateUniforms(this.material, instance);
        this.material.use();
      }
      this.gl.drawArrays(this.primitiveType, 0, num);
    }
  }
  return this.unbindAttribs();
};
Mesh.prototype.bindAttribs = function() {
  var numInstances = 0;
  var program = this.material.program;
  for (name in this.geometry.attribs) {
    var attrib = this.geometry.attribs[name];
    attrib.location = this.gl.getAttribLocation(program.handle, attrib.name);
    if (attrib.location >= 0) {
      this.gl.bindBuffer(this.gl.ARRAY_BUFFER, attrib.buffer.handle);
      this.gl.vertexAttribPointer(attrib.location, attrib.buffer.elementSize, this.gl.FLOAT, false, 0, 0);
      this.gl.enableVertexAttribArray(attrib.location);
      if (attrib.instanced) {
        this.vertexAttribDivisor(attrib.location, 1);
        numInstances = attrib.length;
      }
    }
  }
  return numInstances;
}
Mesh.prototype.unbindAttribs = function() {
  for (name in this.geometry.attribs) {
    var attrib = this.geometry.attribs[name];
    if (attrib.location >= 0) {
      if (attrib.instanced) {
        this.vertexAttribDivisor(attrib.location, 0);
      }
      this.gl.disableVertexAttribArray(attrib.location);
    }
  }
};
Mesh.prototype.vertexAttribDivisor = function(location, divisor) {
  if (this.gl.vertexAttribDivisor) {
    this.gl.vertexAttribDivisor(location, divisor);
  }
  else {
    if (!Mesh.extensions.instancedArrays) {
      Mesh.extensions.instancedArrays = this.gl.getExtension("ANGLE_instanced_arrays");
      if (!Mesh.extensions.instancedArrays) {
        throw 'Mesh has instanced geometry but ANGLE_instanced_arrays is not available';
      }
    }
    Mesh.extensions.instancedArrays.vertexAttribDivisorANGLE(location, divisor);
  }
}
Mesh.prototype.resetAttribLocations = function() {
  for (name in this.geometry.attribs) {
    var attrib = this.geometry.attribs[name];
    attrib.location = -1;
  }
};
Mesh.prototype.updateMatrices = function(camera, instance) {
  var position = instance && instance.position ? instance.position : this.position;
  var rotation = instance && instance.rotation ? instance.rotation : this.rotation;
  var scale = instance && instance.scale ? instance.scale : this.scale;
  rotation.toMat4(this.rotationMatrix);
  this.modelWorldMatrix.identity().translate(position.x, position.y, position.z).mul(this.rotationMatrix).scale(scale.x, scale.y, scale.z);
  if (camera) {
    this.projectionMatrix.copy(camera.getProjectionMatrix());
    this.viewMatrix.copy(camera.getViewMatrix());
    this.invViewMatrix.copy(camera.getViewMatrix().dup().invert());
    this.modelViewMatrix.copy(camera.getViewMatrix()).mul(this.modelWorldMatrix);
    return this.normalMatrix.copy(this.modelViewMatrix).invert().transpose();
  }
};
Mesh.prototype.updateUniforms = function(material, instance) {
  for (uniformName in instance.uniforms) {
    var uniformValue = instance.uniforms[uniformName];
    material.uniforms[uniformName] = uniformValue;
  }
};
Mesh.prototype.updateMatricesUniforms = function(material) {
  var materialUniforms, programUniforms;
  programUniforms = this.material.program.uniforms;
  materialUniforms = this.material.uniforms;
  if (programUniforms.projectionMatrix) {
    materialUniforms.projectionMatrix = this.projectionMatrix;
  }
  if (programUniforms.viewMatrix) {
    materialUniforms.viewMatrix = this.viewMatrix;
  }
  if (programUniforms.invViewMatrix) {
    materialUniforms.invViewMatrix = this.invViewMatrix;
  }
  if (programUniforms.modelWorldMatrix) {
    materialUniforms.modelWorldMatrix = this.modelWorldMatrix;
  }
  if (programUniforms.modelViewMatrix) {
    materialUniforms.modelViewMatrix = this.modelViewMatrix;
  }
  if (programUniforms.normalMatrix) {
    return materialUniforms.normalMatrix = this.normalMatrix;
  }
};
Mesh.prototype.getMaterial = function() {
  return this.material;
};
Mesh.prototype.setMaterial = function(material) {
  this.material = material;
  return this.resetAttribLocations();
};
Mesh.prototype.getProgram = function() {
  return this.material.program;
};
Mesh.prototype.setProgram = function(program) {
  this.material.program = program;
  return this.resetAttribLocations();
};
Mesh.prototype.dispose = function() {
  return this.geometry.dispose();
};
Mesh.prototype.getBoundingBox = function() {
  if (!this.boundingBox) {
    this.updateBoundingBox();
  }
  return this.boundingBox;
};
Mesh.prototype.updateBoundingBox = function() {
  this.updateMatrices();
  return this.boundingBox = BoundingBox.fromPoints(this.geometry.vertices.map((function(_this) {
    return function(v) {
      return v.dup().transformMat4(_this.modelWorldMatrix);
    };
  })(this)));
};
module.exports = Mesh;