"use strict";
///////////////////////////////////////////////
//
//  Copyright 2014 David Gross
//  Not for distribution of modification of any kind without explicit written permission!
//
//  Bunches of webGL related junks!
//  Some handy functions/wrappers, along with classes for handling various things.
//

function adddef(s){return "#define " + s + "\n";}

var _torad = 0.01745329251994329576923690768489; //1/(180/pi) | rad = deg * _torad

var gl = null;
var glcanvas = null;

var _gl_clear = new Array(4);

//  Commonly used values, assigned in initWebGL
var no_rot;
var zero_vec;
var one_vec;

var g_cam = null;



function glClearColor(r,g,b,a){
	_gl_clear[0] = r;
	_gl_clear[1] = g;
	_gl_clear[2] = b;
	_gl_clear[3] = a;
	gl.clearColor(r,g,b,a);
}

function glClear(){
	//console.log("glClear()");
	gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
}

function initWebGL(e){
	glcanvas = document.getElementById(e);
	gl = glcanvas.getContext("webgl", {antialias:true}) || glcanvas.getContext("experimental-webgl", {antialias:true});
	if(!gl){
		alert("No webGL, how's that IE6 working out for you?");
		return false;
	}
	
	no_rot		= quat.create();
	zero_vec	= vec3.create();
	one_vec		= vec3.create();
	one_vec[0] = 1; one_vec[1] = 1; one_vec[2] = 1;
	
	gl.viewport(0, 0, glcanvas.width, glcanvas.height);
	gl.enable(gl.DEPTH_TEST);
	//gl.disable(gl.CULL_FACE);
	gl.enable(gl.CULL_FACE);
	gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
	gl.enable(gl.BLEND); // Enable this when needed.
	glClearColor(0,0,0,0);
	glClear();
	gl.flush();
	
	if(!g_cam) g_cam = new camera();
	{
		// Create some dummy lamps.
		{
			var l = new lamp();
			l.pos = [0, 0, 2.5];
			l.col = [.2, .6, .8];
			l.dist = 4.5;
			g_cam.lamps.push(l);
		}
		{
			var l = new lamp();
			l.pos = [0, 10, 2.5];
			l.col = [1, 1, 1];
			l.dist = 14.5;
			g_cam.lamps.push(l);
		}
		{
			var l = new lamp();
			l.pos = [512, 512, 512];
			l.col = [1, 1, 1];
			l.dist = 2048.5;
			g_cam.lamps.push(l);
		}
		{
			var l = new lamp();
			l.pos = [0, -10, 2.5];
			l.col = [1, 1, 1];
			l.dist = 24.5;
			g_cam.lamps.push(l);
		}
	}
	return true;
}

function preprender(){
	gl.viewport(0, 0, glcanvas.width, glcanvas.height);
	glClearColor(0,0,0,0);
	glClear();
	glClearColor(0,0,0,0); // Not being cleared after this point.

	
	
	g_cam.perspective();
	g_cam.makemats();
	g_cam.preplamps();
	for(var a in shaders) g_cam.apply(shaders[a]); // Apply the camera stuff to all shaders.
	//g_cam.apply(basic_shader);
}

////////////////////////////////////////////////////////////
// Program related things
////////////////////////////////////////////////////////////
function program(name_){
	this.name = name_;
	this.prog = null;
	
	// Attributes
	this.aPos	= null;
	this.aNorm	= null;
	this.aCol	= null;
	this.aUv	= null;
	this.aUv2	= null;
	
	//Uniforms
	this.uModel	= null;
	this.uView	= null;
	this.uProj	= null;
	
	this.uDiff		= null;
	this.uNorm		= null;
	this.uSpec		= null;
	this.uDetail	= null;
	
	this.uLamps			= null;
	this.uLampCol		= null;
	this.uLampPos		= null;
	this.uLampSize		= null;
	this.uLampAmbient	= null;
	
}

program.prototype.bind = function(){
	//console.log("Bound: ", this.name, this.prog);
	gl.useProgram(this.prog);
}

