var textureImages = [];
var canvas, gl;

if (typeof CanvasFloatArray == "undefined" && typeof WebGLFloatArray != "undefined")
	CanvasFloatArray = WebGLFloatArray;
if (typeof CanvaUnsignedShortArray == "undefined" && typeof WebGLUnsignedShortArray != "undefined")
	CanvasUnsignedShortArray = WebGLUnsignedShortArray;

var GLTest = (function() {


var running = false;
var paused = false;


var shaderSources = {
	fragment : {},
	vertex : {}
};

var programs = {};

var modelViewMatrix,
    projMatrix,
    normalMatrix;

var uProjMatrix, uModelViewMatrix, uNormalMatrix;


var shaderProgram;
var shaders = {vertex:[],fragment:[]};

var maxLogLines = 25;
var logLines = [];

var width, height;

function loadShaders(shaderType, names, baseUrl, callback) {

	if (names.length == 0) {
		if (callback) callback();
		return;
	}
		
	var loaders = 0;
	var check = function() {
		if (loaders == 0) {
			if (callback) callback();
			return true;
		}
		return false;
	}
	var ext = (shaderType == gl.VERTEX_SHADER ? "vs" : "fs");
	var shaderTypeName = (shaderType == gl.VERTEX_SHADER ? "vertex" : "fragment");

	loaders = names.length;
	for (var i=0;i<names.length;i++) {
		(function() {
			var name = names[i];
			$.ajax({
				url : "shaders/" + name + "." + ext,
				dataType : "text",
				cache : false,
				success : function(text) {
					log("Loaded " + shaderTypeName + " shader: <a href='shaders/" + name + "." + ext + "'>" + name + "</a>");
					shaderSources[shaderTypeName][name] = text;
					loaders--;
					check();
				},
				error : function() {
					error("Failed loading " + shaderTypeName + " shader: " + name)
					loaders--;
					check();
				}
			});
		})();
	}
}


function loadTextureImages(textures, callback) {
	if (textures.length == 0) {
		if (callback) callback();
		return;
	}
		
	var loaders = 0;
	var check = function() {
		if (loaders == 0) {
			if (callback) callback();
			return true;
		}
		return false;
	}

	for (var a in textures) {
		if (textures.hasOwnProperty(a)) {
			(function() {
				var file = textures[a];
				var name = a;

				loaders++;
				var img = new Image();
				img.onload = function() {

					log("Loaded texture: <a href='textures/" + file + "'>" + name + "</a>");
					textureImages[name] = img;
					loaders--;
					check();
				},
				img.onerror = function() {
					error("Failed loading texture: " + name)
					loaders--;
					check();
				}
				img.src = "textures/" + file;
			})();
		}
	}
}

function initGL() {
	canvas = document.getElementById("output");

	try {
    		gl = canvas.getContext("moz-webgl");
	} catch (e) {}

	if (gl) {
		log("Got moz-webgl context");
	} else {
		try {
    			gl = canvas.getContext("webkit-3d");
		} catch (e) {}
		if (gl) {
			log("Got webkit-3d context");
		}
	}
	if (!gl) {
		error("Could not get 3d context, is WebGL enabled?", "red");
		return false;
	}

	// type-specific getter functions have been joined in a single function
	if (typeof gl.getShaderParameter != "function")
		gl.getShaderParameter = gl.getShaderi;
	if (typeof gl.getShaderi != "function")
		gl.getShaderi = gl.getShaderParameter;
	if (typeof gl.getProgramParameter != "function")
		gl.getProgramParameter = gl.getProgrami
	if (typeof gl.getProgrami != "function")
		gl.getProgrami = gl.getProgramParameter


	return true;
}

function setup() {
	var w = Test.width;
	var h = Test.height;

	log("Setting up " + w + "x" + h + " canvas");

	if (Test.background) {
		canvas.style.backgroundColor = Test.background;
	}

	if (Test.backgroundImage) {
		canvas.style.backgroundImage = "url(textures/" + Test.backgroundImage + ")";
	}

	$("#screen").append(canvas);
	canvas.width = w;
	canvas.height = h;
	canvas.id = "output";


	if (typeof gl.clearDepth != "function") {
		gl.clearDepth = gl.clearDepthf;
	}

	gl.clearDepth(1.0)
	gl.enable(gl.DEPTH_TEST);
	gl.depthFunc(gl.LEQUAL);

	setupShaders();

	$(canvas).click(function(e) {
		var offset = $(canvas).offset();
		var scrollTop = (document.body.scrollTop || document.documentElement.scrollTop);
		var scrollLeft = (document.body.scrollLeft || document.documentElement.scrollLeft);
		var x = e.clientX - offset.left + scrollLeft;
		var y = e.clientY - offset.top + scrollTop;
		if (Test && Test.click) {
			Test.click(gl, x / Test.width, y / Test.height);
		}
	});

	return gl;
}


function createShader(shaderType, name) {
	var shader = gl.createShader(shaderType);

	var source;
	if (shaderType == gl.VERTEX_SHADER)
		source = shaderSources.vertex[name];
	if (shaderType == gl.FRAGMENT_SHADER)
		source = shaderSources.fragment[name];

	gl.shaderSource(shader, source);
	gl.compileShader(shader);

	if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
		error(gl.getShaderInfoLog(shader));
		return null;
	}

	if (shaderType == gl.VERTEX_SHADER)
		shaders.vertex[name] = shader;
	if (shaderType == gl.FRAGMENT_SHADER)
		shaders.fragment[name] = shader;

	return shader;
}


function setupShaders() {

	for (var i in Test.programs) {
		if (Test.programs.hasOwnProperty(i)) {
			var prog = Test.programs[i];
			var program = gl.createProgram();
			for (var a=0;a<prog.vertex.length;a++) {
				gl.attachShader(program, createShader(gl.VERTEX_SHADER, prog.vertex[a]));
			}
			for (var a=0;a<prog.fragment.length;a++) {
				gl.attachShader(program, createShader(gl.FRAGMENT_SHADER, prog.fragment[a]));
			}
			gl.linkProgram(program);
			if (gl.getProgrami(program, gl.LINK_STATUS))
				log("Shader setup succeeded")
			else
				error("Shader setup failed");
		}
		programs[i] = program;
	}
}



function error(str) {
	log(str, "red");
}

function log(str, color) {
	logLines.push((color ? ("<span style='color:" + color + "'>" + str + "</span>")	: str));
	if (maxLogLines > 0 && logLines.length > maxLogLines)
		logLines.shift();
	$("#log").html(logLines.join("<br/>"));

	try {
		console.log(str);
	} catch(e) {}
}


function loadPrograms() {
	if (Test.programs) {
		log("Loading programs...");
		var vs = [], fs = [];
		for (var i in Test.programs) {
			if (Test.programs.hasOwnProperty(i)) {
				var prog = Test.programs[i];
				for (var a=0;a<prog.vertex.length;a++) {
					vs.push(prog.vertex[a]);
				}
				for (var a=0;a<prog.fragment.length;a++) {
					fs.push(prog.fragment[a]);
				}
			}
		}
		loadShaders(gl.VERTEX_SHADER, vs, GLTest.settings.shaderUrl + "/vertex/", function() {
			loadShaders(gl.FRAGMENT_SHADER, fs, GLTest.settings.shaderUrl + "/fragment/", loadTextures);			
		});

	} else {
		log("No programs to load.");
		loadTextures();
	}
}


function loadTextures() {
	if (Test.textures) {
		log("Loading textures...");
		loadTextureImages(Test.textures, runTest)
	} else {
		log("No textures to load.");
		runTest();
	}
}


function runTest() {
	log("Initializing test...");

	setup(Test.width, Test.height);

	gl.programs = programs;

	log("Running test code...");

	log("Press 'P' to pause rendering");

	if (Test.description) {
		log("");
		log(Test.description);
	}

	if (width)
		Test.width = width;
	if (height)
		Test.height = height;

	Test.run(gl, programs);

	var fps;
	var frames = 0;
	var frameTime = 0;

	var lastRenderTime = 0;

	var nextFrame = function() {
		if (!paused) {

			Test.draw(gl, programs);
			gl.flush();
			gl.finish();

			var after = new Date().getTime();
			frameTime += after - lastRenderTime;

			frames++;
		}

		if (GLTest.settings.fpsLimit > 0) {
			var now = new Date().getTime();
			var timeDelta = now - lastRenderTime;
			var renderDelay = 1000 / GLTest.settings.fpsLimit;
			if (timeDelta > renderDelay) {
				renderDelay = Math.max(1, renderDelay - (timeDelta - renderDelay))
			}
			setTimeout(nextFrame, renderDelay);
		} else {
			setTimeout(nextFrame, 1);
		}

		lastRenderTime = new Date().getTime();

	};

	var lastTime = new Date().getTime();
	setInterval(function() {
		if (frames > 0) {
			var fps = frames / frameTime * 1000;

			$("#fps").html("FPS: " + fps.toFixed(2));

			frames = 0;
			frameTime = 0;
		}

	}, 100);

	running = true;

	nextFrame();

	// setup key handlers
	$(document).keydown(function(e) {
		switch (e.keyCode) {
			// p
			case 80:
				paused = !paused;
				if (paused && GLTest.onpause) {
					GLTest.onpause();
				} else if (!paused && GLTest.onunpause) {
					GLTest.onunpause();
				}
				log(paused ? "Pausing" : "Unpausing");
				break;
			default :
				if (Test && Test.keydown) {
					Test.keydown(gl, e.keyCode);
				}
		}
	});

	if (GLTest.onload) {
		GLTest.onload();
	}
}


function frustrum(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 $M([
		[X, 0, A, 0],
		[0, Y, B, 0],
		[0, 0, C, D],
		[0, 0, -1, 0]
	]);
}

function perspective(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 frustrum(xmin, xmax, ymin, ymax, znear, zfar);
}


function setModelView(m, program) {
	var uModelViewMatrix = gl.getUniformLocation(program, "uModelViewMatrix");
	modelViewMatrix = m;
	if (uModelViewMatrix != null) {
		gl.uniformMatrix4fv(uModelViewMatrix, false, new CanvasFloatArray(modelViewMatrix.flatten()));
	}
}

function setProjection(fov, program) {
	var uProjMatrix = gl.getUniformLocation(program, "uProjMatrix");

	projMatrix = perspective(fov, Test.width / Test.height, 0.1, 100.0);
	if (uProjMatrix != null) {
		gl.uniformMatrix4fv(uProjMatrix, false, new CanvasFloatArray(projMatrix.flatten()));
	}
}

function setNormalMatrix(program) {
	var uNormalMatrix = gl.getUniformLocation(program, "uNormalMatrix");
	normalMatrix = modelViewMatrix.inverse().transpose();
	if (uNormalMatrix != null) {
		gl.uniformMatrix4fv(uNormalMatrix, false, new CanvasFloatArray(normalMatrix.flatten()));
	}
}

function createTexture(name) {
	var tex = gl.createTexture();
	gl.enable(gl.TEXTURE_2D);
	gl.bindTexture(gl.TEXTURE_2D, tex);
	gl.texImage2D(gl.TEXTURE_2D, 0, textureImages[name]);
	gl.generateMipmap(gl.TEXTURE_2D);
	gl.bindTexture(gl.TEXTURE_2D, null);
	return tex;
}


function initTest(testname) {


	log("Loading test: <a href='tests/" + testname + ".js'>" + testname + "</a>");

	var $script = $("<script>")
		.attr("type", "text/javascript")
		.attr("src", "tests/" + testname + ".js?" + new Date().getTime())
		.load(function() {
			loadPrograms();
			//loadFragmentShaders();
		})
	;
	$("head")[0].appendChild($script[0]);
}

// shamelessly stolen from WebKit demos
function makeSphere(radius, lats, longs, texture) {
	var geometryData = [ ];
	var normalData = [ ];
	var texCoordData = [ ];
	var indexData = [ ];
    
	for (var latNumber = 0; latNumber <= lats; ++latNumber) {
		for (var longNumber = 0; longNumber <= longs; ++longNumber) {
			var theta = latNumber * Math.PI / lats;
			var phi = longNumber * 2 * Math.PI / longs;
			var sinTheta = Math.sin(theta);
			var sinPhi = Math.sin(phi);
			var cosTheta = Math.cos(theta);
			var cosPhi = Math.cos(phi);
			
			var x = cosPhi * sinTheta;
			var y = cosTheta;
			var z = sinPhi * sinTheta;
			var u = 1-(longNumber/longs);
			var v = latNumber/lats;
			
			normalData.push(x);
			normalData.push(y);
			normalData.push(z);
			texCoordData.push(u);
			texCoordData.push(v);
			geometryData.push(radius * x);
			geometryData.push(radius * y);
			geometryData.push(radius * z);
		}
	}
    
	longs += 1;
	for (var latNumber = 0; latNumber < lats; ++latNumber) {
		for (var longNumber = 0; longNumber < longs; ++longNumber) {
			var first = (latNumber * longs) + (longNumber % longs);
			var second = first + longs;

			if (first+1 < geometryData.length/3 && second+1 < geometryData.length/3) {
				indexData.push(first);
				indexData.push(second);
				indexData.push(first+1);
			
				indexData.push(second);
				indexData.push(second+1);
				indexData.push(first+1);
			}
		}
	}
    
	var retval = { };
	
	retval.normalObject = gl.createBuffer();
	gl.bindBuffer(gl.ARRAY_BUFFER, retval.normalObject);
	gl.bufferData(gl.ARRAY_BUFFER, new CanvasFloatArray(normalData), gl.STATIC_DRAW);
	
	retval.texCoordObject = gl.createBuffer();
	gl.bindBuffer(gl.ARRAY_BUFFER, retval.texCoordObject);
	gl.bufferData(gl.ARRAY_BUFFER, new CanvasFloatArray(texCoordData), gl.STATIC_DRAW);
	
	retval.vertexObject = gl.createBuffer();
	gl.bindBuffer(gl.ARRAY_BUFFER, retval.vertexObject);
	gl.bufferData(gl.ARRAY_BUFFER, new CanvasFloatArray(geometryData), gl.STATIC_DRAW);
	
	retval.numIndices = indexData.length;
	retval.indexObject = gl.createBuffer();
	gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, retval.indexObject);
	gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new CanvasUnsignedShortArray(indexData), gl.STREAM_DRAW);

	if (texture)
		retval.texture = GLTest.createTexture(texture);
	
	return retval;
}

