/*
** Copyright (c) 2012 The Khronos Group Inc.
**
** Permission is hereby granted, free of charge, to any person obtaining a
** copy of this software and/or associated documentation files (the
** "Materials"), to deal in the Materials without restriction, including
** without limitation the rights to use, copy, modify, merge, publish,
** distribute, sublicense, and/or sell copies of the Materials, and to
** permit persons to whom the Materials are 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 Materials.
**
** THE MATERIALS ARE 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
** MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS.
*/
OpenGLESTestRunner = (function(){
var wtu = WebGLTestUtils;
var gl;

var HALF_GRID_MAX_SIZE = 32;
var KNOWN_ATTRIBS = [
  "gtf_Vertex",
  "gtf_Color"
];

var GTFPIXELTOLERANCE = 24;
var GTFACCEPTABLEFAILURECONT = 10;
var GTFAMDPIXELTOLERANCE = 12;
var GTFSCORETOLERANCE = 0.65;
var GTFNCCTOLARANCEZERO = 0.25;
var GTFKERNALSIZE = 5;

function log(msg) {
  // debug(msg);
}

function compareImages(refData, tstData, width, height, diff) {
  function isPixelSame(offset) {
    // First do simple check
    if (Math.abs(refData[offset + 0] - tstData[offset + 0]) <= GTFPIXELTOLERANCE &&
        Math.abs(refData[offset + 1] - tstData[offset + 1]) <= GTFPIXELTOLERANCE &&
        Math.abs(refData[offset + 2] - tstData[offset + 2]) <= GTFPIXELTOLERANCE) {
      return true;
    }

    // TODO: Implement crazy check that's used in OpenGL ES 2.0 conformance tests.
    // NOTE: on Desktop things seem to be working. Maybe the more complex check
    // is needed for embedded systems?
    return false;
  }

  var same = true;
  for (var yy = 0; yy < height; ++yy) {
    for (var xx = 0; xx < width; ++xx) {
      var offset = (yy * width + xx) * 4;
      var diffOffset = ((height - yy - 1) * width + xx) * 4;
      diff[diffOffset + 0] = 0;
      diff[diffOffset + 1] = 0;
      diff[diffOffset + 2] = 0;
      diff[diffOffset + 3] = 255;
      if (!isPixelSame(offset)) {
        diff[diffOffset] = 255;
        if (same) {
          same = false;
          testFailed("pixel @ (" + xx + ", " + yy + " was [" +
                     tstData[offset + 0] + "," +
                     tstData[offset + 1] + "," +
                     tstData[offset + 2] + "," +
                     tstData[offset + 3] + "] expected [" +
                     refData[offset + 0] + "," +
                     refData[offset + 1] + "," +
                     refData[offset + 2] + "," +
                     refData[offset + 3] + "]")
        }
      }
    }
  }
  return same;
}

function persp(fovy, aspect, n, f) {
  var dz = f - n;
  var rad = fovy / 2.0 * 3.14159265 / 180;

  var s = Math.sin(rad);
  if (dz == 0 || s == 0 || aspect == 0)
    return;

  var cot = Math.cos(rad) / s;

  return [
    cot / aspect,
    0.0,
    0.0,
    0.0,

    0.0,
    cot,
    0.0,
    0.0,

    0.0,
    0.0,
    -(f + n) / dz,
    -1.0,

    0.0,
    0.0,
    -2.0 * f * n / dz,
    0.0
  ];
}

function setAttribs(attribs, buffers) {
  for (var name in attribs) {
    var buffer = buffers[name];
    if (!buffer) {
      testFailed("no buffer for attrib:" + name);
      continue;
    }
    var loc = attribs[name];
    log("setup attrib: " + loc + " as " + name);
    var buf = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, buf);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(buffer.data), gl.STATIC_DRAW);
    gl.enableVertexAttribArray(loc);
    gl.vertexAttribPointer(loc, buffer.numComponents, gl.FLOAT, false, 0, 0);
  }
}

