/*
Utilities for the OpenGL ES 2.0 HTML Canvas context

Copyright (C) 2009  Ilmari Heikkinen <ilmari.heikkinen@gmail.com>

Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
*/

function loadTexture(gl, elem, mipmaps) {
  var tex = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, tex);
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, elem);
  if (mipmaps != false)
    gl.generateMipmap(gl.TEXTURE_2D);
  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_MAG_FILTER, gl.LINEAR);
  if (mipmaps)
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
  else
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
  return tex;
}

function getShader(gl, id) {
  var shaderScript = document.getElementById(id);
  if (!shaderScript) {
    throw(new Error("No shader element with id: "+id));
  }

  var str = "";
  var k = shaderScript.firstChild;
  while (k) {
    if (k.nodeType == 3)
      str += k.textContent;
    k = k.nextSibling;
  }

  var shader;
  if (shaderScript.type == "x-shader/x-fragment") {
    shader = gl.createShader(gl.FRAGMENT_SHADER);
  } else if (shaderScript.type == "x-shader/x-vertex") {
    shader = gl.createShader(gl.VERTEX_SHADER);
  } else {
    throw(new Error("Unknown shader type "+shaderScript.type));
  }

  gl.shaderSource(shader, str);
  gl.compileShader(shader);

  if (gl.getShaderParameter(shader, gl.COMPILE_STATUS) != 1) {
    var ilog = gl.getShaderInfoLog(shader);
    gl.deleteShader(shader);
    throw(new Error("Failed to compile shader "+shaderScript.id + ", Shader info log: " + ilog));
  }
  return shader;
}

function loadShaderArray(gl, shaders) {
  var id = gl.createProgram();
  var shaderObjs = [];
  for (var i=0; i<shaders.length; ++i) {
    try {
      var sh = getShader(gl, shaders[i]);
      shaderObjs.push(sh);
      gl.attachShader(id, sh);
    } catch (e) {
      var pr = {program: id, shaders: shaderObjs};
      deleteShader(gl, pr);
      throw (e);
    }
  }
  var prog = {program: id, shaders: shaderObjs};
  gl.linkProgram(id);
  gl.validateProgram(id);
  if (gl.getProgramParameter(id, gl.LINK_STATUS) != 1) {
    deleteShader(gl,prog);
    throw(new Error("Failed to link shader"));
  }
  if (gl.getProgramParameter(id, gl.VALIDATE_STATUS) != 1) {
    deleteShader(gl,prog);
    throw(new Error("Failed to validate shader"));
  }
  return prog;
}
function loadShader(gl) {
  var sh = [];
  for (var i=1; i<arguments.length; ++i)
    sh.push(arguments[i]);
  return loadShaderArray(gl, sh);
}

function deleteShader(gl, sh) {
  gl.useProgram(null);
  sh.shaders.forEach(function(s){
    gl.detachShader(sh.program, s);
    gl.deleteShader(s);
  });
  gl.deleteProgram(sh.program);
}

function getGLErrorAsString(ctx, err) {
  if (err === ctx.NO_ERROR) {
    return "NO_ERROR";
  }
  for (var name in ctx) {
    if (ctx[name] === err) {
      return name;
    }
  }
  return err.toString();
}

function checkError(gl, msg) {
  var e = gl.getError();
  if (e != gl.NO_ERROR) {
    log("Error " + getGLErrorAsString(gl, e) + " at " + msg);
  }
  return e;
}

function throwError(gl, msg) {
  var e = gl.getError();
  if (e != 0) {
    throw(new Error("Error " + getGLErrorAsString(gl, e) + " at " + msg));
  }
}

Math.cot = function(z) { return 1.0 / Math.tan(z); }