program.prototype.makeshader = function(verte, frage, defines){
	var vert = gl.createShader(gl.VERTEX_SHADER);
	var frag = gl.createShader(gl.FRAGMENT_SHADER);
	gl.shaderSource(vert, defines + "\n\n" + document.getElementById(verte).text);
	gl.shaderSource(frag, defines + "\n\n" + document.getElementById(frage).text);
	gl.compileShader(vert);
	gl.compileShader(frag);

	if (!gl.getShaderParameter(vert, gl.COMPILE_STATUS)) {
 		console.log(gl.getShaderInfoLog(vert));
	}else console.log("Vert compiled fine.");
	if (!gl.getShaderParameter(frag, gl.COMPILE_STATUS)) {
 		console.log(gl.getShaderInfoLog(frag));
	}else console.log("Frag compiled fine.");

	this.prog = gl.createProgram();
	gl.attachShader(this.prog, vert);
	gl.attachShader(this.prog, frag);
	
	gl.linkProgram(this.prog);
	if(!gl.getProgramParameter(this.prog, gl.LINK_STATUS)){
		console.log(gl.getProgramInfoLog(this.prog));
	}else console.log("Shader: ", this.name, " linked fine.");
	this.bind();
	
	this.aPos	= gl.getAttribLocation(this.prog, "aPos");
	this.aNorm	= gl.getAttribLocation(this.prog, "aNorm");
	this.aCol	= gl.getAttribLocation(this.prog, "aCol");
	this.aUv	= gl.getAttribLocation(this.prog, "aUv");
	this.aUv2	= gl.getAttribLocation(this.prog, "aUv2");
	
	this.uModel	= gl.getUniformLocation(this.prog, "uModel");
	this.uView	= gl.getUniformLocation(this.prog, "uView");
	this.uProj	= gl.getUniformLocation(this.prog, "uProj");
	
	this.uSpecPow	= gl.getUniformLocation(this.prog, "uSpecPow");
	this.uLampPow	= gl.getUniformLocation(this.prog, "uLampPow");
	
	this.uColor		= gl.getUniformLocation(this.prog, "uColor");
	
	this.uDiff		= gl.getUniformLocation(this.prog, "uDiff");
	this.uNorm		= gl.getUniformLocation(this.prog, "uNorm");
	this.uSpec		= gl.getUniformLocation(this.prog, "uSpec");
	this.uDetail	= gl.getUniformLocation(this.prog, "uDetail");
	
	if(this.uDiff)		gl.uniform1i(this.uDiff,	0);
	if(this.uNorm)		gl.uniform1i(this.uNorm,	1);
	if(this.uSpec)		gl.uniform1i(this.uSpec,	2);
	if(this.uDetail)	gl.uniform1i(this.uDetail,	3);
	
	//lamps
	this.uLamps			= gl.getUniformLocation(this.prog, "uLamps");
	this.uLampPos		= gl.getUniformLocation(this.prog, "uLampPos");
	this.uLampCol		= gl.getUniformLocation(this.prog, "uLampCol");
	this.uLampSize		= gl.getUniformLocation(this.prog, "uLampSize");
	this.uLampAmbient	= gl.getUniformLocation(this.prog, "uLampAmbient");
	if(this.uLamps)		gl.uniform1i(this.uLamps, 0);
	
	if(this.uSpecPow)	gl.uniform1f(this.uSpecPow, 250.0);
	if(this.uLampPow)	gl.uniform1f(this.uLampPow, 1.0);
	if(this.uColor)		gl.uniform4f(this.uColor, 1.0,1.0,1.0,1.0);
	
	if(this.aPos != -1)		gl.enableVertexAttribArray(this.aPos);
	if(this.aNorm != -1)	gl.enableVertexAttribArray(this.aNorm);
	if(this.aCol != -1)		gl.enableVertexAttribArray(this.aCol);
	if(this.aUv != -1)		gl.enableVertexAttribArray(this.aUv);
	if(this.aUv2 != -1)		gl.enableVertexAttribArray(this.aUv2);
	
	//console.log(this.aUv2, ", ", this.name);
	
	//dumpobj(this);
	
	//console.log("a", this.aPos, this.aNorm, this.aCol, this.aUv, "u", this.uModel, this.uView, this.uProj, this.uDiff, this.uNorm, this.uSpec);
}




////////////////////////////////////////////////////////////
// Texture related things
////////////////////////////////////////////////////////////


var textures = {}; // Should store/delete images in this upon creation/discard.

function texture(name, image){
	this.name	= name;
	this.id		= gl.createTexture();
	this.w		= image.width;
	this.h		= image.height;
	
	gl.bindTexture(gl.TEXTURE_2D, this.id);
	gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
	gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
	gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
	gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
	//console.log(this.name, this.w, this.h);
}

texture.prototype.del = function(){
	this.clear();
}

texture.prototype.clear = function(){
	//console.log("TODO - texture.prototype.clear !!!");
	gl.deleteTexture(this.id);
	this.id = null;
}

texture.prototype.bind = function(i){
	gl.activeTexture(gl.TEXTURE0 + i);
	gl.bindTexture(gl.TEXTURE_2D, this.id);
}