// 360 must be dividable by slices
function makeDisc(radius, slices, texture) {
	var geometryData = [];
	var normalData = [];
	var indexData = [];

	var deg = 360 / slices;

	normalData.push(0,1,0);
	geometryData.push(0,0,0);


	var idx = 1;
	for (var i=0;i<360;i+=deg) {
		var rad = i/180*Math.PI;
		var x = Math.sin(rad) * radius;
		var y = 0;
		var z = Math.cos(rad) * radius;

		geometryData.push(x,y,z);
		normalData.push(0,1,0);

		if (i+deg >= 360)
			indexData.push(0,1,idx)
		else
			indexData.push(0,idx+1,idx)
		idx++;
	}
  
	var retval = {
		radius : radius,
		slices : slices
	};
	
	retval.normalObject = gl.createBuffer();
	gl.bindBuffer(gl.ARRAY_BUFFER, retval.normalObject);
	gl.bufferData(gl.ARRAY_BUFFER, new CanvasFloatArray(normalData), gl.STATIC_DRAW);

	retval.vertexObject = gl.createBuffer();
	gl.bindBuffer(gl.ARRAY_BUFFER, retval.vertexObject);
	gl.bufferData(gl.ARRAY_BUFFER, new CanvasFloatArray(geometryData), gl.STATIC_DRAW);
	
	retval.numIndices = indexData.length;
	retval.indexObject = gl.createBuffer();
	gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, retval.indexObject);
	gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new CanvasUnsignedShortArray(indexData), gl.STREAM_DRAW);

	if (texture)
		retval.texture = GLTest.createTexture(texture);
	
	return retval;
}