/*
  Matrix utilities, using the OpenGL element order where
  the last 4 elements are the translation column.

  Uses flat arrays as matrices for performance.

  Most operations have in-place variants to avoid allocating temporary matrices.

  Naming logic:
    Matrix.method operates on a 4x4 Matrix and returns a new Matrix.
    Matrix.method3x3 operates on a 3x3 Matrix and returns a new Matrix. Not all operations have a 3x3 version (as 3x3 is usually only used for the normal matrix: Matrix.transpose3x3(Matrix.inverseTo3x3(mat4x4)))
    Matrix.method[3x3]InPlace(args, target) stores its result in the target matrix.

    Matrix.scale([sx, sy, sz]) -- non-uniform scale by vector
    Matrix.scale1(s)           -- uniform scale by scalar
    Matrix.scale3(sx, sy, sz)  -- non-uniform scale by scalars

    Ditto for translate.
*/
Matrix = {
  identity : [
    1.0, 0.0, 0.0, 0.0,
    0.0, 1.0, 0.0, 0.0,
    0.0, 0.0, 1.0, 0.0,
    0.0, 0.0, 0.0, 1.0
  ],

  newIdentity : function() {
    return [
      1.0, 0.0, 0.0, 0.0,
      0.0, 1.0, 0.0, 0.0,
      0.0, 0.0, 1.0, 0.0,
      0.0, 0.0, 0.0, 1.0
    ];
  },

  newIdentity3x3 : function() {
    return [
      1.0, 0.0, 0.0,
      0.0, 1.0, 0.0,
      0.0, 0.0, 1.0
    ];
  },

  copyMatrix : function(src, dst) {
    for (var i=0; i<16; i++) dst[i] = src[i];
    return dst;
  },

  to3x3 : function(m) {
    return [
      m[0], m[1], m[2],
      m[4], m[5], m[6],
      m[8], m[9], m[10]
    ];
  },

  // orthonormal matrix inverse
  inverseON : function(m) {
    var n = this.transpose4x4(m);
    var t = [m[12], m[13], m[14]];
    n[3] = n[7] = n[11] = 0;
    n[12] = -Vec3.dot([n[0], n[4], n[8]], t);
    n[13] = -Vec3.dot([n[1], n[5], n[9]], t);
    n[14] = -Vec3.dot([n[2], n[6], n[10]], t);
    return n;
  },

  inverseTo3x3 : function(m) {
    return this.inverse4x4to3x3InPlace(m, this.newIdentity3x3());
  },

  inverseTo3x3InPlace : function(m,n) {
    var a11 = m[10]*m[5]-m[6]*m[9],
        a21 = -m[10]*m[1]+m[2]*m[9],
        a31 = m[6]*m[1]-m[2]*m[5],
        a12 = -m[10]*m[4]+m[6]*m[8],
        a22 = m[10]*m[0]-m[2]*m[8],
        a32 = -m[6]*m[0]+m[2]*m[4],
        a13 = m[9]*m[4]-m[5]*m[8],
        a23 = -m[9]*m[0]+m[1]*m[8],
        a33 = m[5]*m[0]-m[1]*m[4];
    var det = m[0]*(a11) + m[1]*(a12) + m[2]*(a13);
    if (det == 0) // no inverse
      return [1,0,0,0,1,0,0,0,1];
    var idet = 1 / det;
    n[0] = idet*a11;
    n[1] = idet*a21;
    n[2] = idet*a31;
    n[3] = idet*a12;
    n[4] = idet*a22;
    n[5] = idet*a32;
    n[6] = idet*a13;
    n[7] = idet*a23;
    n[8] = idet*a33;
    return n;
  },

  inverse3x3 : function(m) {
    return this.inverse3x3InPlace(m, this.newIdentity3x3());
  },

  inverse3x3InPlace : function(m,n) {
    var a11 = m[8]*m[4]-m[5]*m[7],
        a21 = -m[8]*m[1]+m[2]*m[7],
        a31 = m[5]*m[1]-m[2]*m[4],
        a12 = -m[8]*m[3]+m[5]*m[6],
        a22 = m[8]*m[0]-m[2]*m[6],
        a32 = -m[5]*m[0]+m[2]*m[3],
        a13 = m[7]*m[4]-m[4]*m[8],
        a23 = -m[7]*m[0]+m[1]*m[6],
        a33 = m[4]*m[0]-m[1]*m[3];
    var det = m[0]*(a11) + m[1]*(a12) + m[2]*(a13);
    if (det == 0) // no inverse
      return [1,0,0,0,1,0,0,0,1];
    var idet = 1 / det;
    n[0] = idet*a11;
    n[1] = idet*a21;
    n[2] = idet*a31;
    n[3] = idet*a12;
    n[4] = idet*a22;
    n[5] = idet*a32;
    n[6] = idet*a13;
    n[7] = idet*a23;
    n[8] = idet*a33;
    return n;
  },

  frustum : function (left, right, bottom, top, znear, zfar) {
    var X = 2*znear/(right-left);
    var Y = 2*znear/(top-bottom);
    var A = (right+left)/(right-left);
    var B = (top+bottom)/(top-bottom);
    var C = -(zfar+znear)/(zfar-znear);
    var D = -2*zfar*znear/(zfar-znear);

    return [
      X, 0, 0, 0,
      0, Y, 0, 0,
      A, B, C, -1,
      0, 0, D, 0
    ];
 },

  perspective : function (fovy, aspect, znear, zfar) {
    var ymax = znear * Math.tan(fovy * Math.PI / 360.0);
    var ymin = -ymax;
    var xmin = ymin * aspect;
    var xmax = ymax * aspect;

    return this.frustum(xmin, xmax, ymin, ymax, znear, zfar);
  },

  mul4x4 : function (a,b) {
    return this.mul4x4InPlace(a,b,this.newIdentity());
  },

  mul4x4InPlace : function (a, b, c) {
        c[0] =   b[0] * a[0] +
                 b[0+1] * a[4] +
                 b[0+2] * a[8] +
                 b[0+3] * a[12];
        c[0+1] = b[0] * a[1] +
                 b[0+1] * a[5] +
                 b[0+2] * a[9] +
                 b[0+3] * a[13];
        c[0+2] = b[0] * a[2] +
                 b[0+1] * a[6] +
                 b[0+2] * a[10] +
                 b[0+3] * a[14];
        c[0+3] = b[0] * a[3] +
                 b[0+1] * a[7] +
                 b[0+2] * a[11] +
                 b[0+3] * a[15];
        c[4] =   b[4] * a[0] +
                 b[4+1] * a[4] +
                 b[4+2] * a[8] +
                 b[4+3] * a[12];
        c[4+1] = b[4] * a[1] +
                 b[4+1] * a[5] +
                 b[4+2] * a[9] +
                 b[4+3] * a[13];
        c[4+2] = b[4] * a[2] +
                 b[4+1] * a[6] +
                 b[4+2] * a[10] +
                 b[4+3] * a[14];
        c[4+3] = b[4] * a[3] +
                 b[4+1] * a[7] +
                 b[4+2] * a[11] +
                 b[4+3] * a[15];
        c[8] =   b[8] * a[0] +
                 b[8+1] * a[4] +
                 b[8+2] * a[8] +
                 b[8+3] * a[12];
        c[8+1] = b[8] * a[1] +
                 b[8+1] * a[5] +
                 b[8+2] * a[9] +
                 b[8+3] * a[13];
        c[8+2] = b[8] * a[2] +
                 b[8+1] * a[6] +
                 b[8+2] * a[10] +
                 b[8+3] * a[14];
        c[8+3] = b[8] * a[3] +
                 b[8+1] * a[7] +
                 b[8+2] * a[11] +
                 b[8+3] * a[15];
        c[12] =   b[12] * a[0] +
                 b[12+1] * a[4] +
                 b[12+2] * a[8] +
                 b[12+3] * a[12];
        c[12+1] = b[12] * a[1] +
                 b[12+1] * a[5] +
                 b[12+2] * a[9] +
                 b[12+3] * a[13];
        c[12+2] = b[12] * a[2] +
                 b[12+1] * a[6] +
                 b[12+2] * a[10] +
                 b[12+3] * a[14];
        c[12+3] = b[12] * a[3] +
                 b[12+1] * a[7] +
                 b[12+2] * a[11] +
                 b[12+3] * a[15];
    return c;
  },

  mulv4 : function (a, v) {
    c = new Array(4);
    for (var i=0; i<4; ++i) {
      var x = 0;
      for (var k=0; k<4; ++k)
        x += v[k] * a[k*4+i];
      c[i] = x;
    }
    return c;
  },

  rotate : function (angle, axis) {
    axis = Vec3.normalize(axis);
    var x=axis[0], y=axis[1], z=axis[2];
    var c = Math.cos(angle);
    var c1 = 1-c;
    var s = Math.sin(angle);
    return [
      x*x*c1+c, y*x*c1+z*s, z*x*c1-y*s, 0,
      x*y*c1-z*s, y*y*c1+c, y*z*c1+x*s, 0,
      x*z*c1+y*s, y*z*c1-x*s, z*z*c1+c, 0,
      0,0,0,1
    ];
  },
  rotateInPlace : function(angle, axis, m) {
    axis = Vec3.normalize(axis);
    var x=axis[0], y=axis[1], z=axis[2];
    var c = Math.cos(angle);
    var c1 = 1-c;
    var s = Math.sin(angle);
    var tmpMatrix = this.tmpMatrix;
    var tmpMatrix2 = this.tmpMatrix2;
    tmpMatrix[0] = x*x*c1+c; tmpMatrix[1] = y*x*c1+z*s; tmpMatrix[2] = z*x*c1-y*s; tmpMatrix[3] = 0;
    tmpMatrix[4] = x*y*c1-z*s; tmpMatrix[5] = y*y*c1+c; tmpMatrix[6] = y*z*c1+x*s; tmpMatrix[7] = 0;
    tmpMatrix[8] = x*z*c1+y*s; tmpMatrix[9] = y*z*c1-x*s; tmpMatrix[10] = z*z*c1+c; tmpMatrix[11] = 0;
    tmpMatrix[12] = 0; tmpMatrix[13] = 0; tmpMatrix[14] = 0; tmpMatrix[15] = 1;
    this.copyMatrix(m, tmpMatrix2);
    return this.mul4x4InPlace(tmpMatrix2, tmpMatrix, m);
  },

  scale : function(v) {
    return [
      v[0], 0, 0, 0,
      0, v[1], 0, 0,
      0, 0, v[2], 0,
      0, 0, 0, 1
    ];
  },
  scale3 : function(x,y,z) {
    return [
      x, 0, 0, 0,
      0, y, 0, 0,
      0, 0, z, 0,
      0, 0, 0, 1
    ];
  },
  scale1 : function(s) {
    return [
      s, 0, 0, 0,
      0, s, 0, 0,
      0, 0, s, 0,
      0, 0, 0, 1
    ];
  },
  scale3InPlace : function(x, y, z, m) {
    var tmpMatrix = this.tmpMatrix;
    var tmpMatrix2 = this.tmpMatrix2;
    tmpMatrix[0] = x; tmpMatrix[1] = 0; tmpMatrix[2] = 0; tmpMatrix[3] = 0;
    tmpMatrix[4] = 0; tmpMatrix[5] = y; tmpMatrix[6] = 0; tmpMatrix[7] = 0;
    tmpMatrix[8] = 0; tmpMatrix[9] = 0; tmpMatrix[10] = z; tmpMatrix[11] = 0;
    tmpMatrix[12] = 0; tmpMatrix[13] = 0; tmpMatrix[14] = 0; tmpMatrix[15] = 1;
    this.copyMatrix(m, tmpMatrix2);
    return this.mul4x4InPlace(tmpMatrix2, tmpMatrix, m);
  },
  scale1InPlace : function(s, m) { return this.scale3InPlace(s, s, s, m); },
  scaleInPlace : function(s, m) { return this.scale3InPlace(s[0],s[1],s[2],m); },

  translate3 : function(x,y,z) {
    return [
      1, 0, 0, 0,
      0, 1, 0, 0,
      0, 0, 1, 0,
      x, y, z, 1
    ];
  },

  translate : function(v) {
    return this.translate3(v[0], v[1], v[2]);
  },
  tmpMatrix : [0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0],
  tmpMatrix2 : [0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0],
  translate3InPlace : function(x,y,z,m) {
    var tmpMatrix = this.tmpMatrix;
    var tmpMatrix2 = this.tmpMatrix2;
    tmpMatrix[0] = 1; tmpMatrix[1] = 0; tmpMatrix[2] = 0; tmpMatrix[3] = 0;
    tmpMatrix[4] = 0; tmpMatrix[5] = 1; tmpMatrix[6] = 0; tmpMatrix[7] = 0;
    tmpMatrix[8] = 0; tmpMatrix[9] = 0; tmpMatrix[10] = 1; tmpMatrix[11] = 0;
    tmpMatrix[12] = x; tmpMatrix[13] = y; tmpMatrix[14] = z; tmpMatrix[15] = 1;
    this.copyMatrix(m, tmpMatrix2);
    return this.mul4x4InPlace(tmpMatrix2, tmpMatrix, m);
  },
  translateInPlace : function(v,m){ return this.translate3InPlace(v[0], v[1], v[2], m); },

  lookAt : function (eye, center, up) {
    var z = Vec3.direction(eye, center);
    var x = Vec3.normalizeInPlace(Vec3.cross(up, z));
    var y = Vec3.normalizeInPlace(Vec3.cross(z, x));

    var m = [
      x[0], y[0], z[0], 0,
      x[1], y[1], z[1], 0,
      x[2], y[2], z[2], 0,
      0, 0, 0, 1
    ];

    var t = [
      1, 0, 0, 0,
      0, 1, 0, 0,
      0, 0, 1, 0,
      -eye[0], -eye[1], -eye[2], 1
    ];

    return this.mul4x4(m,t);
  },

  transpose4x4 : function(m) {
    return [
      m[0], m[4], m[8], m[12],
      m[1], m[5], m[9], m[13],
      m[2], m[6], m[10], m[14],
      m[3], m[7], m[11], m[15]
    ];
  },

  transpose4x4InPlace : function(m) {
    var tmp = 0.0;
    tmp = m[1]; m[1] = m[4]; m[4] = tmp;
    tmp = m[2]; m[2] = m[8]; m[8] = tmp;
    tmp = m[3]; m[3] = m[12]; m[12] = tmp;
    tmp = m[6]; m[6] = m[9]; m[9] = tmp;
    tmp = m[7]; m[7] = m[13]; m[13] = tmp;
    tmp = m[11]; m[11] = m[14]; m[14] = tmp;
    return m;
  },

  transpose3x3 : function(m) {
    return [
      m[0], m[3], m[6],
      m[1], m[4], m[7],
      m[2], m[5], m[8]
    ];
  },

  transpose3x3InPlace : function(m) {
    var tmp = 0.0;
    tmp = m[1]; m[1] = m[3]; m[3] = tmp;
    tmp = m[2]; m[2] = m[6]; m[6] = tmp;
    tmp = m[5]; m[5] = m[7]; m[7] = tmp;
    return m;
  },
}