////////////////////////////////////////////////////////////
// Drawable related things
////////////////////////////////////////////////////////////

// [P]os (vec3), [N]ormal (vec3), [U]v (vec2), [C]olor (vec4)

var DRAWABLE_NONE = 0;
var DRAWABLE_PNU = 1;
var DRAWABLE_PNCU = 2;
var DRAWABLE_PNUU = 3;
//var DRAWABLE_

var DRAWABLE_NAME = new Array();
DRAWABLE_NAME.push("none");
DRAWABLE_NAME.push("pnu");
DRAWABLE_NAME.push("pncu");
DRAWABLE_NAME.push("pnuu");

function drawable(name_){
	this.name		= name_;
	this.numVerts	= 0;
	this.numIndices	= 0;
	this.type		= 0;
	this.shader		= null;

	this.buffer		= null;
	this.bufIndices	= null;


	this.ok			= 0;
	
	this.texDiff	= null;
	this.texNorm	= null;
	this.texSpec	= null;
	this.texDetail	= null;
	
	this.color		= [1,1,1,1];
	
	this.data; // TEMP, HACK, Storing this info so merging drawables will be easier.
	this.indices; // ^
}

drawable.prototype.del = function(){ // lol JS and deconstructors.
	this.clear();
}

drawable.prototype.clear = function(){
	if(this.ok == 0) return;
	this.ok = 0;
	
	if(this.buffer)		gl.deleteBuffer(this.buffer);
	if(this.bufIndices)	gl.deleteBuffer(this.bufIndices);
	
	this.buffer 	= null;
	this.bufIndices	= null;
	this.type		= 0;
	
	this.texture_diffuse	= null;
	this.texture_normal		= null;
	this.texture_specular	= null;
	this.texture_detail		= null;
}


// Perhaps in the future, make callbacks to call functions, instead of some if/else trees.

drawable.prototype.render = function(pos, rot, scale){try{/**/
	if(!this.ok){
		console.log("render failed, not setup.");
		return;
	}
	if(!this.shader){
		console.log("render failed, no shader.");
		return;
	}
	grendercalls++;

	this.shader.bind();
	
	
	
	{
	
		if(this.shader.uColor){
			gl.uniform4f(this.shader.uColor, this.color[0], this.color[1], this.color[2], this.color[3]);
		}
		
		var m = mat4.create(); // Model matrix.
		var tm = mat4.create();
		var tp = [pos[0] * 1, pos[1] * 1, pos[2] * 1];
		var tq = quat.clone(rot);
		
		
		mat4.translate(m, m, tp);
		quat.invert(tq, rot);
		mat4.fromQuat(tm, rot);
		mat4.multiply(m, m, tm);
		mat4.scale(m, m, scale);
		if(this.shader.uModel) gl.uniformMatrix4fv(this.shader.uModel, false, m);
		//else console.log("No uModel?");
		//console.log(mat4.str(m));
		//g_cam.apply(this.shader);
	}
	
	
	// POS, NORMAL, COLOR(s), UV(s)
	
	if(this.type == DRAWABLE_PNU || (this.type == DRAWABLE_PNUU)){
		var siz = (3 + 3 + 2) * 4; // p3, n3, u2
		if(this.type == DRAWABLE_PNUU) siz += 2 * 4; // extra u2
		
		gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
		if(this.shader.aPos != -1)		gl.vertexAttribPointer(this.shader.aPos,	3, gl.FLOAT, false, siz, 0 * 4);
		if(this.shader.aNorm != -1)		gl.vertexAttribPointer(this.shader.aNorm,	3, gl.FLOAT, false, siz, 3 * 4);
		if(this.shader.aUv != -1)		gl.vertexAttribPointer(this.shader.aUv,		2, gl.FLOAT, false, siz, 6 * 4);
		if(this.shader.aUv2 != -1)		gl.vertexAttribPointer(this.shader.aUv2, 	2, gl.FLOAT, false, siz, 8 * 4);
		
		if(this.texture_diffuse)	this.texture_diffuse.bind(0);
		if(this.texture_normal)		this.texture_normal.bind(1);
		if(this.texture_specular)	this.texture_specular.bind(2);
		if(this.texture_detail)		this.texture_detail.bind(3);
	}
	
	
	

	gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.bufIndices);
	if(this.shader.uColor) if(this.color[3] < 1.0){
		gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
		gl.enable(gl.BLEND);
	}
	if(this.numIndices) gl.drawElements(gl.TRIANGLES, this.numIndices, gl.UNSIGNED_SHORT, 0);
	else console.log("numIndices is ZERO!");
	gl.disable(gl.BLEND);
	gl.flush();
}
catch(err){
	console.log(this.name, ", ", err);
}/**/
}