function drawSquare(attribs) {
  var buffers = {
    "gtf_Vertex": {
      data: [
        1.0, -1.0, -2.0,
        1.0, 1.0, -2.0,
        -1.0, -1.0, -2.0,
        -1.0, 1.0, -2.0
      ],
      numComponents: 3
    },
    "gtf_Color": {
      data: [
        0.5, 1.0, 0.0,
        0.0, 1.0, 1.0,
        1.0, 0.0, 0.0,
        0.5, 0.0, 1.0
      ],
      numComponents: 3,
    },
    "gtf_SecondaryColor": {
      data: [
        0.5, 0.0, 1.0,
        1.0, 0.0, 0.0,
        0.0, 1.0, 1.0,
        0.5, 1.0, 0.0
      ],
      numComponents: 3,
    },
    "gtf_Normal": {
      data: [
        0.5, 0.0, 1.0,
        1.0, 0.0, 0.0,
        0.0, 1.0, 1.0,
        0.5, 1.0, 0.0
      ],
      numComponents: 3,
    },
    "gtf_MultiTexCoord0": {
      data: [
        1.0, 0.0,
        1.0, 1.0,
        0.0, 0.0,
        0.0, 1.0
      ],
      numComponents: 2,
    },
    "gtf_FogCoord": {
      data: [
        0.0,
        1.0,
        0.0,
        1.0
      ],
      numComponents: 1,
    }
  };
  setAttribs(attribs, buffers);
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
}

function drawFrontBackSquare(attribs) {
  var front = {
    "gtf_Vertex": {
      data: [
        1.0, -1.0, -2.0,
        1.0, 0.0, -2.0,
        -1.0, -1.0, -2.0,
        -1.0, 0.0, -2.0
      ],
      numComponents: 3
    },
    "gtf_Color": {
      data: [
        0.0, 1.0, 0.0,
        0.0, 1.0, 0.0,
        0.0, 1.0, 0.0,
        0.0, 1.0, 0.0
      ],
      numComponents: 3,
    },
    "gtf_MultiTexCoord0": {
      data: [
        1.0, 0.0,
        1.0, 0.5,
        0.0, 0.0,
        0.0, 0.5
      ],
      numComponents: 2,
    }
  };
  setAttribs(attribs, front);
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);

  var back = {
    "gtf_Vertex": {
      data: [
        1.0, 1.0, -2.0,
        1.0, 0.0, -2.0,
        -1.0, 1.0, -2.0,
        -1.0, 0.0, -2.0
      ],
      numComponents: 3
    },
    "gtf_Color": {
      data: [
        1.0, 0.0, 0.0,
        1.0, 0.0, 0.0,
        1.0, 0.0, 0.0,
        1.0, 0.0, 0.0
      ],
      numComponents: 3,
    },
    "gtf_MultiTexCoord0": {
      data: [
        1.0, 0.1,
        1.0, 0.5,
        0.0, 0.1,
        0.0, 0.5
      ],
      numComponents: 2,
    }
  };
  setAttribs(attribs, back);
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
}