Vec3 = {
  make : function() { return [0,0,0]; },
  copy : function(v) { return [v[0],v[1],v[2]]; },

  add : function (u,v) {
    return [u[0]+v[0], u[1]+v[1], u[2]+v[2]];
  },

  sub : function (u,v) {
    return [u[0]-v[0], u[1]-v[1], u[2]-v[2]];
  },

  negate : function (u) {
    return [-u[0], -u[1], -u[2]];
  },

  direction : function (u,v) {
    return this.normalizeInPlace(this.sub(u,v));
  },

  normalizeInPlace : function(v) {
    var imag = 1.0 / Math.sqrt(v[0]*v[0] + v[1]*v[1] + v[2]*v[2]);
    v[0] *= imag; v[1] *= imag; v[2] *= imag;
    return v;
  },

  normalize : function(v) {
    return this.normalizeInPlace(this.copy(v));
  },

  scale : function(f, v) {
    return [f*v[0], f*v[1], f*v[2]];
  },

  dot : function(u,v) {
    return u[0]*v[0] + u[1]*v[1] + u[2]*v[2];
  },

  inner : function(u,v) {
    return [u[0]*v[0], u[1]*v[1], u[2]*v[2]];
  },

  cross : function(u,v) {
    return [
      u[1]*v[2] - u[2]*v[1],
      u[2]*v[0] - u[0]*v[2],
      u[0]*v[1] - u[1]*v[0]
    ];
  }
}