////////////////////////////////////////////////////////////
// Functions to suppliment drawables, textures, and programs
////////////////////////////////////////////////////////////


// Object that has a bunch of default values assigned to easily be passed to create_drawable(...).
function create_blank_PNU_obj(){
	this.name = "";
	this.texture_diffuse = "default.png";
	this.texture_normal = "NONE";
	this.texture_specular = "NONE";
	this.texture_detail = "NONE";
	this.material = "basic";
	this.normals = 1;
	this.verts = 0;
	this.color_channels = 0;
	this.uv_channels = 1;
	this.data = new Array();
	this.indices = new Array();
}

function find_shader(s, type){
	var ss = DRAWABLE_NAME[type];
	if(s + "_" + ss in shaders) return shaders[s + "_" + ss];
	return shaders[s];
}

function grab_texture(s, refdefault){
	//console.log("grab_texture(", s, refdefault, ")");
	if(s == null) return null;
	if(s in textures) return textures[s];
	if("models/" + s in textures) return textures["models/" + s];
	if("models/" + s + ".png" in textures) return textures["models/" + s + ".png"];
	if(refdefault){
		if("default.png" in textures) return textures["default.png"];
		console.log("Error: No default.png texture found.");
	}
	return null;
}

var shaders = {};
function make_shader(name, vert, frag, defines){
	var shader = new program(name);
	shader.makeshader(vert, frag, defines);
	shaders[name] = shader;
	return shader;
}

var drawables = {};
function create_drawable(data, addtodict){
	data.name.trim();
	
	var type;
	
	if(data.normals == 1 && data.color_channels == 0 && data.uv_channels == 1) type = DRAWABLE_PNU;
	else if(data.normals == 1 && data.color_channels == 0 && data.uv_channels == 2) type = DRAWABLE_PNUU;
	else type = 0;
	
	if(type == 0){
		console.log(data.name, " isn't something supported.");
		data.data = "culled";
		//dumpobj(data);
	}
	var d = new drawable(data.name);
	d.type = type;

	var mat = data.material;
	mat = mat.replace("-material", ""); // Assimp uses the name, instead of id for whatever you can easily access, and Blender adds '-material' to the end of names, probably so it doesn't match the ID.
	d.shader = find_shader(mat, type);
	//console.log(d.shader.name, " ", DRAWABLE_NAME[type]);
	if(data.texture_diffuse == "NONE") data.texture_diffuse = data.name;
	if(data.texture_diffuse)	d.texture_diffuse	= grab_texture(data.texture_diffuse,	1);
	if(data.texture_normal)		d.texture_normal	= grab_texture(data.texture_normal,		0);
	if(data.texture_specular)	d.texture_specular	= grab_texture(data.texture_specular,	0);
	if(data.texture_detail)		d.texture_detail	= grab_texture(data.texture_detail,		0);
	
	if(d.type == DRAWABLE_PNU || d.type == DRAWABLE_PNUU){
		
		
		if(d.shader == null) {
			console.log("Warning: missing material: " + mat + ".  Defaulting to basic.");
			if(d.type == DRAWABLE_PNU)	d.shader = shaders["basic"];
			if(d.type == DRAWABLE_PNUU)	d.shader = shaders["basic_pnuu"];
		}
		//console.log(d.shader.name);
		d.shader.bind();
		
		d.buffer = gl.createBuffer();
		gl.bindBuffer(gl.ARRAY_BUFFER, d.buffer);
		gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data.data), gl.STATIC_DRAW);

		d.bufIndices = gl.createBuffer();
		gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, d.bufIndices);
		gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(data.indices), gl.STATIC_DRAW);
		
		d.numIndices	= data.indices.length;
		d.numVerts		= data.verts;
		
		d.data = data.data; // TEMP HACK -- FOR MERGING DRAWABLES LATER ON.
		d.indices = data.indices;
		
		d.ok = 1;
	}
	
	//console.log("OMG");
	//dumpobj(d);
	if(addtodict) drawables[data.name] = d;
	return d;
	//console.log(drawables);
	
}

////////////////////////////////////////////////////////////
// Lamp stuff.
////////////////////////////////////////////////////////////

function lamp(){
	this.pos = vec3.create();
	this.col = vec3.create();
	this.dist = 32.0;
	this.amb = 0.125;
}

////////////////////////////////////////////////////////////
// Camera stuff.
////////////////////////////////////////////////////////////