function drawGrid(attribs, width, height) {
  var n = Math.min(Math.floor(Math.max(width, height) / 4), HALF_GRID_MAX_SIZE);

  var numVertices = (n + n) * (n + n) * 6;

  var gridVertices   = [];
  var gridColors     = [];
  var gridSecColors  = [];
  var gridNormals    = [];
  var gridFogCoords  = [];
  var gridTexCoords0 = [];

  var currentVertex    = 0;
  var currentColor     = 0;
  var currentSecColor  = 0;
  var currentTexCoord0 = 0;
  var currentNormal    = 0;
  var currentFogCoord  = 0;

  var z = -2.0;
  for(var i = -n; i < n; ++i)
  {
      var x1 = i / n;
      var x2 = (i + 1) / n;
      for(var j = -n; j < n; ++j)
      {
          var y1 = j / n;
          var y2 = (j + 1) / n;

          // VERTEX 0
          gridVertices[currentVertex++] = x1;
          gridVertices[currentVertex++] = y1;
          gridVertices[currentVertex++] = z;
          gridColors[currentColor++] = 1.0 - (x1 + y1 + 2.0) / 4.0;
          gridColors[currentColor++] = (x1 + 1.0) / 2.0;
          gridColors[currentColor++] = (y1 + 1.0) / 2.0;
          gridSecColors[currentSecColor++] = 1.0 - (x2 + y2 + 2.0) / 4.0;
          gridSecColors[currentSecColor++] = (x2 + 1.0) / 2.0;
          gridSecColors[currentSecColor++] = (y2 + 1.0) / 2.0;
          gridTexCoords0[currentTexCoord0++] = (x1 + 1.0) / 2.0;
          gridTexCoords0[currentTexCoord0++] = (y1 + 1.0) / 2.0;
          gridNormals[currentNormal++] = 1.0 - (x2 + y2 + 2.0) / 4.0;
          gridNormals[currentNormal++] = (x2 + 1.0) / 2.0;
          gridNormals[currentNormal++] = (y2 + 1.0) / 2.0;
          gridFogCoords[currentFogCoord++] = (y1 + 1.0) / 2.0;

          // VERTEX 1
          gridVertices[currentVertex++] = x2;
          gridVertices[currentVertex++] = y1;
          gridVertices[currentVertex++] = z;
          gridColors[currentColor++] = 1.0 - (x2 + y1 + 2.0) / 4.0;
          gridColors[currentColor++] = (x2 + 1.0) / 2.0;
          gridColors[currentColor++] = (y1 + 1.0) / 2.0;
          gridSecColors[currentSecColor++] = 1.0 - (x1 + y2 + 2.0) / 4.0;
          gridSecColors[currentSecColor++] = (x1 + 1.0) / 2.0;
          gridSecColors[currentSecColor++] = (y2 + 1.0) / 2.0;
          gridTexCoords0[currentTexCoord0++] = (x2 + 1.0) / 2.0;
          gridTexCoords0[currentTexCoord0++] = (y1 + 1.0) / 2.0;
          gridNormals[currentNormal++] = 1.0 - (x1 + y2 + 2.0) / 4.0;
          gridNormals[currentNormal++] = (x1 + 1.0) / 2.0;
          gridNormals[currentNormal++] = (y2 + 1.0) / 2.0;
          gridFogCoords[currentFogCoord++] = (y1 + 1.0) / 2.0;

          // VERTEX 2
          gridVertices[currentVertex++] = x2;
          gridVertices[currentVertex++] = y2;
          gridVertices[currentVertex++] = z;
          gridColors[currentColor++] = 1.0 - (x2 + y2 + 2.0) / 4.0;
          gridColors[currentColor++] = (x2 + 1.0) / 2.0;
          gridColors[currentColor++] = (y2 + 1.0) / 2.0;
          gridSecColors[currentSecColor++] = 1.0 - (x1 + y1 + 2.0) / 4.0;
          gridSecColors[currentSecColor++] = (x1 + 1.0) / 2.0;
          gridSecColors[currentSecColor++] = (y1 + 1.0) / 2.0;
          gridTexCoords0[currentTexCoord0++] = (x2 + 1.0) / 2.0;
          gridTexCoords0[currentTexCoord0++] = (y2 + 1.0) / 2.0;
          gridNormals[currentNormal++] = 1.0 - (x1 + y1 + 2.0) / 4.0;
          gridNormals[currentNormal++] = (x1 + 1.0) / 2.0;
          gridNormals[currentNormal++] = (y1 + 1.0) / 2.0;
          gridFogCoords[currentFogCoord++] = (y2 + 1.0) / 2.0;

          // VERTEX 2
          gridVertices[currentVertex++] = x2;
          gridVertices[currentVertex++] = y2;
          gridVertices[currentVertex++] = z;
          gridColors[currentColor++] = 1.0 - (x2 + y2 + 2.0) / 4.0;
          gridColors[currentColor++] = (x2 + 1.0) / 2.0;
          gridColors[currentColor++] = (y2 + 1.0) / 2.0;
          gridSecColors[currentSecColor++] = 1.0 - (x1 + y1 + 2.0) / 4.0;
          gridSecColors[currentSecColor++] = (x1 + 1.0) / 2.0;
          gridSecColors[currentSecColor++] = (y1 + 1.0) / 2.0;
          gridTexCoords0[currentTexCoord0++] = (x2 + 1.0) / 2.0;
          gridTexCoords0[currentTexCoord0++] = (y2 + 1.0) / 2.0;
          gridNormals[currentNormal++] = 1.0 - (x1 + y1 + 2.0) / 4.0;
          gridNormals[currentNormal++] = (x1 + 1.0) / 2.0;
          gridNormals[currentNormal++] = (y1 + 1.0) / 2.0;
          gridFogCoords[currentFogCoord++] = (y2 + 1.0) / 2.0;

          // VERTEX 3
          gridVertices[currentVertex++] = x1;
          gridVertices[currentVertex++] = y2;
          gridVertices[currentVertex++] = z;
          gridColors[currentColor++] = 1.0 - (x1 + y2 + 2.0) / 4.0;
          gridColors[currentColor++] = (x1 + 1.0) / 2.0;
          gridColors[currentColor++] = (y2 + 1.0) / 2.0;
          gridSecColors[currentSecColor++] = 1.0 - (x2 + y1 + 2.0) / 4.0;
          gridSecColors[currentSecColor++] = (x2 + 1.0) / 2.0;
          gridSecColors[currentSecColor++] = (y1 + 1.0) / 2.0;
          gridTexCoords0[currentTexCoord0++] = (x1 + 1.0) / 2.0;
          gridTexCoords0[currentTexCoord0++] = (y2 + 1.0) / 2.0;
          gridNormals[currentNormal++] = 1.0 - (x2 + y1 + 2.0) / 4.0;
          gridNormals[currentNormal++] = (x2 + 1.0) / 2.0;
          gridNormals[currentNormal++] = (y1 + 1.0) / 2.0;
          gridFogCoords[currentFogCoord++] = (y2 + 1.0) / 2.0;

          // VERTEX 0
          gridVertices[currentVertex++] = x1;
          gridVertices[currentVertex++] = y1;
          gridVertices[currentVertex++] = z;
          gridColors[currentColor++] = 1.0 - (x1 + y1 + 2.0) / 4.0;
          gridColors[currentColor++] = (x1 + 1.0) / 2.0;
          gridColors[currentColor++] = (y1 + 1.0) / 2.0;
          gridSecColors[currentSecColor++] = 1.0 - (x2 + y2 + 2.0) / 4.0;
          gridSecColors[currentSecColor++] = (x2 + 1.0) / 2.0;
          gridSecColors[currentSecColor++] = (y2 + 1.0) / 2.0;
          gridTexCoords0[currentTexCoord0++] = (x1 + 1.0) / 2.0;
          gridTexCoords0[currentTexCoord0++] = (y1 + 1.0) / 2.0;
          gridNormals[currentNormal++] = 1.0 - (x2 + y2 + 2.0) / 4.0;
          gridNormals[currentNormal++] = (x2 + 1.0) / 2.0;
          gridNormals[currentNormal++] = (y2 + 1.0) / 2.0;
          gridFogCoords[currentFogCoord++] = (y1 + 1.0) / 2.0;
      }
  }

  var buffers = {
    "gtf_Vertex":         { data: gridVertices,   numComponents: 3 },
    "gtf_Color":          { data: gridColors,     numComponents: 3 },
    "gtf_SecondaryColor": { data: gridSecColors,  numComponents: 3 },
    "gtf_Normal":         { data: gridNormals,    numComponents: 3 },
    "gtf_FogCoord":       { data: gridFogCoords,  numComponents: 1 },
    "gtf_MultiTexCoord0": { data: gridTexCoords0, numComponents: 2 }
  };
  setAttribs(attribs, buffers);
  gl.drawArrays(gl.TRIANGLES, 0, numVertices);
}