Shader = function(gl){
  this.gl = gl;
  this.shaders = [];
  this.uniformLocations = {};
  this.attribLocations = {};
  for (var i=1; i<arguments.length; i++) {
    this.shaders.push(arguments[i]);
  }
}
Shader.prototype = {
  id : null,
  gl : null,
  compiled : false,
  shader : null,
  shaders : [],

  destroy : function() {
    if (this.shader != null) deleteShader(this.gl, this.shader);
  },

  compile : function() {
    this.shader = loadShaderArray(this.gl, this.shaders);
  },

  use : function() {
    if (this.shader == null)
      this.compile();
    this.gl.useProgram(this.shader.program);
  },

  uniform1fv : function(name, value) {
    var loc = this.uniform(name);
    this.gl.uniform1fv(loc, value);
  },

  uniform2fv : function(name, value) {
    var loc = this.uniform(name);
    this.gl.uniform2fv(loc, value);
  },

  uniform3fv : function(name, value) {
    var loc = this.uniform(name);
    this.gl.uniform3fv(loc, value);
  },

  uniform4fv : function(name, value) {
    var loc = this.uniform(name);
    this.gl.uniform4fv(loc, value);
  },

  uniform1f : function(name, value) {
    var loc = this.uniform(name);
    this.gl.uniform1f(loc, value);
  },

  uniform2f : function(name, v1,v2) {
    var loc = this.uniform(name);
    this.gl.uniform2f(loc, v1,v2);
  },

  uniform3f : function(name, v1,v2,v3) {
    var loc = this.uniform(name);
    this.gl.uniform3f(loc, v1,v2,v3);
  },

  uniform4f : function(name, v1,v2,v3,v4) {
    var loc = this.uniform(name);
    this.gl.uniform4f(loc, v1, v2, v3, v4);
  },

  uniform1iv : function(name, value) {
    var loc = this.uniform(name);
    this.gl.uniform1iv(loc, value);
  },

  uniform2iv : function(name, value) {
    var loc = this.uniform(name);
    this.gl.uniform2iv(loc, value);
  },

  uniform3iv : function(name, value) {
    var loc = this.uniform(name);
    this.gl.uniform3iv(loc, value);
  },

  uniform4iv : function(name, value) {
    var loc = this.uniform(name);
    this.gl.uniform4iv(loc, value);
  },

  uniform1i : function(name, value) {
    var loc = this.uniform(name);
    this.gl.uniform1i(loc, value);
  },

  uniform2i : function(name, v1,v2) {
    var loc = this.uniform(name);
    this.gl.uniform2i(loc, v1,v2);
  },

  uniform3i : function(name, v1,v2,v3) {
    var loc = this.uniform(name);
    this.gl.uniform3i(loc, v1,v2,v3);
  },

  uniform4i : function(name, v1,v2,v3,v4) {
    var loc = this.uniform(name);
    this.gl.uniform4i(loc, v1, v2, v3, v4);
  },

  uniformMatrix4fv : function(name, value) {
    var loc = this.uniform(name);
    this.gl.uniformMatrix4fv(loc, false, value);
  },

  uniformMatrix3fv : function(name, value) {
    var loc = this.uniform(name);
    this.gl.uniformMatrix3fv(loc, false, value);
  },

  uniformMatrix2fv : function(name, value) {
    var loc = this.uniform(name);
    this.gl.uniformMatrix2fv(loc, false, value);
  },

  attrib : function(name) {
    if (this.attribLocations[name] == null) {
      var loc = this.gl.getAttribLocation(this.shader.program, name);
      this.attribLocations[name] = loc;
    }
    return this.attribLocations[name];
  },

  uniform : function(name) {
    if (this.uniformLocations[name] == null) {
      var loc = this.gl.getUniformLocation(this.shader.program, name);
      this.uniformLocations[name] = loc;
    }
    return this.uniformLocations[name];
  }
}
Filter = function(gl, shader) {
  Shader.apply(this, arguments);
}
Filter.prototype = new Shader();
Filter.prototype.apply = function(init) {
  this.use();
  var va = this.attrib("Vertex");
  var ta = this.attrib("Tex");
  var vbo = Quad.getCachedVBO(this.gl);
  if (init) init(this);
  vbo.draw(va, null, ta);
}


