text: var ShaderCall = require('../renderer/ShaderCall');
  var Matrix3 = require('../math/Matrix3');
  var Matrix4 = require('../math/Matrix4');
  var World = require('../entities/World');
  var RenderQueue = require('../renderer/RenderQueue');
  var ObjectUtils = require('../util/ObjectUtils');
  var SystemBus = require('../entities/SystemBus');
  
  /* global WebGLRenderingContext */
  
  /**
   * Defines vertex and fragment shader and uniforms to shader callbacks
   * @param {string} name Shader name (mostly for debug/tool use)
   * @param {ShaderDefinition} shaderDefinition Shader data
   *
   * <code>
   * {
   *    vshader: [required] vertex shader source
   *    fshader: [required] fragment shader source
   *    defines : shader definitions (becomes #define)
   *    attributes : attribute bindings
   *       attribute bindings need to map to an attribute in the meshdata being rendered
   *    uniforms : uniform bindings
   *       uniform bindings can be a value (like 2.5 or [1, 2]) or a function
   * }
   * </code>
   */
  function Shader(name, shaderDefinition) {
  	if (!shaderDefinition.vshader || !shaderDefinition.fshader) {
  		throw new Error('Missing shader sources for shader: ' + name);
  	}
  
  	this.originalShaderDefinition = shaderDefinition;
  	this.shaderDefinition = shaderDefinition;
  
  	this.origVertexSource = shaderDefinition.vshader;
  	this.origFragmentSource = shaderDefinition.fshader;
  
  	/** The shader name
  	 * @type {string}
  	 */
  	this.name = name;
  
  	this.shaderProgram = null;
  	this.vertexShader = null;
  	this.fragmentShader = null;
  
  	/**
  	 * The renderer where the program and shaders were allocated.
  	 * @type {WebGLRenderingContext}
  	 */
  	this.renderer = null;
  
  	/**
  	 * Attributes detected in the shader source code.
  	 * Maps attribute variable's name to <code>{format: string}</code>
  	 * @type {Object<string, object>}}
  	 */
  	this.attributeMapping = {};
  
  	/**
  	 * Maps attribute variable's name to attribute location (from getAttribLocation).
  	 * @type {Object<string, number>}
  	 */
  	this.attributeIndexMapping = {};
  
  	/**
  	 * Uniforms detected in the shader source code.
  	 * Maps variable name to <code>{format: string}</code>.
  	 * @type {Object<string, object>}
  	 */
  	this.uniformMapping = {};
  
  	/**
  	 * Maps uniform variable name to ShaderCall object.
  	 * @type {Object<string, ShaderCall>}
  	 */
  	this.uniformCallMapping = {};
  
  	/**
  	 * Texture slots detected in the shader source code.
  	 * Will be an array of <code>{format: string, name: string}</code>
  	 * @type {Array}
  	 */
  	this.textureSlots = [];
  	this.textureSlotsNaming = {};
  	this.textureIndex = 0;
  
  	this.currentCallbacks = {};
  
  	this.overridePrecision = shaderDefinition.precision || null;
  	this.processors = shaderDefinition.processors;
  	this.builder = shaderDefinition.builder;
  	this.defines = shaderDefinition.defines || {};
  	this.attributes = shaderDefinition.attributes || {};
  	this.uniforms = shaderDefinition.uniforms || {};
  	this.defineKey = shaderDefinition.defineKey || '';
  	this.defineKeyDirty = true;
  	this.frameStart = true;
  	this.attributeKeys = null;
  	this.matchedUniforms = [];
  
  	/** Determines the order in which an object is drawn. There are four pre-defined render queues:
  	 *		<ul>
  	 *			<li>RenderQueue.BACKGROUND = Rendered before any other objects. Commonly used for skyboxes and the likes (0-999)
  	 *			<li>RenderQueue.OPAQUE = Used for most objects, typically opaque geometry. Rendered front to back (1000-1999)
  	 *			<li>RenderQueue.TRANSPARENT = For all alpha-blended objects. Rendered back to front (2000-2999)
  	 *			<li>RenderQueue.OVERLAY = For overlay effects like lens-flares etc (3000+)
  	 *		</ul>
  	 * By default materials use the render queue of the shader. See {@link RenderQueue} for more info
  	 * @type {number}
  	 */
  	this.renderQueue = RenderQueue.OPAQUE;
  
  	// this._id = Shader.id++;
  	if (Shader.cache.has(shaderDefinition)) {
  		this._id = Shader.cache.get(shaderDefinition);
  	} else {
  		this._id = Shader.cache.size;
  		Shader.cache.set(shaderDefinition, this._id);
  	}
  	// console.log('creating shader', this._id, this.name);
  
  	this.errorOnce = false;
  
  	this.vertexSource = typeof this.origVertexSource === 'function' ? this.origVertexSource() : this.origVertexSource;
  	this.fragmentSource = typeof this.origFragmentSource === 'function' ? this.origFragmentSource() : this.origFragmentSource;
  
  	// @ifdef DEBUG
  	Object.seal(this);
  	// @endif
  }
  
  // Shader.id = 0;
  Shader.cache = new Map();
  
  Shader.prototype.clone = function () {
  	return new Shader(this.name, ObjectUtils.deepClone({
  		precision: this.precision,
  		processors: this.processors,
  		builder: this.builder,
  		defines: this.defines,
  		attributes: this.attributes,
  		uniforms: this.uniforms,
  		vshader: this.origVertexSource,
  		fshader: this.origFragmentSource,
  		defineKey: this.defineKey
  	}));
  };
  
  Shader.prototype.cloneOriginal = function () {
  	return new Shader(this.name, this.originalShaderDefinition);
  };
  
  /**
   * Compiles a shader and does not apply it
   * @private
   * @param renderer
   */
  Shader.prototype.precompile = function (renderer) {
  	if (this.shaderProgram === null) {
  		this._investigateShaders();
  		this.addDefines(this.defines);
  		this.addPrecision(this.overridePrecision || renderer.shaderPrecision);
  		this.compile(renderer);
  	}
  };
  
  /*
   * Matches an attribute or uniform variable declaration.
   *
   * Match groups:
   *
   *   1: type (attribute|uniform)
   *   2: format (float|int|bool|vec2|vec3|vec4|mat2|mat3|mat4|sampler2D|sampler3D|samplerCube)
   *   3: variable name
   *   4: if exists, the variable is an array
   */
  var regExp = /\b(attribute|uniform)\s+(float|int|bool|vec2|vec3|vec4|mat2|mat3|mat4|sampler2D|sampler3D|samplerCube)\s+(\w+)(\s*\[\s*\w+\s*\])*;/g;
  
  Shader.prototype.compileProgram = function (renderer) {
  	if (this.shaderProgram === null) {
  		this._investigateShaders();
  		this.addDefines(this.defines);
  		this.addPrecision(this.overridePrecision || renderer.shaderPrecision);
  		this.compile(renderer);
  	}
  };
  
  Shader.prototype.activateProgram = function (record, context) {
  	if (record.usedProgram !== this.shaderProgram) {
  		context.useProgram(this.shaderProgram);
  		record.usedProgram = this.shaderProgram;
  		return true;
  	}
  };
  
  Shader.prototype.bindAttributeKey = function (record, renderer, attributeMap, key) {
  	var attribute = attributeMap[this.attributes[key]];
  	if (!attribute) {
  		return;
  	}
  
  	var attributeIndex = this.attributeIndexMapping[key];
  	if (attributeIndex === undefined) {
  		// console.warn('Attribute binding [' + name + '] does not exist in the shader.');
  		return;
  	}
  
  	record.newlyEnabledAttributes[attributeIndex] = true;
  	renderer.bindVertexAttribute(attributeIndex, attribute);
  };
  
  Shader.prototype.bindAttributes = function (record, renderer, attributeMap) {
  	if (this.attributes) {
  		for (var i = 0, l = this.attributeKeys.length; i < l; i++) {
  			this.bindAttributeKey(record, renderer, attributeMap, this.attributeKeys[i]);
  		}
  	}
  };
  
  Shader.prototype.disableAttributes = function (record, context) {
  	for (var i = 0, l = record.enabledAttributes.length; i < l; i++) {
  		var enabled = record.enabledAttributes[i];
  		var newEnabled = record.newlyEnabledAttributes[i];
  		if (!newEnabled && enabled) {
  			context.disableVertexAttribArray(i);
  			record.enabledAttributes[i] = false;
  		}
  	}
  };
  
  Shader.prototype.enableAttributes = function (record, context) {
  	for (var i = 0, l = record.newlyEnabledAttributes.length; i < l; i++) {
  		var enabled = record.enabledAttributes[i];
  		var newEnabled = record.newlyEnabledAttributes[i];
  		if (newEnabled && !enabled) {
  			context.enableVertexAttribArray(i);
  			record.enabledAttributes[i] = true;
  		}
  	}
  };
  
  Shader.prototype.matchUniforms = function (shaderInfo) {
  	var uniforms = this.matchedUniforms;
  	if (uniforms) {
  		this.textureIndex = 0;
  
  		for (var i = 0, l = uniforms.length; i < l; i++) {
  			this._bindUniform(uniforms[i], shaderInfo);
  		}
  	}
  };
  
  Shader.prototype.apply = function (shaderInfo, renderer) {
  	var context = renderer.context;
  	var record = renderer.rendererRecord;
  
  	this.compileProgram(renderer);
  	// Set the ShaderProgram active
  	this.activateProgram(record, context);
  
  	record.newlyEnabledAttributes.length = 0;
  
  	// Bind attributes
  	this.bindAttributes(record, renderer, shaderInfo.meshData.attributeMap);
  
  	this.disableAttributes(record, context);
  	this.enableAttributes(record, context);
  	this.matchUniforms(shaderInfo);
  };
  
  Shader.prototype.defineValue = function (shaderInfo, name) {
  	var defValue = shaderInfo.material.uniforms[name];
  	if (defValue === undefined) {
  		defValue = this.uniforms[name];
  	}
  	return defValue;
  };
  
  
  Shader.prototype.mapSlot = function (shaderInfo, mapping, slot) {
  	var maps = shaderInfo.material.getTexture(slot.mapping);
  	if (maps instanceof Array) {
  		this.arrayType(mapping, slot, maps);
  	} else {
  		slot.index = this.textureIndex;
  		mapping.call(this.textureIndex++);
  	}
  };
  
  Shader.prototype.arrayType = function (mapping, slot, maps) {
  	var arr = [];
  	slot.index = [];
  	for (var i = 0; i < maps.length; i++) {
  		slot.index.push(this.textureIndex);
  		arr.push(this.textureIndex++);
  	}
  	mapping.call(arr);
  };
  
  Shader.prototype.stringType = function (shaderInfo, name, mapping) {
  	var callback = this.currentCallbacks[name];
  	if (callback) {
  		callback(mapping, shaderInfo);
  	} else {
  		var slot = this.textureSlotsNaming[name];
  		if (slot !== undefined) {
  			this.mapSlot(shaderInfo, mapping, slot);
  		}
  	}
  };
  
  Shader.prototype.callMapping = function (shaderInfo, name, mapping) {
  	var defValue = this.defineValue(shaderInfo, name);
  	var type = typeof defValue;
  	if (type === 'string') {
  		this.stringType(shaderInfo, name, mapping, defValue);
  	} else {
  		var value = type === 'function' ? defValue(shaderInfo) : defValue;
  		if (value !== undefined) {
  			mapping.call(value);
  		}
  	}
  };
  
  Shader.prototype._bindUniform = function (name, shaderInfo) {
  	var mapping = this.uniformCallMapping[name];
  	if (mapping === undefined) {
  		return;
  	}
  	this.callMapping(shaderInfo, name, mapping);
  };
  
  Shader.prototype.setDefine = function (key, value) {
  	this.defineKeyDirty = this.defineKeyDirty || this.defines[key] !== value;
  	this.defines[key] = value;
  };
  
  Shader.prototype.removeDefine = function (key) {
  	this.defineKeyDirty = this.defineKeyDirty || this.defines[key] !== undefined;
  	this.defines[key] = undefined;
  };
  
  Shader.prototype.hasDefine = function (key) {
  	return this.defines[key] !== false && this.defines[key] !== undefined;
  };
  
  Shader.prototype.startFrame = function () {
  	this.frameStart = true;
  };
  
  Shader.prototype.endFrame = function () {
  	this.frameStart = false;
  };
  
  Shader.prototype.updateProcessors = function (renderInfo) {
  	if (this.processors) {
  		for (var j = 0; j < this.processors.length; j++) {
  			this.processors[j](this, renderInfo);
  		}
  	}
  };
  
  Shader.prototype.getDefineKey = function (definesIndices) {
  	if (this.defineKeyDirty) {
  		var key = 'Key:' + this.name;
  		var defineArray = Object.keys(this.defines);
  		for (var i = 0; i < defineArray.length; i++) {
  			var defineArrayKey = defineArray[i];
  			var defineVal = this.defines[defineArrayKey];
  			if (defineVal === undefined || defineVal === false) {
  				continue;
  			}
  			if (definesIndices.indexOf(defineArrayKey) === -1) {
  				definesIndices.push(defineArrayKey);
  			}
  		}
  		for (var i = 0, l = definesIndices.length; i < l; i++) {
  			var defineArrayKey = definesIndices[i];
  			var defineVal = this.defines[defineArrayKey];
  			if (defineVal === undefined || defineVal === false) {
  				continue;
  			}
  			key += '_' + i + ':' + defineVal;
  		}
  		this.defineKey = key;
  		this.defineKeyDirty = false;
  	}
  
  	return this.defineKey;
  };
  
  
  Shader.prototype.rebuild = function () {
  	this.shaderProgram = null;
  	this.attributeMapping = {};
  	this.attributeIndexMapping = {};
  	this.uniformMapping = {};
  	this.uniformCallMapping = {};
  	this.currentCallbacks = {};
  	this.attributeKeys = null;
  	this.vertexSource = typeof this.origVertexSource === 'function' ? this.origVertexSource() : this.origVertexSource;
  	this.fragmentSource = typeof this.origFragmentSource === 'function' ? this.origFragmentSource() : this.origFragmentSource;
  	this.defineKeyDirty = true;
  };
  
  Shader.prototype._investigateShaders = function () {
  	this.textureSlots = [];
  	this.textureSlotsNaming = {};
  	Shader.investigateShader(this.vertexSource, this);
  	Shader.investigateShader(this.fragmentSource, this);
  };
  
  /**
   * Extract shader variable definitions from shader source code.
   * @param {string} source The source code.
   * @param {Object} target
   * @param {Object} target.attributeMapping
   * @param {Object} target.uniformMapping
   * @param {Array} target.textureSlots
   */
  Shader.investigateShader = function (source, target) {
  	regExp.lastIndex = 0;
  	var matcher = regExp.exec(source);
  
  	while (matcher !== null) {
  		var definition = {
  			// data type: float, int, ...
  			format: matcher[2]
  		};
  		var type = matcher[1];  // "attribute" or "uniform"
  		var variableName = matcher[3];
  		var arrayDeclaration = matcher[4];
  		if (arrayDeclaration) {
  			if (definition.format === 'float') {
  				definition.format = 'floatarray';
  			} else if (definition.format === 'int') {
  				definition.format = 'intarray';
  			} else if (definition.format.indexOf('sampler') === 0) {
  				definition.format = 'samplerArray';
  			}
  		}
  
  		if (type === 'attribute') {
  			target.attributeMapping[variableName] = definition;
  		} else {
  			if (definition.format.indexOf('sampler') === 0) {
  				var textureSlot = {
  					format: definition.format,
  					name: variableName,
  					mapping: target.uniforms[variableName],
  					index: target.textureSlots.length
  				};
  				target.textureSlots.push(textureSlot);
  				target.textureSlotsNaming[textureSlot.name] = textureSlot;
  			}
  			target.uniformMapping[variableName] = definition;
  		}
  
  		matcher = regExp.exec(source);
  	}
  };
  
  Shader.prototype.compile = function (renderer) {
  	var context = renderer.context;
  	this.renderer = renderer;
  	this.vertexShader = this._getShader(context, context.VERTEX_SHADER !== undefined ? context.VERTEX_SHADER : WebGLRenderingContext.VERTEX_SHADER, this.vertexSource);
  	this.fragmentShader = this._getShader(context, context.FRAGMENT_SHADER !== undefined ? context.FRAGMENT_SHADER : WebGLRenderingContext.FRAGMENT_SHADER, this.fragmentSource);
  
  	if (this.vertexShader === null || this.fragmentShader === null) {
  		console.error('Shader error - no shaders');
  	}
  
  	this.shaderProgram = context.createProgram();
  
  	var error = context.getError();
  	if (this.shaderProgram === null || error !== 0) {
  		console.error('Shader error: ' + error + ' [shader: ' + this.name + ']');
  		SystemBus.emit('goo.shader.error');
  	}
  
  	context.attachShader(this.shaderProgram, this.vertexShader);
  	context.attachShader(this.shaderProgram, this.fragmentShader);
  
  	// Link the Shader Program
  	context.linkProgram(this.shaderProgram);
  	if (!context.getProgramParameter(this.shaderProgram, (context.LINK_STATUS !== undefined ? context.LINK_STATUS : WebGLRenderingContext.LINK_STATUS))) {
  		var errInfo = context.getProgramInfoLog(this.shaderProgram);
  		console.error('Could not initialise shaders: ' + errInfo);
  		SystemBus.emit('goo.shader.error', errInfo);
  	}
  
  	for (var key in this.attributeMapping) {
  		var attributeIndex = context.getAttribLocation(this.shaderProgram, key);
  		if (attributeIndex === -1) {
  			continue;
  		}
  
  		this.attributeIndexMapping[key] = attributeIndex;
  	}
  
  	for (var key in this.uniformMapping) {
  		var uniform = context.getUniformLocation(this.shaderProgram, key);
  
  		if (!uniform) {
  			var l = this.textureSlots.length;
  			for (var i = 0; i < l; i++) {
  				var slot = this.textureSlots[i];
  				if (slot.name === key) {
  					this.textureSlots.splice(i, 1);
  					delete this.textureSlotsNaming[slot.name];
  					for (; i < l - 1; i++) {
  						this.textureSlots[i].index--;
  					}
  					break;
  				}
  			}
  			continue;
  		}
  
  		this.uniformCallMapping[key] = new ShaderCall(context, uniform, this.uniformMapping[key].format);
  	}
  
  	if (this.attributes) {
  		this.attributeKeys = Object.keys(this.attributes);
  	}
  
  	if (this.uniforms) {
  		this.matchedUniforms = [];
  		for (var name in this.uniforms) {
  			var mapping = this.uniformCallMapping[name];
  			if (mapping !== undefined) {
  				this.matchedUniforms.push(name);
  			}
  
  			var value = this.uniforms[name];
  			if (this.defaultCallbacks[value]) {
  				this.currentCallbacks[name] = this.defaultCallbacks[value];
  			}
  		}
  	}
  };
  
  var errorRegExp = /\b\d+:(\d+):\s(.+)\b/g;
  var errorRegExpIE = /\((\d+),\s*\d+\):\s(.+)/g;
  
  Shader.prototype._getShader = function (context, type, source) {
  	var shader = context.createShader(type);
  
  	context.shaderSource(shader, source);
  	context.compileShader(shader);
  
  	// check if the Shader is successfully compiled
  	if (!context.getShaderParameter(shader, context.COMPILE_STATUS !== undefined ? context.COMPILE_STATUS : WebGLRenderingContext.COMPILE_STATUS)) {
  		var infoLog = context.getShaderInfoLog(shader);
  		var shaderType = type === (context.VERTEX_SHADER !== undefined ? context.VERTEX_SHADER : WebGLRenderingContext.VERTEX_SHADER) ? 'VertexShader' : 'FragmentShader';
  
  		errorRegExp.lastIndex = 0;
  		var errorMatcher = errorRegExp.exec(infoLog);
  		if (errorMatcher === null) {
  			errorMatcher = errorRegExpIE.exec(infoLog);
  		}
  		if (errorMatcher !== null) {
  			while (errorMatcher !== null) {
  				var splitSource = source.split('\n');
  				var lineNum = errorMatcher[1];
  				var errorStr = errorMatcher[2];
  				console.error('Error in ' + shaderType + ' - [' + this.name + '][' + this._id + '] at line ' + lineNum + ':');
  				console.error('\tError: ' + errorStr);
  				console.error('\tSource: ' + splitSource[lineNum - 1]);
  				errorMatcher = errorRegExp.exec(infoLog);
  			}
  		} else {
  			console.error('Error in ' + shaderType + ' - [' + this.name + '][' + this._id + '] ' + infoLog);
  		}
  
  		return null;
  	}
  
  	return shader;
  };
  
  var precisionRegExp = /\bprecision\s+(lowp|mediump|highp)\s+(float|int);/g;
  
  Shader.prototype.addPrecision = function (precision) {
  	precisionRegExp.lastIndex = 0;
  	var vertMatcher = precisionRegExp.exec(this.vertexSource);
  	if (vertMatcher === null) {
  		this.vertexSource = 'precision ' + precision + ' float;' + '\n' + this.vertexSource;
  	}
  	precisionRegExp.lastIndex = 0;
  	var fragMatcher = precisionRegExp.exec(this.fragmentSource);
  	if (fragMatcher === null) {
  		this.fragmentSource = 'precision ' + precision + ' float;' + '\n' + this.fragmentSource;
  	}
  };
  
  Shader.prototype.addDefines = function (defines) {
  	if (!defines) {
  		return;
  	}
  
  	var defineStr = this.generateDefines(defines);
  
  	this.vertexSource = defineStr + '\n' + this.vertexSource;
  	this.fragmentSource = defineStr + '\n' + this.fragmentSource;
  };
  
  Shader.prototype.generateDefines = function (defines) {
  	var chunks = [];
  	for (var d in defines) {
  		var value = defines[d];
  		if (value === false || value === undefined) {
  			continue;
  		}
  
  		var chunk = '#define ' + d + ' ' + value;
  		chunks.push(chunk);
  	}
  
  	return chunks.join('\n');
  };
  
  function setupDefaultCallbacks(defaultCallbacks) {
  	defaultCallbacks[Shader.PROJECTION_MATRIX] = function (uniformCall, shaderInfo) {
  		var matrix = shaderInfo.camera.getProjectionMatrix();
  		uniformCall.uniformMatrix4fv(matrix);
  	};
  	defaultCallbacks[Shader.VIEW_MATRIX] = function (uniformCall, shaderInfo) {
  		var matrix = shaderInfo.camera.getViewMatrix();
  		uniformCall.uniformMatrix4fv(matrix);
  	};
  	defaultCallbacks[Shader.WORLD_MATRIX] = function (uniformCall, shaderInfo) {
  		//! AT: when is this condition ever true?
  		var matrix = shaderInfo.transform !== undefined ? shaderInfo.transform.matrix : Matrix4.IDENTITY;
  		uniformCall.uniformMatrix4fv(matrix);
  	};
  	defaultCallbacks[Shader.NORMAL_MATRIX] = function (uniformCall, shaderInfo) {
  		//! AT: when is this condition ever true?
  		var matrix = shaderInfo.transform !== undefined ? shaderInfo.transform.normalMatrix : Matrix3.IDENTITY;
  		uniformCall.uniformMatrix3fv(matrix);
  	};
  
  	defaultCallbacks[Shader.VIEW_INVERSE_MATRIX] = function (uniformCall, shaderInfo) {
  		var matrix = shaderInfo.camera.getViewInverseMatrix();
  		uniformCall.uniformMatrix4fv(matrix);
  	};
  	defaultCallbacks[Shader.VIEW_PROJECTION_MATRIX] = function (uniformCall, shaderInfo) {
  		var matrix = shaderInfo.camera.getViewProjectionMatrix();
  		uniformCall.uniformMatrix4fv(matrix);
  	};
  	defaultCallbacks[Shader.VIEW_PROJECTION_INVERSE_MATRIX] = function (uniformCall, shaderInfo) {
  		var matrix = shaderInfo.camera.getViewProjectionInverseMatrix();
  		uniformCall.uniformMatrix4fv(matrix);
  	};
  
  	for (var i = 0; i < 16; i++) {
  		defaultCallbacks[Shader['TEXTURE' + i]] = (function (i) {
  			return function (uniformCall) {
  				uniformCall.uniform1i(i);
  			};
  		})(i);
  	}
  
  	for (var i = 0; i < 8; i++) {
  		defaultCallbacks[Shader['LIGHT' + i]] = (function (i) {
  			return function (uniformCall, shaderInfo) {
  				var light = shaderInfo.lights[i];
  				if (light !== undefined) {
  					uniformCall.uniform3f(light.translation.x, light.translation.y, light.translation.z);
  				} else {
  					uniformCall.uniform3f(-20, 20, 20);
  				}
  			};
  		})(i);
  	}
  	defaultCallbacks[Shader.LIGHTCOUNT] = function (uniformCall, shaderInfo) {
  		uniformCall.uniform1i(shaderInfo.lights.length);
  	};
  
  	defaultCallbacks[Shader.CAMERA] = function (uniformCall, shaderInfo) {
  		var cameraPosition = shaderInfo.camera.translation;
  		uniformCall.uniform3f(cameraPosition.x, cameraPosition.y, cameraPosition.z);
  	};
  	defaultCallbacks[Shader.NEAR_PLANE] = function (uniformCall, shaderInfo) {
  		uniformCall.uniform1f(shaderInfo.camera.near);
  	};
  	defaultCallbacks[Shader.FAR_PLANE] = function (uniformCall, shaderInfo) {
  		uniformCall.uniform1f(shaderInfo.camera.far);
  	};
  	defaultCallbacks[Shader.MAIN_NEAR_PLANE] = function (uniformCall, shaderInfo) {
  		uniformCall.uniform1f(shaderInfo.mainCamera.near);
  	};
  	defaultCallbacks[Shader.MAIN_FAR_PLANE] = function (uniformCall, shaderInfo) {
  		uniformCall.uniform1f(shaderInfo.mainCamera.far);
  	};
  	defaultCallbacks[Shader.MAIN_DEPTH_SCALE] = function (uniformCall, shaderInfo) {
  		uniformCall.uniform1f(1.0 / (shaderInfo.mainCamera.far - shaderInfo.mainCamera.near));
  	};
  
  
  	defaultCallbacks[Shader.AMBIENT] = function (uniformCall, shaderInfo) {
  		var materialState = shaderInfo.material.materialState !== undefined ? shaderInfo.material.materialState.ambient : Shader.DEFAULT_AMBIENT;
  		uniformCall.uniform4fv(materialState);
  	};
  	defaultCallbacks[Shader.EMISSIVE] = function (uniformCall, shaderInfo) {
  		var materialState = shaderInfo.material.materialState !== undefined ? shaderInfo.material.materialState.emissive : Shader.DEFAULT_EMISSIVE;
  		uniformCall.uniform4fv(materialState);
  	};
  	defaultCallbacks[Shader.DIFFUSE] = function (uniformCall, shaderInfo) {
  		var materialState = shaderInfo.material.materialState !== undefined ? shaderInfo.material.materialState.diffuse : Shader.DEFAULT_DIFFUSE;
  		uniformCall.uniform4fv(materialState);
  	};
  	defaultCallbacks[Shader.SPECULAR] = function (uniformCall, shaderInfo) {
  		var materialState = Shader.DEFAULT_SPECULAR;
  		if (shaderInfo.material.materialState !== undefined) {
  			materialState = shaderInfo.material.materialState.specular;
  			materialState[3] = Math.max(shaderInfo.material.materialState.shininess, 1);
  		}
  		uniformCall.uniform4fv(materialState);
  	};
  	defaultCallbacks[Shader.SPECULAR_POWER] = function (uniformCall, shaderInfo) {
  		var shininess = shaderInfo.material.materialState !== undefined ? shaderInfo.material.materialState.shininess : Shader.DEFAULT_SHININESS;
  		shininess = Math.max(shininess, 1.0);
  		uniformCall.uniform1f(shininess);
  	};
  
  	defaultCallbacks[Shader.TIME] = function (uniformCall) {
  		uniformCall.uniform1f(World.time);
  	};
  	defaultCallbacks[Shader.TPF] = function (uniformCall) {
  		uniformCall.uniform1f(World.tpf);
  	};
  
  	defaultCallbacks[Shader.RESOLUTION] = function (uniformCall, shaderInfo) {
  		uniformCall.uniform2f(shaderInfo.renderer.viewportWidth, shaderInfo.renderer.viewportHeight);
  	};
  }
  
  Shader.prototype.getShaderDefinition = function () {
  	return {
  		vshader: this.vertexSource,
  		fshader: this.fragmentSource,
  		defines: this.defines,
  		attributes: this.attributes,
  		uniforms: this.uniforms
  	};
  };
  
  Shader.prototype.destroy = function () {
  	if (this.shaderProgram) {
  		this.renderer.context.deleteProgram(this.shaderProgram);
  		this.shaderProgram = null;
  	}
  	if (this.vertexShader) {
  		this.renderer.context.deleteShader(this.vertexShader);
  		this.vertexShader = null;
  	}
  	if (this.fragmentShader) {
  		this.renderer.context.deleteShader(this.fragmentShader);
  		this.fragmentShader = null;
  	}
  	this.renderer = null;
  };
  
  Shader.prototype.toString = function () {
  	return this.name;
  };
  
  Shader.PROJECTION_MATRIX = 'PROJECTION_MATRIX';
  Shader.VIEW_MATRIX = 'VIEW_MATRIX';
  Shader.VIEW_INVERSE_MATRIX = 'VIEW_INVERSE_MATRIX';
  Shader.VIEW_PROJECTION_MATRIX = 'VIEW_PROJECTION_MATRIX';
  Shader.VIEW_PROJECTION_INVERSE_MATRIX = 'VIEW_PROJECTION_INVERSE_MATRIX';
  Shader.WORLD_MATRIX = 'WORLD_MATRIX';
  Shader.NORMAL_MATRIX = 'NORMAL_MATRIX';
  for (var i = 0; i < 8; i++) {
  	Shader['LIGHT' + i] = 'LIGHT' + i;
  }
  Shader.CAMERA = 'CAMERA';
  Shader.AMBIENT = 'AMBIENT';
  Shader.EMISSIVE = 'EMISSIVE';
  Shader.DIFFUSE = 'DIFFUSE';
  Shader.SPECULAR = 'SPECULAR';
  Shader.SPECULAR_POWER = 'SPECULAR_POWER';
  Shader.NEAR_PLANE = 'NEAR_PLANE';
  Shader.FAR_PLANE = 'FAR_PLANE';
  Shader.MAIN_NEAR_PLANE = 'MAIN_NEAR_PLANE';
  Shader.MAIN_FAR_PLANE = 'MAIN_FAR_PLANE';
  Shader.MAIN_DEPTH_SCALE = 'DEPTH_SCALE';
  Shader.TIME = 'TIME';
  Shader.TPF = 'TPF';
  Shader.RESOLUTION = 'RESOLUTION';
  
  Shader.DIFFUSE_MAP = 'DIFFUSE_MAP';
  Shader.NORMAL_MAP = 'NORMAL_MAP';
  Shader.SPECULAR_MAP = 'SPECULAR_MAP';
  Shader.LIGHT_MAP = 'LIGHT_MAP';
  Shader.SHADOW_MAP = 'SHADOW_MAP';
  Shader.AO_MAP = 'AO_MAP';
  Shader.EMISSIVE_MAP = 'EMISSIVE_MAP';
  Shader.DEPTH_MAP = 'DEPTH_MAP';
  
  Shader.DEFAULT_AMBIENT = [0.1, 0.1, 0.1, 1.0];
  Shader.DEFAULT_EMISSIVE = [0, 0, 0, 0];
  Shader.DEFAULT_DIFFUSE = [0.8, 0.8, 0.8, 1.0];
  Shader.DEFAULT_SPECULAR = [0.6, 0.6, 0.6, 64.0];
  Shader.DEFAULT_SHININESS = 64.0;
  
  Shader.prototype.defaultCallbacks = {};
  setupDefaultCallbacks(Shader.prototype.defaultCallbacks);
  
  module.exports = Shader;
  