/**
 * @author mrdoob / http://mrdoob.com/
 *
 * parameters = {
 *   canvas: canvas,
 *   contextAttributes: {
 *     alpha: true,
 *     depth: true,
 *     stencil: false,
 *     antialias: true,
 *     premultipliedAlpha: true,
 *     preserveDrawingBuffer: false
 *   }
 * }
 *
 */

THREE.WebGLRenderer3 = function ( parameters ) {

	console.log( 'THREE.WebGLRenderer3', THREE.REVISION );

	parameters = parameters || {};

	var scope = this;

	var canvas = parameters.canvas !== undefined ? parameters.canvas : document.createElement( 'canvas' );

	var devicePixelRatio = parameters.devicePixelRatio !== undefined
				? parameters.devicePixelRatio
				: window.devicePixelRatio !== undefined
					? window.devicePixelRatio
					: 1;

	var gl;

	try {

		var attributes = parameters.contextAttributes || {};

		gl = canvas.getContext( 'webgl', attributes ) || canvas.getContext( 'experimental-webgl', attributes );

	} catch ( exception ) {

		console.error( exception );

	}

	var precision = 'highp';
	var extensions = {};

	if ( gl !== null ) {

		extensions.element_index_uint = gl.getExtension( 'OES_element_index_uint' );
		extensions.texture_float = gl.getExtension( 'OES_texture_float' );
		extensions.texture_float_linear = gl.getExtension( 'OES_texture_float_linear' );
		extensions.standard_derivatives = gl.getExtension( 'OES_standard_derivatives' );
		extensions.texture_filter_anisotropic = gl.getExtension( 'EXT_texture_filter_anisotropic' ) || gl.getExtension( 'MOZ_EXT_texture_filter_anisotropic' ) || gl.getExtension( 'WEBKIT_EXT_texture_filter_anisotropic' );
		extensions.compressed_texture_s3tc = gl.getExtension( 'WEBGL_compressed_texture_s3tc' ) || gl.getExtension( 'MOZ_WEBGL_compressed_texture_s3tc' ) || gl.getExtension( 'WEBKIT_WEBGL_compressed_texture_s3tc' );

		gl.clearColor( 0, 0, 0, 1 );
		gl.clearDepth( 1 );
		gl.clearStencil( 0 );

		gl.enable( gl.DEPTH_TEST );
		gl.depthFunc( gl.LEQUAL );

		gl.enable( gl.CULL_FACE );
		gl.frontFace( gl.CCW );
		gl.cullFace( gl.BACK );

		gl.enable( gl.BLEND );
		gl.blendEquation( gl.FUNC_ADD );
		gl.blendFunc( gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA );

		gl.clearColor( 0, 0, 0, 0 );

	}

	var clearColor = new THREE.Color( 0x000000 );
	var clearAlpha = 0;

	//

	var vector3 = new THREE.Vector3();
	var frustum = new THREE.Frustum();
	var normalMatrix = new THREE.Matrix3();
	var modelViewMatrix = new THREE.Matrix4();
	var cameraViewProjectionMatrix = new THREE.Matrix4();

	// buffers

	var buffers = {};

	var getBuffer = function ( geometry, material ) {

		var hash = geometry.id.toString() + '+' + material.id.toString();

		if ( buffers[ hash ] !== undefined ) {

			return buffers[ hash ];

		}

		var vertices = geometry.vertices;
		var faces = geometry.faces;

		//

		var positions = [];
		var addPosition = function ( position ) {

			positions.push( position.x, position.y, position.z );

		}

		var normals = [];
		var addNormal = function ( normal ) {

			normals.push( normal.x, normal.y, normal.z );

		}

		for ( var i = 0, l = faces.length; i < l; i ++ ) {

			var face = faces[ i ];
			var vertexNormals = face.vertexNormals.length > 0;

			addPosition( vertices[ face.a ] );
			addPosition( vertices[ face.b ] );
			addPosition( vertices[ face.c ] );

			if ( vertexNormals === true ) {

				addNormal( face.vertexNormals[ 0 ] );
				addNormal( face.vertexNormals[ 1 ] );
				addNormal( face.vertexNormals[ 2 ] );

			} else {

				addNormal( face.normal );
				addNormal( face.normal );
				addNormal( face.normal );

			}

			if ( face instanceof THREE.Face4 ) {

				addPosition( vertices[ face.a ] );
				addPosition( vertices[ face.c ] );
				addPosition( vertices[ face.d ] );

				if ( vertexNormals === true ) {

					addNormal( face.vertexNormals[ 0 ] );
					addNormal( face.vertexNormals[ 2 ] );
					addNormal( face.vertexNormals[ 3 ] );

				} else {

					addNormal( face.normal );
					addNormal( face.normal );
					addNormal( face.normal );

				}

			}

		}

		var buffer = {
			positions: gl.createBuffer(),
			normals: gl.createBuffer(),
			count: positions.length / 3
		};

		gl.bindBuffer( gl.ARRAY_BUFFER, buffer.positions );
		gl.bufferData( gl.ARRAY_BUFFER, new Float32Array( positions ), gl.STATIC_DRAW );

		gl.bindBuffer( gl.ARRAY_BUFFER, buffer.normals );
		gl.bufferData( gl.ARRAY_BUFFER, new Float32Array( normals ), gl.STATIC_DRAW );

		buffers[ hash ] = buffer;

		scope.info.memory.geometries ++;

		return buffer;

	};

	// programs

	var programs = {};
	var programsCache = {};

	var getProgram = function ( material ) {

		if ( programs[ material.id ] !== undefined ) {

			return programs[ material.id ];

		}

		var vertexShader = [
			'precision ' + precision + ' float;',
			'attribute vec3 position;',
			'attribute vec3 normal;',
			'uniform mat4 modelViewMatrix;',
			'uniform mat3 normalMatrix;',
			'uniform mat4 projectionMatrix;',
			''
		].join( '\n' );

		var fragmentShader = [
			'precision ' + precision + ' float;',
			''
		].join( '\n' );

		if ( material instanceof THREE.ShaderMaterial ) {

			vertexShader += material.vertexShader;
			fragmentShader += material.fragmentShader;

		} else if ( material instanceof THREE.MeshNormalMaterial ) {

			vertexShader += [
				'varying vec3 vNormal;',
				'void main() {',
				'	vNormal = normalize( normalMatrix * normal );',
				'	gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );',
				'}'
			].join( '\n' );

			fragmentShader += [
				'varying vec3 vNormal;',
				'uniform float opacity;',
				'void main() {',
				'	gl_FragColor = vec4( 0.5 * normalize( vNormal ) + 0.5, opacity );',
				'}'
			].join( '\n' );

		} else {

			vertexShader += [
				'void main() {',
				'	gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );',
				'}'
			].join( '\n' );

			fragmentShader += [
				'void main() {',
				'	gl_FragColor = vec4( 1.0, 0, 0, 1.0 );',
				'}'
			].join( '\n' );

		}

		var program;
		var code = vertexShader + fragmentShader;

		if ( programsCache[ code ] !== undefined ) {

			program = programsCache[ code ];
			programs[ material.id ] = program;

		} else {

			program = gl.createProgram();

			gl.attachShader( program, createShader( gl.VERTEX_SHADER, vertexShader ) );
			gl.attachShader( program, createShader( gl.FRAGMENT_SHADER, fragmentShader ) );
			gl.linkProgram( program );

			if ( gl.getProgramParameter( program, gl.LINK_STATUS ) === true ) {

				programsCache[ code ] = program;
				programs[ material.id ] = program;

				scope.info.memory.programs ++;

			} else {

				console.error( 'VALIDATE_STATUS: ' + gl.getProgramParameter( program, gl.VALIDATE_STATUS ) );
				console.error( 'GL_ERROR: ' + gl.getError() );

				// fallback

				program = getProgram( new THREE.MeshBasicMaterial() );
				programs[ material.id ] = program;

			}

		}

		return program;

	};

	var createShader = function ( type, string ) {

		var shader = gl.createShader( type );

		gl.shaderSource( shader, string );
		gl.compileShader( shader );

		if ( gl.getShaderParameter( shader, gl.COMPILE_STATUS ) === true ) {

			// console.log( string );

		} else {

			console.error( gl.getShaderInfoLog( shader ), string );
			return null;

		}

		return shader;

	};

	this.info = {

		memory: {

			programs: 0,
			geometries: 0,
			textures: 0

		},

		render: {

			calls: 0,
			vertices: 0,
			faces: 0,
			points: 0

		}

	};

	this.domElement = canvas;
	this.extensions = extensions;

	this.autoClear = true; // TODO: Make private

	this.setClearColor = function ( color, alpha ) {

		clearColor.set( color );
		clearAlpha = alpha !== undefined ? alpha : 1;

		gl.clearColor( clearColor.r, clearColor.g, clearColor.b, clearAlpha );

	};

	this.setSize = function ( width, height, updateStyle ) {

		canvas.width = width * devicePixelRatio;
		canvas.height = height * devicePixelRatio;

		if ( devicePixelRatio !== 1 && updateStyle !== false ) {

			canvas.style.width = width + 'px';
			canvas.style.height = height + 'px';

		}

		gl.viewport( 0, 0, canvas.width, canvas.height );

	};

	this.clear = function ( color, depth, stencil ) {

		var bits = 0;

		if ( color === undefined || color ) bits |= gl.COLOR_BUFFER_BIT;
		if ( depth === undefined || depth ) bits |= gl.DEPTH_BUFFER_BIT;
		if ( stencil === undefined || stencil ) bits |= gl.STENCIL_BUFFER_BIT;

		gl.clear( bits );

	};

	// blending

	var currentBlending = null;

	var setBlending = function ( blending ) {

		if ( blending !== currentBlending ) {

			if ( blending === THREE.NoBlending ) {

				gl.disable( gl.BLEND );

			} else {

				gl.enable( gl.BLEND );

			}

			currentBlending = blending;

		}

	};

	// depthTest

	var currentDepthTest = null;

	var setDepthTest = function ( value ) {

		if ( value !== currentDepthTest ) {

			value === true ? gl.enable( gl.DEPTH_TEST ) : gl.disable( gl.DEPTH_TEST );
			currentDepthTest = value;

		}

	};

	// depthWrite

	var currentDepthWrite = null;

	var setDepthWrite = function ( value ) {

		if ( value !== currentDepthWrite ) {

			gl.depthMask( value );
			currentDepthWrite = value;

		}

	};

	var objectsOpaque = [];
	var objectsTransparent = [];

	var projectObject = function ( object, camera ) {

		if ( object.visible === false ) return;

		if ( object instanceof THREE.Mesh && frustum.intersectsObject( object ) === true ) {

			// TODO: Do not polute scene graph with .z

			if ( object.renderDepth !== null ) {

				object.z = object.renderDepth;

			} else {

				vector3.getPositionFromMatrix( object.matrixWorld );
				vector3.applyProjection( cameraViewProjectionMatrix );

				object.z = vector3.z;

			}

			if ( object.material.transparent === true ) {

				objectsTransparent.push( object );

			} else {

				objectsOpaque.push( object );

			}

		}

		for ( var i = 0, l = object.children.length; i < l; i ++ ) {

			projectObject( object.children[ i ], camera );

		}

	};

	var sortOpaque = function ( a, b ) {

		return a.z - b.z;

	};

	var sortTransparent = function ( a, b ) {

		return a.z !== b.z ? b.z - a.z : b.id - a.id;

	};

	var currentBuffer, currentMaterial, currentProgram;
	var locations = {};

	var renderObject = function ( object, camera ) {

		var buffer = getBuffer( object.geometry, object.material );

		var material = object.material;

		if ( material !== currentMaterial ) {

			var program = getProgram( object.material );

			if ( program !== currentProgram ) {

				gl.useProgram( program );

				locations.modelViewMatrix = gl.getUniformLocation( program, 'modelViewMatrix' );
				locations.normalMatrix = gl.getUniformLocation( program, 'normalMatrix' );
				locations.projectionMatrix = gl.getUniformLocation( program, 'projectionMatrix' );

				locations.position = gl.getAttribLocation( program, 'position' );
				locations.normal = gl.getAttribLocation( program, 'normal' );

				gl.uniformMatrix4fv( locations.projectionMatrix, false, camera.projectionMatrix.elements );

				currentProgram = program;

			}

			if ( material instanceof THREE.MeshNormalMaterial ) {

				gl.uniform1f( gl.getUniformLocation( program, 'opacity' ), material.opacity );

			} else if ( material instanceof THREE.ShaderMaterial ) {

				var uniforms = material.uniforms;

				for ( var uniform in uniforms ) {

					var location = gl.getUniformLocation( program, uniform );

					var type = uniforms[ uniform ].type;
					var value = uniforms[ uniform ].value;

					if ( type === "i" ) { // single integer

						gl.uniform1i( location, value );

					} else if ( type === "f" ) { // single float

						gl.uniform1f( location, value );

					} else if ( type === "v2" ) { // single THREE.Vector2

						gl.uniform2f( location, value.x, value.y );

					} else if ( type === "v3" ) { // single THREE.Vector3

						gl.uniform3f( location, value.x, value.y, value.z );

					} else if ( type === "v4" ) { // single THREE.Vector4

						gl.uniform4f( location, value.x, value.y, value.z, value.w );

					} else if ( type === "c" ) { // single THREE.Color

						gl.uniform3f( location, value.r, value.g, value.b );

					}

				}

			}

			currentMaterial = material;

		}

		if ( buffer !== currentBuffer ) {

			gl.bindBuffer( gl.ARRAY_BUFFER, buffer.positions );
			gl.enableVertexAttribArray( locations.position );
			gl.vertexAttribPointer( locations.position, 3, gl.FLOAT, false, 0, 0 );

			if ( locations.normal >= 0 ) {

				gl.bindBuffer( gl.ARRAY_BUFFER, buffer.normals );
				gl.enableVertexAttribArray( locations.normal );
				gl.vertexAttribPointer( locations.normal, 3, gl.FLOAT, false, 0, 0 );

			}

			currentBuffer = buffer;

		}

		modelViewMatrix.multiplyMatrices( camera.matrixWorldInverse, object.matrixWorld );
		normalMatrix.getNormalMatrix( modelViewMatrix );

		gl.uniformMatrix4fv( locations.modelViewMatrix, false, modelViewMatrix.elements );
		gl.uniformMatrix3fv( locations.normalMatrix, false, normalMatrix.elements );

		gl.drawArrays( gl.TRIANGLES, 0, buffer.count );

		scope.info.render.calls ++;

	};

	this.render = function ( scene, camera ) {

		if ( this.autoClear === true ) this.clear();

		scene.updateMatrixWorld();

		if ( camera.parent === undefined ) camera.updateMatrixWorld();

		camera.matrixWorldInverse.getInverse( camera.matrixWorld );

		cameraViewProjectionMatrix.multiplyMatrices( camera.projectionMatrix, camera.matrixWorldInverse );
		frustum.setFromMatrix( cameraViewProjectionMatrix );

		objectsOpaque.length = 0;
		objectsTransparent.length = 0;

		scope.info.render.calls = 0;

		currentBuffer = undefined;
		currentMaterial = undefined;
		currentProgram = undefined;

		projectObject( scene, camera );

		if ( objectsOpaque.length > 0 ) {

			objectsOpaque.sort( sortOpaque );

			setBlending( THREE.NoBlending );

			for ( var i = 0, l = objectsOpaque.length; i < l; i ++ ) {

				renderObject( objectsOpaque[ i ], camera );

			}

		}

		if ( objectsTransparent.length > 0 ) {

			objectsTransparent.sort( sortTransparent );

			setBlending( THREE.NormalBlending );

			for ( var i = 0, l = objectsTransparent.length; i < l; i ++ ) {

				renderObject( objectsTransparent[ i ], camera );

			}

		}

	};

};