var MODEL_FUNCS = {
  square:          drawSquare,
  frontbacksquare: drawFrontBackSquare,
  grid:            drawGrid
};

function drawWithProgram(program, programInfo, test) {
  gl.useProgram(program);
  var attribs = { };

  var numAttribs = gl.getProgramParameter(program, gl.ACTIVE_ATTRIBUTES);
  for (var ii = 0; ii < numAttribs; ++ii) {
    var info = gl.getActiveAttrib(program, ii);
    var name = info.name;
    var location = gl.getAttribLocation(program, name);
    attribs[name] = location;

    if (KNOWN_ATTRIBS.indexOf(name) < 0) {
      testFailed("unknown attrib:" + name)
    }
  }

  var uniforms = { };
  var numUniforms = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS);
  for (var ii = 0; ii < numUniforms; ++ii) {
    var info = gl.getActiveUniform(program, ii);
    var name = info.name;
    if (name.match(/\[0\]$/)) {
      name = name.substr(0, name.length - 3);
    }
    var location = gl.getUniformLocation(program, name);
    uniforms[name] = {location: location};
  }

  var getUniformLocation = function(name) {
    var uniform = uniforms[name];
    if (uniform) {
      uniform.used = true;
      return uniform.location;
    }
    return null;
  }

  // Set known uniforms
  var loc = getUniformLocation("gtf_ModelViewProjectionMatrix");
  if (loc)  {
    gl.uniformMatrix4fv(
      loc,
      false,
      persp(60, 1, 1, 30));
  }
  var loc = getUniformLocation("viewportwidth");
  if (loc) {
    gl.uniform1f(loc, gl.canvas.width);
  }
  var loc = getUniformLocation("viewportheight");
  if (loc) {
    gl.uniform1f(loc, gl.canvas.height);
  }

  // Set test specific uniforms
  for (var name in programInfo.uniforms) {
    var location = getUniformLocation(name);
    if (!location) {
      continue;
    }
    var uniform = programInfo.uniforms[name];
    var type = uniform.type;
    var value = uniform.value;
    var transpose = uniform.transpose;
    if (transpose !== undefined) {
      log("gl." + type + '("' + name + '", ' + transpose + ", " + value + ")");
      gl[type](location, transpose, value);
    } else if (!type.match("v$")) {
      var args = [location];
      for (var ii = 0; ii < value.length; ++ii) {
        args.push(value[ii]);
      }
      gl[type].apply(gl, args);
      log("gl." + type + '("' + name + '", ' + args.slice(1) + ")");
    } else {
      log("gl." + type + '("' + name + '", ' + value + ")");
      gl[type](location, value);
    }
    var err = gl.getError();
    if (err != gl.NO_ERROR) {
      testFailed(wtu.glEnumToString(gl, err) + " generated setting uniform: " + name);
    }
  }

  // Filter out specified built-in uniforms
  if (programInfo.builtin_uniforms) {
    var num_builtins_found = 0;
    var valid_values = programInfo.builtin_uniforms.valid_values;
    for (var index in valid_values) {
      var uniform = uniforms[valid_values[index]];
      if (uniform) {
        ++num_builtins_found;
        uniform.builtin = true;
      }
    }

    var min_required = programInfo.builtin_uniforms.min_required;
    if (num_builtins_found < min_required) {
      testFailed("only found " + num_builtins_found + " of " + min_required +
                 " required built-in uniforms: " + valid_values);
    }
  }

  // Check for unset uniforms
  for (var name in uniforms) {
    var uniform = uniforms[name];
    if (!uniform.used && !uniform.builtin) {
      testFailed("uniform " + name + " never set");
    }
  }


  for (var state in test.state) {
    var fields = test.state[state];
    switch (state) {
    case 'depthrange':
      gl.depthRange(fields.near, fields.far);
      break;
    default:
      testFailed("unknown state: " + state)
    }
  }

  gl.clearColor(0, 0, 0, 0);
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

  var model = test.model || "square";
  var fn = MODEL_FUNCS[model];
  if (!fn) {
    testFailed("unknown model type: " + model)
  } else {
    log("draw as: " + model)
    fn(attribs, gl.canvas.width, gl.canvas.height);
  }

  var pixels = new Uint8Array(gl.canvas.width * gl.canvas.height * 4);
  gl.readPixels(0, 0, gl.canvas.width, gl.canvas.height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
  return {
    width: gl.canvas.width,
    height: gl.canvas.height,
    pixels: pixels,
    img: wtu.makeImageFromCanvas(gl.canvas)
  };
}