VBO = function(gl) {
  this.gl = gl;
  this.data = [];
  this.elementsVBO = null;
  for (var i=1; i<arguments.length; i++) {
    if (arguments[i].elements)
      this.elements = arguments[i];
    else
      this.data.push(arguments[i]);
  }
}

VBO.prototype = {
  initialized : false,
  length : 0,
  vbos : null,
  type : 'TRIANGLES',
  elementsVBO : null,
  elements : null,

  setData : function() {
    this.destroy();
    this.data = [];
    for (var i=0; i<arguments.length; i++) {
      if (arguments[i].elements)
        this.elements = arguments[i];
      else
        this.data.push(arguments[i]);
    }
  },

  destroy : function() {
    if (this.vbos != null)
      for (var i=0; i<this.vbos.length; i++)
        this.gl.deleteBuffer(this.vbos[i]);
    if (this.elementsVBO != null)
      this.gl.deleteBuffer(this.elementsVBO);
    this.length = this.elementsLength = 0;
    this.vbos = this.elementsVBO = null;
    this.initialized = false;
  },

  init : function() {
    this.destroy();
    var gl = this.gl;

    gl.getError();
    var vbos = [];
    var length = 0;
    for (var i=0; i<this.data.length; i++)
      vbos.push(gl.createBuffer());
    if (this.elements != null)
      this.elementsVBO = gl.createBuffer();
    try {
      throwError(gl, "genBuffers");
      for (var i = 0; i<this.data.length; i++) {
        var d = this.data[i];
        var dlen = Math.floor(d.data.length / d.size);
        if (i == 0 || dlen < length)
            length = dlen;
        if (!d.floatArray)
          d.floatArray = new Float32Array(d.data);
        gl.bindBuffer(gl.ARRAY_BUFFER, vbos[i]);
        throwError(gl, "bindBuffer");
        gl.bufferData(gl.ARRAY_BUFFER, d.floatArray, gl.STATIC_DRAW);
        throwError(gl, "bufferData");
      }
      if (this.elementsVBO != null) {
        var d = this.elements;
        this.elementsLength = d.data.length;
        this.elementsType = d.type == gl.UNSIGNED_BYTE ? d.type : gl.UNSIGNED_SHORT;
        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.elementsVBO);
        throwError(gl, "bindBuffer ELEMENT_ARRAY_BUFFER");
        if (this.elementsType == gl.UNSIGNED_SHORT && !d.ushortArray) {
          d.ushortArray = new Uint16Array(d.data);
          gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, d.ushortArray, gl.STATIC_DRAW);
        } else if (this.elementsType == gl.UNSIGNED_BYTE && !d.ubyteArray) {
          d.ubyteArray = new Uint8Array(d.data);
          gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, d.ubyteArray, gl.STATIC_DRAW);
        }
        throwError(gl, "bufferData ELEMENT_ARRAY_BUFFER");
      }
    } catch(e) {
      for (var i=0; i<vbos.length; i++)
        gl.deleteBuffer(vbos[i]);
      throw(e);
    }

    gl.bindBuffer(gl.ARRAY_BUFFER, null);
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);

    this.length = length;
    this.vbos = vbos;

    this.initialized = true;
  },

  use : function() {
    if (!this.initialized) this.init();
    var gl = this.gl;
    for (var i=0; i<arguments.length; i++) {
      if (arguments[i] == null) continue;
      gl.bindBuffer(gl.ARRAY_BUFFER, this.vbos[i]);
      gl.vertexAttribPointer(arguments[i], this.data[i].size, gl.FLOAT, false, 0, 0);
      gl.enableVertexAttribArray(arguments[i]);
    }
    if (this.elementsVBO != null) {
      gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.elementsVBO);
    }
  },

  draw : function() {
    var args = [];
    this.use.apply(this, arguments);
    var gl = this.gl;
    if (this.elementsVBO != null) {
      gl.drawElements(gl[this.type], this.elementsLength, this.elementsType, 0);
    } else {
      gl.drawArrays(gl[this.type], 0, this.length);
    }
  }
}