function makeCube(size, texture) {

	var vertices = [
		-1,  1,  1,
		 1,  1,  1,
		 1, -1,  1,
		-1, -1,  1,

		-1,  1, -1,
		 1,  1, -1,
		 1, -1, -1,
		-1, -1, -1,

		 1,  1,  1,
		 1,  1, -1,
		 1, -1, -1,
		 1, -1,  1,

		-1,  1,  1,
		-1,  1, -1,
		 1,  1, -1,
		 1,  1,  1,

		 1, -1,  1,
		 1, -1, -1,
		-1, -1, -1,
		-1, -1,  1,

		-1, -1,  1,
		-1, -1, -1,
		-1,  1, -1,
		-1,  1,  1
	];

	var normals = [
		 0,  0,  1,
		 0,  0,  1,
		 0,  0,  1,
		 0,  0,  1,

		 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, -1,  0,
		 0, -1,  0,
		 0, -1,  0,
		 0, -1,  0,

		-1,  0,  0,
		-1,  0,  0,
		-1,  0,  0,
		-1,  0,  0
	];

	var indices = [
		 1,  0,  3,
		 1,  3,  2,

		 5,  4,  7,
		 5,  7,  6,

		 9,  8, 11,
		 9, 11, 10,

		13, 12, 15,
		13, 15, 14,

		17, 16, 19,
		17, 19, 18,

		21, 20, 23,
		21, 23, 22
	];

	var texcoords = [
		0, 1, 
		0, 0,
		1, 0, 
		1, 1,   

		0, 1,  
		0, 0, 
		1, 0, 
		1, 1, 

		0, 1, 
		0, 0,  
		1, 0, 
		1, 1, 

		0, 1, 
		0, 0, 
		1, 0, 
		1, 1, 

		0, 1,  
		0, 0, 
		1, 0, 
		1, 1, 

		0, 1, 
		0, 0,  
		1, 0, 
		1, 1
	];


	var retval = {};

	retval.numIndices = indices.length;
	retval.indexObject = gl.createBuffer();
	gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, retval.indexObject);
	gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new CanvasUnsignedShortArray(indices), gl.STREAM_DRAW);

	retval.normalObject = gl.createBuffer();
	gl.bindBuffer(gl.ARRAY_BUFFER, retval.normalObject);
	gl.bufferData(gl.ARRAY_BUFFER, new CanvasFloatArray(normals), gl.STATIC_DRAW);

	retval.texCoordObject = gl.createBuffer();
	gl.bindBuffer(gl.ARRAY_BUFFER, retval.texCoordObject);
	gl.bufferData(gl.ARRAY_BUFFER, new CanvasFloatArray(texcoords), gl.STATIC_DRAW);
	
	retval.vertexObject = gl.createBuffer();
	gl.bindBuffer(gl.ARRAY_BUFFER, retval.vertexObject);
	gl.bufferData(gl.ARRAY_BUFFER, new CanvasFloatArray(vertices), gl.STATIC_DRAW);
	

	if (texture)
		retval.texture = GLTest.createTexture(texture);

	return retval;
}