function runProgram(programInfo, test, label, callback) {
  var shaders = [];
  var source = [];
  var count = 0;

  function loadShader(path, type, index) {
    wtu.loadTextFileAsync(path, function(success, text) {
      addShader(success, text, type, path, index);
    });
  }

  function addShader(success, text, type, path, index) {
    ++count;
    if (!success) {
      testFailed("could not load: " + path);
    } else {
      var shader = wtu.loadShader(gl, text, type);
      shaders.push(shader);
      source[index] = text;
    }
    if (count == 2) {
      var result;
      if (shaders.length == 2) {
        debug("");
        if (!quietMode()) {
        var consoleDiv = document.getElementById("console");
          wtu.addShaderSources(
              gl, consoleDiv, label + " vertex shader", shaders[0], source[0],
              programInfo.vertexShader);
          wtu.addShaderSources(
              gl, consoleDiv, label + " fragment shader", shaders[1], source[1],
              programInfo.fragmentShader);
        }
        var program = wtu.createProgram(gl, shaders[0], shaders[1]);
        result = drawWithProgram(program, programInfo, test);
      }
      callback(result);
    }
  }

  loadShader(programInfo.vertexShader, gl.VERTEX_SHADER, 0);
  loadShader(programInfo.fragmentShader, gl.FRAGMENT_SHADER, 1);
}