var MAX_LAMPS = 4;
function camera(){
	this.pos = vec3.create();
	this.rot = quat.create();
	
	this.fov = 45;
	this.clipNear = 0.1;
	this.clipFar =  256.0;
	
	this.view = mat4.create();
	this.proj = mat4.create();
	
	this.makemats();
	this.perspective();
	
	//Lamp stuff.
	this.lamps			= new Array();
	this.udat_lamp_pos	= new Array(); // vec3
	this.udat_lamp_col	= new Array(); // vec3
	this.udat_lamp_size	= new Array(); // f32
	this.udat_lamp_amb	= new Array(); // f32
	this.udat_lamps		= 0;
	{
		for(var a=0; a<MAX_LAMPS; a+=1){
			this.udat_lamp_pos.push(0.0);
			this.udat_lamp_pos.push(0.0);
			this.udat_lamp_pos.push(0.0);
			this.udat_lamp_col.push(1.0); // provide a debug color.
			this.udat_lamp_col.push(0.0);
			this.udat_lamp_col.push(0.5);
			this.udat_lamp_size.push(0);
			this.udat_lamp_amb.push(0);
		}
	}
}

camera.prototype.ortho = function(size){
	size *= .5;
	this.proj = mat4.create();
	mat4.ortho(this.proj, -size, size, -size, size, this.clipNear, this.clipFar);
}

camera.prototype.perspective = function(){
	this.proj = mat4.create();
	mat4.perspective(this.proj, this.fov * _torad, glcanvas.width / glcanvas.height, this.clipNear, this.clipFar);
	//				 (out,            fovy,                     aspect,                   near,           far)
	
	//dumpobj([this.proj, this.fov * _torad, glcanvas.width / glcanvas.height, this.clipNear, this.clipFar]);
	
}

camera.prototype.makemats = function(){
	var tp = vec3.create();
	tp[0] = this.pos[0] * -1;
	tp[1] = this.pos[1] * -1;
	tp[2] = this.pos[2] * -1;
	
	this.view = mat4.create();
	mat4.fromQuat(this.view, this.rot);
	mat4.translate(this.view, this.view, tp);

	//view = mat4_cast((rot));
	//view = translate((view),-pos);
}

camera.prototype.preplamps = function(){
	//this.lamps			= new Array();
	//this.udat_lamp_pos	= new Array(); // vec3
	//this.udat_lamp_col	= new Array(); // vec3
	//this.udat_lamp_size	= new Array(); // f32
	//this.udat_lamp_amb	= new Array(); // f32
	this.udat_lamps		= Math.min(this.lamps.length, MAX_LAMPS);
	
	for(var a=0; a<this.udat_lamps; a+=1){
		var l = this.lamps[a];
		var v = vec4.create();
		v[0] = l.pos[0];
		v[1] = l.pos[1];
		v[2] = l.pos[2];
		v[3] = 1.0;
		vec4.transformMat4(v, v, this.view);
		this.udat_lamp_pos[a*3+0] = v[0];
		this.udat_lamp_pos[a*3+1] = v[1];
		this.udat_lamp_pos[a*3+2] = v[2];
		this.udat_lamp_col[a*3+0] = l.col[0];
		this.udat_lamp_col[a*3+1] = l.col[1];
		this.udat_lamp_col[a*3+2] = l.col[2];
		this.udat_lamp_size[a]	  = l.dist;
		this.udat_lamp_amb[a]	  = l.amb;
	}
	//console.log(this);
}

camera.prototype.apply = function(prog){
	prog.bind();
	if(prog.uView) gl.uniformMatrix4fv(prog.uView, false, this.view);
	//else console.log("NO uView!!!");
	if(prog.uProj) gl.uniformMatrix4fv(prog.uProj, false, this.proj);
	//else console.log("NO uProj!!!");
	
	// Cameras.
	if(prog.uLamps){
		gl.uniform1i(prog.uLamps, this.udat_lamps);
		gl.uniform3fv(prog.uLampPos, this.udat_lamp_pos);
		gl.uniform3fv(prog.uLampCol, this.udat_lamp_col);
		gl.uniform1fv(prog.uLampSize, this.udat_lamp_size);
		gl.uniform1fv(prog.uLampAmbient, this.udat_lamp_amb);
		//console.log(this.udat_lamps);
	}//else console.log("nolamps for: ",prog.name)
}

camera.prototype.lookAt = function(p){
	var m = mat4.create();
	var mm = mat3.create();
	mat4.lookAt(m, this.pos, p, [0,0,1]);
	mat3.fromMat4(mm, m);
	quat.fromMat3(this.rot, mm);
	//mat4.lookAt = function (out, eye, center, up) {
}