FBO = function(gl, width, height, use_depth) {
  this.gl = gl;
  this.width = width;
  this.height = height;
  if (use_depth != null)
    this.useDepth = use_depth;
}
FBO.prototype = {
  initialized : false,
  useDepth : true,
  fbo : null,
  rbo : null,
  texture : null,

  destroy : function() {
    if (this.fbo) this.gl.deleteFramebuffer(this.fbo);
    if (this.rbo) this.gl.deleteRenderbuffer(this.rbo);
    if (this.texture) this.gl.deleteTexture(this.texture);
  },

  init : function() {
    var gl = this.gl;
    var w = this.width, h = this.height;
    var fbo = this.fbo != null ? this.fbo : gl.createFramebuffer();
    var rb;

    gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
    checkError(gl, "FBO.init bindFramebuffer");
    if (this.useDepth) {
      rb = this.rbo != null ? this.rbo : gl.createRenderbuffer();
      gl.bindRenderbuffer(gl.RENDERBUFFER, rb);
      checkError(gl, "FBO.init bindRenderbuffer");
      gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, w, h);
      checkError(gl, "FBO.init renderbufferStorage");
    }

    var tex = this.texture != null ? this.texture : gl.createTexture();
    gl.bindTexture(gl.TEXTURE_2D, tex);
    try {
      gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, w, h, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
    } catch (e) { // argh, no null texture support
      var tmp = this.getTempCanvas(w,h);
      gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, tmp);
    }
    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_MAG_FILTER, gl.LINEAR);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
    checkError(gl, "FBO.init tex");

    gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0);
    checkError(gl, "FBO.init bind tex");

    if (this.useDepth) {
      gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, rb);
      checkError(gl, "FBO.init bind depth buffer");
    }

    var fbstat = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
    if (fbstat != gl.FRAMEBUFFER_COMPLETE) {
      var glv;
      for (var v in gl) {
        try { glv = gl[v]; } catch (e) { glv = null; }
        if (glv == fbstat) { fbstat = v; break; }}
        log("Framebuffer status: " + fbstat);
    }
    checkError(gl, "FBO.init check fbo");

    this.fbo = fbo;
    this.rbo = rb;
    this.texture = tex;
    this.initialized = true;
  },

  getTempCanvas : function(w, h) {
    if (!FBO.tempCanvas) {
      FBO.tempCanvas = document.createElement('canvas');
    }
    FBO.tempCanvas.width = w;
    FBO.tempCanvas.height = h;
    return FBO.tempCanvas;
  },

  use : function() {
    if (!this.initialized) this.init();
    this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, this.fbo);
  }
}