function compareResults(expected, actual) {
  var width = expected.width;
  var height = expected.height;
  var canvas = document.createElement("canvas");
  canvas.width = width;
  canvas.height = height;
  var ctx = canvas.getContext("2d");
  var imgData = ctx.getImageData(0, 0, width, height);
  var tolerance = 0;

  var expData = expected.pixels;
  var actData = actual.pixels;

  var same = compareImages(expData, actData, width, height, imgData.data);

  var console = document.getElementById("console");
  var diffImg = null;
  if (!same) {
    ctx.putImageData(imgData, 0, 0);
    diffImg = wtu.makeImageFromCanvas(canvas);
  }

  if (!quietMode()) {
    var div = document.createElement("div");
    div.className = "testimages";
    wtu.insertImage(div, "reference", expected.img);
    wtu.insertImage(div, "test", actual.img);
    if (diffImg) {
      wtu.insertImage(div, "diff", diffImg);
    }
    div.appendChild(document.createElement('br'));

    console.appendChild(div);
  }

  if (!same) {
    testFailed("images are different");
  } else {
    testPassed("images are the same");
  }

  if (!quietMode())
    console.appendChild(document.createElement('hr'));
}

function runCompareTest(test, callback) {
  debug("");
  debug("test: " + test.name);
  var results = [];
  var count = 0;

  function storeResults(index) {
    return function(result) {
      results[index] = result;
      ++count;
      if (count == 2) {
        compareResults(results[0], results[1]);
        wtu.glErrorShouldBe(gl, gl.NO_ERROR, "there should be no errors");
        callback();
      }
    }
  }

  runProgram(test.referenceProgram, test, "reference", storeResults(0));
  runProgram(test.testProgram, test, "test", storeResults(1));
}

