﻿/* 
 * Copyright (c) 2011 Chen Zhuhui
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

"use strict";

var galgames = {

    initialize : function(config) {
	
	var context = {
	    create : function(proto) {
		var newObj = Object.create(proto);
		Object.defineProperty(newObj, "$", { value : this,
						     writable : false });
		return newObj;
	    }
	};

	Object.defineProperty(config.listener, "$",
			      { value : context,
				writable : false });

	var onload = function() {
	    var cfg = config;
	    var ctx = context;

	    // create canvas element
	    var container = document.getElementById(config.container);
	    var canvas = document.createElement("canvas");
	    canvas.width = config.width;
	    canvas.height = config.height;

	    if (config.disableContextMenu) {
		canvas.oncontextmenu = function() {
		    return false;
		};
	    }

	    container.appendChild(canvas);

	    // create webgl context
	    var gl = WebGLUtils.setupWebGL(canvas);
	    var fnr = /function +(\w+) *\(.*?\)/;
	    var ERROR_CODE = {
		0x0000 : "NO_ERROR",
		0x0500 : "INVALID_ENUM",
		0x0501 : "INVALID_VALUE",
		0x0502 : "INVALID_OPERATION",
		0x0503 : "OUT_OF_MEMORY"
	    };

	    // wrapper for calling webgl functions. if config.debug is
	    // true, error code is checked for every webgl functions
	    // call invoked by this wrapper.
	    var glc;
	    if (config.debug) {
		glc =function() {
		    var func = gl[arguments[0]];
		    var args = Array.prototype.slice.call(arguments, 1);
		    var ret = func.apply(gl, args);
		    var error = gl.getError();
		    if (error !== WebGLRenderingContext.NO_ERROR) {
			var fn = (fnr.exec(String(func)))[1];
			console.error("[ERROR: "+ ERROR_CODE[error] +"] " +
				      "WebGL call failed.");
			console.error("The failed call: "+ fn +"("+ args +")");
			throw new Error("WebGL call failed.");
		    }
		    return ret;
		};
	    } else {
		glc = function() {
		    return gl[arguments[0]].
			apply(gl, Array.prototype.slice.call(arguments, 1));
		};
	    }

	    var onDraw = function() {
		requestAnimFrame(onDraw, ctx.canvas);
		cfg.listener.onDraw();
		ctx.gl.flush();
	    };

	    ctx.canvas = canvas;
	    ctx.gl = gl;
	    ctx.glc = glc;

	    cfg.listener.onInitialize();

	    onDraw();
	};

	if (window.addEventListener !== undefined) {
	    window.addEventListener("load", onload, false);
	} else if (window.attachListener !== undefined) {
	    window.attachListener("onload", onload);
	}
	
	return context;
    },

    // constants definition
    VFUsagePosition     :  0,
    VFUsageColor        :  1,
    VFUsageTexCoord     :  2,
    VFUsageNormal       :  3,

    ArrayBuffer         :  0,
    ElementArrayBuffer  :  1,

    Byte                :  0,
    UnsignedByte        :  1,
    Short               :  2,
    UnsignedShort       :  3,
    Int                 :  4,
    UnsignedInt         :  5,
    Float               :  6,

    VFTypeUbyte         :  0,
    VFTypeShort         :  1,
    VFTypeInt           :  2,
    VFTypeFloat         :  3,

    BufUsageStatic      :  0,
    BufUsageDynamic     :  1,
    BufUsageStream      :  2,

    VertexShader        :  0,
    FragmentShader      :  1

};

// classes
galgames.VertexFormat = {
    declare : function() {
	if (arguments.length % 3 !== 0) {
	    throw new Error("Invalid number of arguments: " + 
			    Array.prototype.slice.call(arguments, 0));
	}
	var usages = {};
	var vfSize = 0;
	var i;
	for (i = 0; i < arguments.length; i += 3) {
	    var offset = vfSize;
	    var usage = arguments[i];
	    var type;
	    var typeSize;
	    switch (arguments[i+1]) {
	    case galgames.VFTypeUbyte:
		type = WebGLRenderingContext.UNSIGNED_BYTE;
		typeSize = 1;
		break;
	    case galgames.VFTypeShort:
		type = WebGLRenderingContext.SHORT;
		typeSize = 2;
		break;
	    case galgames.VFTypeInt:
		type = WebGLRenderingContext.INT;
		typeSize = 4;
		break;
	    case galgames.VFTypeFloat:
		type = WebGLRenderingContext.FLOAT;
		typeSize = 4;
		break;
	    default:
		throw new Error("Unexpected type.");
	    }
	    var size = arguments[i+2];
	    var bytes = typeSize * size;
	    usages[usage] = { offset : offset,
			      type : type,
			      size : size };
	    bytes = ((bytes - 1) | 3) + 1;
	    vfSize += bytes;
	}

	Object.defineProperty(this, "usages", { value : usages,
						writable : false,
						enumerable : true
					      });
	Object.defineProperty(this, "size", { value : vfSize, 
					      writable : false,
					      enumerable : true
					    });
    }

};

// buffer for both vertices and indices
galgames.Buffer = {

    _updateGLBuf : function() {
	this.$.glc("bufferSubData",
		   this._type,
		   this._nearestChanged,
		   new Uint8Array(this._dataBuf,
				  this._nearestChanged,
				  this._furthestChanged - 
				  this._nearestChanged));
	this._nearestChanged = this._bufferSize;
	this._furthestChanged = 0;
	this._isDirty = false;
    },

    allocate : function(type, format, size, usage) {

	switch(type) {
	case galgames.ArrayBuffer:
	    this._type = WebGLRenderingContext.ARRAY_BUFFER;
	    break;
	case galgames.ElementArrayBuffer:
	    this._type = WebGLRenderingContext.ELEMENT_ARRAY_BUFFER;
	    break;
	default:
	    throw new Error("Invalid buffer type.");
	}

	if (type === galgames.ArrayBuffer) {
	    // format represents the vertex format for the array buffer.
	    this._format = format;
	    this._formatSize = format.size;

	} else {
	    // format represents the data type of the index element of
	    // the element array buffer (which can only be either
	    // UNSIGNED_BYTE or UNSIGNED_SHORT)
	    switch (format) {
	    case galgames.UnsignedByte:
		this._format = WebGLRenderingContext.UNSIGNED_BYTE;
		this._formatSize = 1;
		break;
	    case galgames.UnsignedShort:
		this._format = WebGLRenderingContext.UNSIGNED_SHORT;
		this._formatSize = 2;
		break;
	    default:
		throw new Error("Unsupport data type for element array buffer");
	    }
	}

	switch (usage) {
	case galgames.BufUsageStatic:
	    this._usage = WebGLRenderingContext.STATIC_DRAW;
	    break;
	case galgames.BufUsageDynamic:
	    this._usage = WebGLRenderingContext.DYNAMIC_DRAW;
	    break;
	case galgames.BufUsageStream:
	    this._usage = WebGLRenderingContext.STREAM_DRAW;
	    break;
	default:
	    throw new Error("Invalid usage type.");
	}

	this._bufferSize = this._formatSize * size;

	this._dataBuf = new ArrayBuffer(this._bufferSize);
	this._glBuf = this.$.glc("createBuffer");
	this._nearestChanged = this._bufferSize;
	this._furthestChanged = 0;
	this._isBound = false;
	this._isDirty = false;

	this.$.glc("bindBuffer", this._type, this._glBuf);
	this.$.glc("bufferData", this._type, this._bufferSize, this._usage);
	this.$.glc("bindBuffer", this._type, null);

	Object.defineProperty(this, "format",
			      { value : format,
				writable : false });
	Object.defineProperty(this, "capacity", 
			      { value : size,
				writable : false,
				enumerable : true });
	
    },

    bind : function() {
	if (!this._isBound) {
	    this.$.glc("bindBuffer", 
		       this._type,
		       this._glBuf);
	    if (this._type === WebGLRenderingContext.ARRAY_BUFFER) {
		this.$.shader.onBindVertexBuffer(this);
	    }
	    this._isBound = true;
	}

	if (this._isDirty) {
	    this._updateGLBuf();
	}
    },

    set : function(start, data) {
	var nc = start * this._formatSize;
	var fc = nc;

	if (this._type === WebGLRenderingContext.ARRAY_BUFFER) {
	    var i = 0;
	    while (i < data.length) {
		var vc = this._format.usages;
		var u;
		for (u in vc) {
		    var usage = vc[u];
		    var tvt;
		    switch (usage.type) {
		    case WebGLRenderingContext.UNSIGNED_BYTE:
			tvt = Uint8Array;
			break;
		    case WebGLRenderingContext.SHORT:
			tvt = Int16Array;
			break;
		    case WebGLRenderingContext.INT:
			tvt = Int32Array;
			break;
		    case WebGLRenderingContext.FLOAT:
			tvt = Float32Array;
			break;
		    }

		    (new tvt(this._dataBuf, fc + usage.offset)).
			set(data.slice(i, i + usage.size));
		    i += usage.size;
		}
		fc += this._formatSize;
	    }
	} else {
	    var t;
	    if (this._format === WebGLRenderingContext.UNSIGNED_BYTE) {
		t = Uint8Array;
	    } else {
		t = Uint16Array;
	    }
	    (new t(this._dataBuf, nc)).set(data);
	    fc += this._formatSize * data.length;
	}
	
	if (nc < this._nearestChanged) {
	    this._nearestChanged = nc;
	}
	if (fc > this._furthestChanged) {
	    this._furthestChanged = fc;
	}

	if (this._isBound) {
	    this._updateGLBuf();
	} else {
	    this._isDirty = true;
	}
    },
    
    unbind : function() {
	if (this._isBound) {
	    if (this._type === WebGLRenderingContext.ARRAY_BUFFER) {
		this.$.shader.onUnbindVertexBuffer(this);
	    }
	    this.$.glc("bindBuffer", this._type, null);
	    this._isBound = false;
	}
    },

    dispose : function() {
	this.unbind();
	this.$.glc("deleteBuffer", this._glBuf);
    }

};

galgames.Mesh = {
    
    Points : 0,
    Lines : 1,
    LineLoop : 2,
    LineStrip : 3,
    Triangles : 4,
    TriangleStrip : 5,
    TriangleFan : 6,

    define : function( vertexFormat,
		       maxVertices, vertexUsage,
		       maxIndices, indexUsage) {
	this._primitiveType = WebGLRenderingContext.TRIANGLE_STRIP;
	if (maxVertices && maxVertices <= 0) {
	    throw new Error("Mesh must contains vertices.");
	}
	this._vb = this.$.create(galgames.Buffer);
	this._vb.allocate(
	    galgames.ArrayBuffer,
	    vertexFormat, maxVertices, vertexUsage);
	if (maxIndices && maxIndices > 0) {
	    this._ib = this.$.create(galgames.Buffer);
	    this._ib.allocate(
		galgames.ElementArrayBuffer,
		galgames.UnsignedShort,
		maxIndices, indexUsage);
	    this._useIndices = true;
	} else {
	    this._useIndices = false;
	}

	Object.defineProperty(this, "primitiveType", {
	    get : function() {
		switch (this._primitiveType) {
		case WebGLRenderingContext.POINTS:
		    return galgames.Mesh.Points;
		case WebGLRenderingContext.LINES:
		    return galgames.Mesh.Lines;
		case WebGLRenderingContext.LINE_LOOP:
		    return galgames.Mesh.LineLoop;
		case WebGLRenderingContext.LINE_STRIP:
		    return galgames.Mesh.LineStrip;
		case WebGLRenderingContext.TRIANGLE_STRIP:
		    return galgames.Mesh.TriangleStrip;
		case WebGLRenderingContext.TRIANGLE_FAN:
		    return galgames.Mesh.TriangleFan;
		default:
		    throw new Error("Unknown primitive type.");
		}
	    },
	    set : function(type) {
		switch (type) {
		case galgames.Mesh.Points:
		    this._primitiveType = WebGLRenderingContext.POINTS;
		    break;
		case galgames.Mesh.Lines:
		    this._primitiveType = WebGLRenderingContext.LINES;
		    break;
		case galgames.Mesh.LineLoop:
		    this._primitiveType = WebGLRenderingContext.LINE_LOOP;
		    break;
		case galgames.Mesh.Triangles:
		    this._primitiveType = WebGLRenderingContext.TRIANGLES;
		    break;
		case galgames.Mesh.TriangleStrip:
		    this._primitiveType = WebGLRenderingContext.TRIANGLE_STRIP;
		    break;
		case galgames.Mesh.TriangleFan:
		    this._primitiveType = WebGLRenderingContext.TRIANGLE_FAN;
		    break;
		default:
		    console.warn("Unknown primitive type. Use TriangleStrip instead.");
		    this._primitiveType = WebGLRenderingContext.TRIANGLE_STRIP;
		    break;
		}
	    }
	});
    },

    setVertices : function(startVertex, vertices) {
	this._vb.set(startVertex, vertices);
    },

    setIndices : function(startIndex, indices) {
	if (this._useIndices && this._ib) {
	    this._ib.set(startIndex, indices);
	}
    },

    setUseIndices : function(useIndices) {
	if (useIndices && this._ib) {
	    this._useIndices = true;
	} else {
	    this._useIndices = false;
	}
    },

    draw : function(start, count) {
	if (!start) {
	    start = 0;
	}
	this._vb.bind();
	if (this._useIndices) {
	    if (!count) {
		count = this._ib.capacity;
	    }
	    this._ib.bind();
	    this.$.glc("drawElements", this._primitiveType, count,
		       WebGLRenderingContext.UNSIGNED_SHORT, start * 2);
	    // this._ib.unbind();
	} else {
	    if (!count) {
		count = this._vb.capacity;
	    }
	    this.$.glc("drawArrays", this._primitiveType, start, count);
	}
	// this._vb.unbind();
    },

    dispose : function() {
	if (this.ib) {
	    this.ib.dispose();
	}
	this.vb.dispose();
    }
};

galgames.ShaderModule = {
    
    define : function(source, type) {
	if (source === undefined || type === undefined) {
	    throw new Error("Source code and a specific type " +
			    "must be assigned to a shader module");
	}
	var shaderType;
	switch (type) {
	case galgames.VertexShader:
	    shaderType = WebGLRenderingContext.VERTEX_SHADER;
	    break;
	case galgames.FragmentShader:
	    shaderType = WebGLRenderingContext.FRAGMENT_SHADER;
	    break;
	default:
	    throw new Error("Unknown shader type: " + shaderType);
	}

	Object.defineProperty(this, "source", { value : source,
						writable : false });
	Object.defineProperty(this, "type", { value : type,
					      writable : false });

	this._shader = this.$.glc("createShader", shaderType);
	this.$.glc("shaderSource", this._shader, source);
	this.$.glc("compileShader", this._shader);

	var compileStatus = this.$.glc("getShaderParameter", this._shader,
				       WebGLRenderingContext.COMPILE_STATUS);
	if (!compileStatus) {
	    console.error("Failed to compile shader module.");
	    var log = this.$.glc("getShaderInfoLog", this._shader);
	    console.error(log);
	}
	
    },

    dispose : function() {
	this.$.glc("deleteShader", this._shader);
    }
    
};

galgames.Shader = {

    _enumerateActiveAttribs : function() {
	var activeAttribsCount = 
	    this.$.glc("getProgramParameter",
		       this._prog,
		       WebGLRenderingContext.ACTIVE_ATTRIBUTES);
	this._attribs = {};
	var info;
	var i;
	for (i = 0; i < activeAttribsCount; ++i) {
	    info = this.$.glc("getActiveAttrib", this._prog, i);
	    var name = info.name;
	    var size = info.size;
	    var type = info.type;
	    
	    var loc = this.$.glc("getAttribLocation", this._prog,
				 name);

	    this._attribs[name] = { location : loc,
				    type : type,
				    size : size };
	}
    },

    _enumerateActiveUniforms : function() {
	var activeUniformsCount =
	    this.$.glc("getProgramParameter",
		       this._prog,
		       WebGLRenderingContext.ACTIVE_UNIFORMS);
	this._uniforms = {};
	var info;
	var i;
	for (i = 0; i < activeUniformsCount; ++i) {
	    info = this.$.glc("getActiveUniform", this._prog, i);
	    var name = info.name;
	    var size = info.size;
	    var type = info.type;

	    var loc = this.$.glc("getUniformLocation", this._prog,
				 name);
	    this._uniforms[name] = { location : loc,
				     type : type,
				     size : size };
	}
    },
    
    define : function() {
	this._lights = [];
	this._prog = this.$.glc("createProgram");
	var i;
	for (i = 0; i < arguments.length; ++i) {
	    this.$.glc("attachShader", this._prog, arguments[i]._shader);
	}
	this.$.glc("linkProgram", this._prog);

	var status = this.$.glc("getProgramParameter", this._prog,
				WebGLRenderingContext.LINK_STATUS);
	if (!status) {
	    console.error("Failed to link shader.");
	    var log = this.$.glc("getProgramInfoLog", this._prog);
	    console.error(log);
	}
	
	this._enumerateActiveAttribs();
	this._enumerateActiveUniforms();

	this._enabledVertexAttribsArray = [];
	this._usageAttribMap = {};
    },

    use : function() {
	if (this.$.shader) {
	    this.$.shader.unuse();
	}
	this.$.shader = this;
	this.$.glc("useProgram", this._prog);
    },

    unuse : function() {
	var i;
	for (i = 0; i < this._enabledVertexAttribsArray.length; ++i) {
	    this.$.glc("disableVertexAttribArray",
		       this._enabledVertexAttribsArray[i]);
	}
	this._enabledVertexAttribsArray = [];
    },

    setAttribute : function(attribName, usage) {
	var attrib = this._attribs[attribName];
	if (attrib === undefined) {
	    throw new Error("Attrib '" + attribName + 
			    "' is not defined or not active " +
			    "in the shader.");
	} else {
	    this._usageAttribMap[usage] = attribName;
	}
    },

    setUniform : function(uniformName, value) {
	var uniform = this._uniforms[uniformName];
	
	if (uniform === undefined) {
	    throw new Error("Uniform named '" + uniformName +
			    "' is not defined or not active " +
			    "in the shader.");
	} else {
	    var webglfunc;
	    switch (uniform.type) {
	    case WebGLRenderingContext.FLOAT:
		this.$.glc("uniform1f", uniform.location, value);
		break;
	    case WebGLRenderingContext.FLOAT_VEC2:
		this.$.glc("uniform2fv", uniform.location, value);
		break;
	    case WebGLRenderingContext.FLOAT_VEC3:
		this.$.glc("uniform3fv", uniform.location, value);
		break;
	    case WebGLRenderingContext.FLOAT_VEC4:
		this.$.glc("uniform4fv", uniform.location, value);
		break;
	    case WebGLRenderingContext.INT:
	    case WebGLRenderingContext.BOOL:
		this.$.glc("uniform1i", uniform.location, value);
		break;
	    case WebGLRenderingContext.INT_VEC2:
	    case WebGLRenderingContext.BOOL_VEC2:
		this.$.glc("uniform2iv", uniform.location, value);
		break;
	    case WebGLRenderingContext.INT_VEC3:
	    case WebGLRenderingContext.BOOL_VEC3:
		this.$.glc("uniform3iv", uniform.location, value);
		break;
	    case WebGLRenderingContext.INT_VEC4:
	    case WebGLRenderingContext.BOOL_VEC4:
		this.$.glc("uniform4iv", uniform.location, value);
		break;
	    case WebGLRenderingContext.FLOAT_MAT2:
		this.$.glc("uniformMatrix2fv", uniform.location, false, value);
		break;
	    case WebGLRenderingContext.FLOAT_MAT3:
		this.$.glc("uniformMatrix3fv", uniform.location, false, value);
		break;
	    case WebGLRenderingContext.FLOAT_MAT4:
		this.$.glc("uniformMatrix4fv", uniform.location, false, value);
		break;
	    }
	}
	
    },

    onBindVertexBuffer : function(vb) {
	var usages = vb.format.usages;
	var u;
	for (u in usages) {
	    var attrib = this._attribs[this._usageAttribMap[u]];
	    if (attrib === undefined) {
		console.warn("No active attrib named '" + 
			     u + "' in this shader.");
		continue;
	    }
	    var usage = usages[u];
	    this.$.glc("enableVertexAttribArray", attrib.location);
	    this.$.glc("vertexAttribPointer", attrib.location,
		       usage.size,
		       usage.type,
		       false,
		       vb.format.size,
		       usage.offset);
	    this._enabledVertexAttribsArray.push(attrib.location);
	}
    },

    onUnbindVertexBuffer : function(vb) {
    },

    dispose : function() {
	this.$.glc("deleteProgram", this._prog);
    }
};