return {
	initGL : initGL,
	initTest : initTest,
	log : log,

	setModelView : setModelView,
	setProjection : setProjection,
	setNormalMatrix : setNormalMatrix,

	createTexture : createTexture,

	makeSphere : makeSphere,
	makeCube : makeCube,
	makeDisc : makeDisc,

	setDimensions : function(w, h) {
		if (typeof Test != "undefined") {
			Test.width = w;
			Test.height = h;
		}
		if (canvas) {
			canvas.width = w;
			canvas.height = h;
		}
		width = w;
		height = h;
	},

	settings : {
		showFps : false,
		fpsLimit : 30,
		shaderUrl : "shaders",
		textureUrl : "textures"
	}
}


})();


//--------------------------------------------------------------------------
//
// augment Sylvester some
// (c) 2009 Vladimir Vukicevic
//
//--------------------------------------------------------------------------
Matrix.Translation = function (v)
{
  if (v.elements.length == 2) {
    var r = Matrix.I(3);
    r.elements[2][0] = v.elements[0];
    r.elements[2][1] = v.elements[1];
    return r;
  }

  if (v.elements.length == 3) {
    var r = Matrix.I(4);
    r.elements[0][3] = v.elements[0];
    r.elements[1][3] = v.elements[1];
    r.elements[2][3] = v.elements[2];
    return r;
  }

  throw "Invalid length for Translation";
}