function runBuildTest(test, callback) {
  debug("");
  debug("test: " + test.name);

  var shaders = [null, null];
  var source = ["",""];
  var success = [undefined, undefined];
  var count = 0;

  function loadShader(path, type, index) {
    if (path == "empty") {
      shaders[index] = gl.createShader();
      success[index] = true;
      source[index] = "/* empty */";
      attachAndLink();
    } else {
      wtu.loadTextFileAsync(path, function(loadSuccess, text) {
        if (!loadSuccess) {
          success[index] = false;
          source[index] = "/* could not load */";
          testFailed("could not load:" + path);
        } else {
          source[index] = text;
          shaders[index] = wtu.loadShader(gl, text, type, function(index) {
            return function(msg) {
              success[index] = false
            }
          }(index));
          if (success[index] === undefined) {
            success[index] = true;
          }
        }
        attachAndLink();
      });
    }
  }

  function attachAndLink() {
    ++count;
    if (count == 2) {
      if (!quietMode()) {
        debug("");
        var c = document.getElementById("console");
        wtu.addShaderSource(
            c, "vertex shader", source[0], test.testProgram.vertexShader);
        debug("compile: " + (success[0] ? "success" : "fail"));
        wtu.addShaderSource(
            c, "fragment shader", source[1], test.testProgram.fragmentShader);
        debug("compile: " + (success[1] ? "success" : "fail"));
      }
      compileSuccess = (success[0] && success[1]);
      if (!test.compstat) {
        if (compileSuccess) {
          testFailed("expected compile failure but was successful");
        } else {
          testPassed("expected compile failure and it failed");
        }
      } else {
        if (compileSuccess) {
          testPassed("expected compile success and it was successful");
        } else {
          testFailed("expected compile success but it failed");
        }
        var linkSuccess = true;
        var program = wtu.createProgram(gl, shaders[0], shaders[1], function() {
          linkSuccess = false;
        });
        if (linkSuccess !== test.linkstat) {
          testFailed("expected link to " + (test.linkstat ? "succeed" : "fail"));
        } else {
          testPassed("shaders compiled and linked as expected.");
        }
      }
      callback();
    }
  }

  loadShader(test.testProgram.vertexShader, gl.VERTEX_SHADER, 0);
  loadShader(test.testProgram.fragmentShader, gl.FRAGMENT_SHADER, 1);
}

var testPatterns = {
  compare: runCompareTest,
  build: runBuildTest,

  dummy: null  // just here to mark the end
};

function LogGLCall(functionName, args) {
  console.log("gl." + functionName + "(" +
            WebGLDebugUtils.glFunctionArgsToString(functionName, args) + ")");
}

// Runs the tests async since they will load shaders.
function run(obj) {
  description();

  var canvas = document.getElementById("example");
  gl = wtu.create3DContext(canvas);
  if (window.WebGLDebugUtils) {
    gl = WebGLDebugUtils.makeDebugContext(gl, undefined, LogGLCall);
  }
  if (!gl) {
    testFailed("context does not exist");
    finishTest();
    return;
  }

  if (gl.canvas.width != 500 || gl.canvas.height != 500) {
    testFailed("canvas must be 500x500 pixels: Several shaders are hard coded to this size.");
  }

  var tests = obj.tests;
  var ndx = 0;

  function runNextTest() {
    if (ndx < tests.length) {
      var test = tests[ndx++];
      var fn = testPatterns[test.pattern];
      if (!fn) {
        testFailed("test pattern: " + test.pattern + " not supoprted")
        runNextTest();
      } else {
        fn(test, runNextTest);
      }
    } else {
      finishTest();
    }
  }
  runNextTest();
}

return {
  run: run,
};
}());