function GLError(err, msg, fileName, lineNumber) {
  this.message = msg;
  this.glError = err;
}

GLError.prototype = new Error();

function makeGLErrorWrapper(gl, fname) {
  return (function() {
    try {
      var rv = gl[fname].apply(gl, arguments);
      var err = gl.getError();
      if (err != gl.NO_ERROR) {
        throw(new GLError(
            err, "GL error "+getGLErrorAsString(gl, err)+" in "+fname));
      }
      return rv;
    } catch (e) {
      if (e.glError !== undefined) {
        throw e;
      }
      throw(new Error("Threw " + e.name +
                      " in " + fname + "\n" +
                      e.message + "\n" +
                      arguments.callee.caller));
    }
  });
}

function wrapGLContext(gl) {
  var wrap = {};
  for (var i in gl) {
    try {
      if (typeof gl[i] == 'function') {
          wrap[i] = makeGLErrorWrapper(gl, i);
      } else {
          wrap[i] = gl[i];
      }
    } catch (e) {
      // log("wrapGLContext: Error accessing " + i);
    }
  }
  wrap.getError = function(){ return gl.getError(); };
  return wrap;
}

// Assert that f generates a specific GL error.
function assertGLError(gl, err, name, f) {
  if (f == null) { f = name; name = null; }
  var r = false;
  var glErr = 0;
  try { f(); } catch(e) { r=true; glErr = e.glError; }
  if (glErr !== err) {
    if (glErr === undefined) {
      testFailed("assertGLError: UNEXPCETED EXCEPTION", name, f);
    } else {
      testFailed("assertGLError: expected: " + getGLErrorAsString(gl, err) +
                 " actual: " + getGLErrorAsString(gl, glErr), name, f);
    }
    return false;
  }
  return true;
}

// Assert that f generates some GL error. Used in situations where it's
// ambigious which of multiple possible errors will be generated.
function assertSomeGLError(gl, name, f) {
  if (f == null) { f = name; name = null; }
  var r = false;
  var glErr = 0;
  var err = 0;
  try { f(); } catch(e) { r=true; glErr = e.glError; }
  if (glErr === 0) {
    if (glErr === undefined) {
      testFailed("assertGLError: UNEXPCETED EXCEPTION", name, f);
    } else {
      testFailed("assertGLError: expected: " + getGLErrorAsString(gl, err) +
                 " actual: " + getGLErrorAsString(gl, glErr), name, f);
    }
    return false;
  }
  return true;
}

// Assert that f throws an exception but does not generate a GL error.
function assertThrowNoGLError(gl, name, f) {
  if (f == null) { f = name; name = null; }
  var r = false;
  var glErr = undefined;
  var exp;
  try { f(); } catch(e) { r=true; glErr = e.glError; exp = e;}
  if (!r) {
    testFailed(
      "assertThrowNoGError: should have thrown exception", name, f);
    return false;
  } else {
    if (glErr !== undefined) {
      testFailed(
        "assertThrowNoGError: should be no GL error but generated: " +
        getGLErrorAsString(gl, glErr), name, f);
      return false;
    } else {
      // console.log("threw:" + exp);
    }
  }
  return true;
}