Matrix.prototype.flatten = function ()
{
    var result = [];
    if (this.elements.length === 0) {
        return [];
    }


    for (var j = 0; j < this.elements[0].length; j++) {
        for (var i = 0; i < this.elements.length; i++) {
            result.push(this.elements[i][j]);
        }
    }
    return result;
}

Matrix.prototype.ensure4x4 = function()
{
    if (this.elements.length == 4 && 
        this.elements[0].length == 4) {
        return this;
    }

    if (this.elements.length > 4 ||
        this.elements[0].length > 4) {
        return null;
    }

    for (var i = 0; i < this.elements.length; i++) {
        for (var j = this.elements[i].length; j < 4; j++) {
            if (i == j) {
                this.elements[i].push(1);
            }else {
                this.elements[i].push(0);
            }
        }
    }

    for (var i = this.elements.length; i < 4; i++) {
        if (i === 0) {
            this.elements.push([1, 0, 0, 0]);
        }else if (i == 1) {
            this.elements.push([0, 1, 0, 0]);
        }else if (i == 2) {
            this.elements.push([0, 0, 1, 0]);
        }else if (i == 3) {
            this.elements.push([0, 0, 0, 1]);
        }
    }

    return this;
};

Matrix.prototype.make3x3 = function()
{
    if (this.elements.length != 4 ||
        this.elements[0].length != 4) {
        return null;
    }

    return Matrix.create([[this.elements[0][0], this.elements[0][1], this.elements[0][2]],
                          [this.elements[1][0], this.elements[1][1], this.elements[1][2]],
                          [this.elements[2][0], this.elements[2][1], this.elements[2][2]]]);
};

Vector.prototype.flatten = function ()
{
    return this.elements;
};