Quad = {
  vertices : [
    -1,-1,0,
    1,-1,0,
    -1,1,0,
    1,-1,0,
    1,1,0,
    -1,1,0
  ],
  normals : [
    0,0,-1,
    0,0,-1,
    0,0,-1,
    0,0,-1,
    0,0,-1,
    0,0,-1
  ],
  texcoords : [
    0,0,
    1,0,
    0,1,
    1,0,
    1,1,
    0,1
  ],
  indices : [0,1,2,1,5,2],
  makeVBO : function(gl) {
    return new VBO(gl,
        {size:3, data: Quad.vertices},
        {size:3, data: Quad.normals},
        {size:2, data: Quad.texcoords}
    )
  },
  cache: {},
  getCachedVBO : function(gl) {
    if (!this.cache[gl])
      this.cache[gl] = this.makeVBO(gl);
    return this.cache[gl];
  }
}
Cube = {
  vertices : [  0.5, -0.5,  0.5, // +X
                0.5, -0.5, -0.5,
                0.5,  0.5, -0.5,
                0.5,  0.5,  0.5,

                0.5,  0.5,  0.5, // +Y
                0.5,  0.5, -0.5,
                -0.5,  0.5, -0.5,
                -0.5,  0.5,  0.5,

                0.5,  0.5,  0.5, // +Z
                -0.5,  0.5,  0.5,
                -0.5, -0.5,  0.5,
                0.5, -0.5,  0.5,

                -0.5, -0.5,  0.5, // -X
                -0.5,  0.5,  0.5,
                -0.5,  0.5, -0.5,
                -0.5, -0.5, -0.5,

                -0.5, -0.5,  0.5, // -Y
                -0.5, -0.5, -0.5,
                0.5, -0.5, -0.5,
                0.5, -0.5,  0.5,

                -0.5, -0.5, -0.5, // -Z
                -0.5,  0.5, -0.5,
                0.5,  0.5, -0.5,
                0.5, -0.5, -0.5,
      ],

  normals : [ 1, 0, 0,
              1, 0, 0,
              1, 0, 0,
              1, 0, 0,

              0, 1, 0,
              0, 1, 0,
              0, 1, 0,
              0, 1, 0,

              0, 0, 1,
              0, 0, 1,
              0, 0, 1,
              0, 0, 1,

              -1, 0, 0,
              -1, 0, 0,
              -1, 0, 0,
              -1, 0, 0,

              0,-1, 0,
              0,-1, 0,
              0,-1, 0,
              0,-1, 0,

              0, 0,-1,
              0, 0,-1,
              0, 0,-1,
              0, 0,-1
      ],

  indices : [],
  create : function(){
    for (var i = 0; i < 6; i++) {
      Cube.indices.push(i*4 + 0);
      Cube.indices.push(i*4 + 1);
      Cube.indices.push(i*4 + 3);
      Cube.indices.push(i*4 + 1);
      Cube.indices.push(i*4 + 2);
      Cube.indices.push(i*4 + 3);
    }
  },

  makeVBO : function(gl) {
    return new VBO(gl,
        {size:3, data: Cube.vertices},
        {size:3, data: Cube.normals},
        {elements: true, data: Cube.indices}
    )
  },
  cache : {},
  getCachedVBO : function(gl) {
    if (!this.cache[gl])
      this.cache[gl] = this.makeVBO(gl);
    return this.cache[gl];
  }
}
Cube.create();

Sphere = {
  vertices : [],
  normals : [],
  indices : [],
  create : function(){
    var r = 0.75;
    function vert(theta, phi)
    {
      var r = 0.75;
      var x, y, z, nx, ny, nz;

      nx = Math.sin(theta) * Math.cos(phi);
      ny = Math.sin(phi);
      nz = Math.cos(theta) * Math.cos(phi);
      Sphere.normals.push(nx);
      Sphere.normals.push(ny);
      Sphere.normals.push(nz);

      x = r * Math.sin(theta) * Math.cos(phi);
      y = r * Math.sin(phi);
      z = r * Math.cos(theta) * Math.cos(phi);
      Sphere.vertices.push(x);
      Sphere.vertices.push(y);
      Sphere.vertices.push(z);
    }
    for (var phi = -Math.PI/2; phi < Math.PI/2; phi += Math.PI/20) {
      var phi2 = phi + Math.PI/20;
      for (var theta = -Math.PI/2; theta <= Math.PI/2; theta += Math.PI/20) {
        vert(theta, phi);
        vert(theta, phi2);
      }
    }
  }
}

Sphere.create();

initGL_CONTEXT_ID = function(){
  var c = document.createElement('canvas');
  var contextNames = ['webgl', 'experimental-webgl'];
  GL_CONTEXT_ID = null;
  for (var i=0; i<contextNames.length; i++) {
    try {
      if (c.getContext(contextNames[i])) {
        GL_CONTEXT_ID = contextNames[i];
        break;
      }
    } catch (e) {}
  }
  if (!GL_CONTEXT_ID) {
    log("No WebGL context found. Unable to run tests.");
  }
}

initGL_CONTEXT_ID();
