/**
 * Loader for CTM encoded models generated by OpenCTM tools:
 *	http://openctm.sourceforge.net/
 *
 * Uses js-openctm library by Juan Mellado
 *	http://code.google.com/p/js-openctm/
 *
 * @author alteredq / http://alteredqualia.com/
 */

THREE.CTMLoader = function ( showStatus ) {

	THREE.Loader.call( this, showStatus );

};

THREE.CTMLoader.prototype = Object.create( THREE.Loader.prototype );

// Load multiple CTM parts defined in JSON

THREE.CTMLoader.prototype.loadParts = function( url, callback, parameters ) {

	var scope = this;

	var xhr = new XMLHttpRequest();

	var basePath = parameters.basePath ? parameters.basePath : this.extractUrlBase( url );

	xhr.onreadystatechange = function() {

		if ( xhr.readyState === 4 ) {

			if ( xhr.status === 200 || xhr.status === 0 ) {

				var jsonObject = JSON.parse( xhr.responseText );

				var materials = [], geometries = [], counter = 0;

				function callbackFinal( geometry ) {

					counter += 1;

					geometries.push( geometry );

					if ( counter === jsonObject.offsets.length ) {

						callback( geometries, materials );

					}

				}


				// init materials

				for ( var i = 0; i < jsonObject.materials.length; i ++ ) {

					materials[ i ] = THREE.Loader.prototype.createMaterial( jsonObject.materials[ i ], basePath );

				}

				// load joined CTM file

				var partUrl = basePath + jsonObject.data;
				var parametersPart = { useWorker: parameters.useWorker, useBuffers: parameters.useBuffers, offsets: jsonObject.offsets };
				scope.load( partUrl, callbackFinal, parametersPart );

			}

		}

	}

	xhr.open( "GET", url, true );
	if ( xhr.overrideMimeType ) xhr.overrideMimeType( "text/plain; charset=x-user-defined" );
	xhr.setRequestHeader( "Content-Type", "text/plain" );
	xhr.send( null );

};

// Load CTMLoader compressed models
//  - parameters
//		- url (required)
//		- callback (required)

THREE.CTMLoader.prototype.load = function( url, callback, parameters ) {

	var scope = this;

	var offsets = parameters.offsets !== undefined ? parameters.offsets : [ 0 ];
	var useBuffers = parameters.useBuffers !== undefined ? parameters.useBuffers : true;

	var xhr = new XMLHttpRequest(),
		callbackProgress = null;

	var length = 0;

	xhr.onreadystatechange = function() {

		if ( xhr.readyState === 4 ) {

			if ( xhr.status === 200 || xhr.status === 0 ) {

				var binaryData = xhr.responseText;

				//var s = Date.now();

				if ( parameters.useWorker ) {

					var worker = new Worker( "js/loaders/ctm/CTMWorker.js" );

					worker.onmessage = function( event ) {

						var files = event.data;

						for ( var i = 0; i < files.length; i ++ ) {

							var ctmFile = files[ i ];

							if ( useBuffers ) {

								scope.createModelBuffers( ctmFile, callback );

							} else {

								scope.createModelClassic( ctmFile, callback );

							}

						}

						//var e = Date.now();
						//console.log( "CTM data parse time [worker]: " + (e-s) + " ms" );

					};

					worker.postMessage( { "data": binaryData, "offsets": offsets } );

				} else {

					for ( var i = 0; i < offsets.length; i ++ ) {

						var stream = new CTM.Stream( binaryData );
						stream.offset = offsets[ i ];

						var ctmFile = new CTM.File( stream );

						if ( useBuffers ) {

							scope.createModelBuffers( ctmFile, callback );

						} else {

							scope.createModelClassic( ctmFile, callback );

						}

					}

					//var e = Date.now();
					//console.log( "CTM data parse time [inline]: " + (e-s) + " ms" );

				}

			} else {

				console.error( "Couldn't load [" + url + "] [" + xhr.status + "]" );

			}

		} else if ( xhr.readyState === 3 ) {

			if ( callbackProgress ) {

				if ( length === 0 ) {

					length = xhr.getResponseHeader( "Content-Length" );

				}

				callbackProgress( { total: length, loaded: xhr.responseText.length } );

			}

		} else if ( xhr.readyState === 2 ) {

			length = xhr.getResponseHeader( "Content-Length" );

		}

	}

	xhr.overrideMimeType( "text/plain; charset=x-user-defined" );
	xhr.open( "GET", url, true );
	xhr.send( null );

};


THREE.CTMLoader.prototype.createModelBuffers = function ( file, callback ) {

	var Model = function ( ) {

		var scope = this;

		var reorderVertices = true;

		scope.materials = [];

		THREE.BufferGeometry.call( this );

		// init GL buffers

		var vertexIndexArray = file.body.indices,
		vertexPositionArray = file.body.vertices,
		vertexNormalArray = file.body.normals;

		var vertexUvArray, vertexColorArray;

		if ( file.body.uvMaps !== undefined && file.body.uvMaps.length > 0 ) {

			vertexUvArray = file.body.uvMaps[ 0 ].uv;

		}

		if ( file.body.attrMaps !== undefined && file.body.attrMaps.length > 0 && file.body.attrMaps[ 0 ].name === "Color" ) {

			vertexColorArray = file.body.attrMaps[ 0 ].attr;

		}

		// reorder vertices
		// (needed for buffer splitting, to keep together face vertices)

		if ( reorderVertices ) {

			var newFaces = new Uint32Array( vertexIndexArray.length ),
				newVertices = new Float32Array( vertexPositionArray.length );

			var newNormals, newUvs, newColors;

			if ( vertexNormalArray ) newNormals = new Float32Array( vertexNormalArray.length );
			if ( vertexUvArray ) newUvs = new Float32Array( vertexUvArray.length );
			if ( vertexColorArray ) newColors = new Float32Array( vertexColorArray.length );

			var indexMap = {}, vertexCounter = 0;

			function handleVertex( v ) {

				if ( indexMap[ v ] === undefined ) {

					indexMap[ v ] = vertexCounter;

					var sx = v * 3,
						sy = v * 3 + 1,
						sz = v * 3 + 2,

						dx = vertexCounter * 3,
						dy = vertexCounter * 3 + 1,
						dz = vertexCounter * 3 + 2;

					newVertices[ dx ] = vertexPositionArray[ sx ];
					newVertices[ dy ] = vertexPositionArray[ sy ];
					newVertices[ dz ] = vertexPositionArray[ sz ];

					if ( vertexNormalArray ) {

						newNormals[ dx ] = vertexNormalArray[ sx ];
						newNormals[ dy ] = vertexNormalArray[ sy ];
						newNormals[ dz ] = vertexNormalArray[ sz ];

					}

					if ( vertexUvArray ) {

						newUvs[ vertexCounter * 2 ] 	= vertexUvArray[ v * 2 ];
						newUvs[ vertexCounter * 2 + 1 ] = vertexUvArray[ v * 2 + 1 ];

					}

					if ( vertexColorArray ) {

						newColors[ vertexCounter * 4 ] 	   = vertexColorArray[ v * 4 ];
						newColors[ vertexCounter * 4 + 1 ] = vertexColorArray[ v * 4 + 1 ];
						newColors[ vertexCounter * 4 + 2 ] = vertexColorArray[ v * 4 + 2 ];
						newColors[ vertexCounter * 4 + 3 ] = vertexColorArray[ v * 4 + 3 ];

					}

					vertexCounter += 1;

				}

			}

			var a, b, c;

			for ( var i = 0; i < vertexIndexArray.length; i += 3 ) {

				a = vertexIndexArray[ i ];
				b = vertexIndexArray[ i + 1 ];
				c = vertexIndexArray[ i + 2 ];

				handleVertex( a );
				handleVertex( b );
				handleVertex( c );

				newFaces[ i ] 	  = indexMap[ a ];
				newFaces[ i + 1 ] = indexMap[ b ];
				newFaces[ i + 2 ] = indexMap[ c ];

			}

			vertexIndexArray = newFaces;
			vertexPositionArray = newVertices;

			if ( vertexNormalArray ) vertexNormalArray = newNormals;
			if ( vertexUvArray ) vertexUvArray = newUvs;
			if ( vertexColorArray ) vertexColorArray = newColors;

		}

		// compute offsets

		scope.offsets = [];

		var indices = vertexIndexArray;

		var start = 0,
			min = vertexPositionArray.length,
			max = 0,
			minPrev = min;

		for ( var i = 0; i < indices.length; ) {

			for ( var j = 0; j < 3; ++ j ) {

				var idx = indices[ i ++ ];

				if ( idx < min ) min = idx;
				if ( idx > max ) max = idx;

			}

			if ( max - min > 65535 ) {

				i -= 3;

				for ( var k = start; k < i; ++ k ) {

					indices[ k ] -= minPrev;

				}

				scope.offsets.push( { start: start, count: i - start, index: minPrev } );

				start = i;
				min = vertexPositionArray.length;
				max = 0;

			}

			minPrev = min;

		}

		for ( var k = start; k < i; ++ k ) {

			indices[ k ] -= minPrev;

		}

		scope.offsets.push( { start: start, count: i - start, index: minPrev } );

		// recast CTM 32-bit indices as 16-bit WebGL indices

		var vertexIndexArray16 = new Uint16Array( vertexIndexArray );

		// attributes

		var attributes = scope.attributes;

		attributes[ "index" ]    = { itemSize: 1, array: vertexIndexArray16, numItems: vertexIndexArray16.length };
		attributes[ "position" ] = { itemSize: 3, array: vertexPositionArray, numItems: vertexPositionArray.length };

		if ( vertexNormalArray !== undefined ) {

			attributes[ "normal" ] = { itemSize: 3, array: vertexNormalArray, numItems: vertexNormalArray.length };

		}

		if ( vertexUvArray !== undefined ) {

			attributes[ "uv" ] = { itemSize: 2, array: vertexUvArray, numItems: vertexUvArray.length };

		}

		if ( vertexColorArray !== undefined ) {

			attributes[ "color" ]  = { itemSize: 4, array: vertexColorArray, numItems: vertexColorArray.length };

		}

	}

	Model.prototype = Object.create( THREE.BufferGeometry.prototype );

	var geometry = new Model();

	// compute vertex normals if not present in the CTM model

	if ( geometry.attributes[ "normal" ] === undefined ) {

		geometry.computeVertexNormals();

	}

	callback( geometry );

};

THREE.CTMLoader.prototype.createModelClassic = function ( file, callback ) {

	var Model = function ( ) {

		var scope = this;

		scope.materials = [];

		THREE.Geometry.call( this );

		var normals = [],
			uvs = [],
			colors = [];

		init_vertices( file.body.vertices );

		if ( file.body.normals !== undefined )
			init_normals( file.body.normals );

		if ( file.body.uvMaps !== undefined && file.body.uvMaps.length > 0 )
			init_uvs( file.body.uvMaps[ 0 ].uv );

		if ( file.body.attrMaps !== undefined && file.body.attrMaps.length > 0 && file.body.attrMaps[ 0 ].name === "Color" )
			init_colors( file.body.attrMaps[ 0 ].attr );

		var hasNormals = normals.length > 0 ? true : false,
			hasUvs = uvs.length > 0 ? true : false,
			hasColors = colors.length > 0 ? true : false;

		init_faces( file.body.indices );

		this.computeCentroids();
		this.computeFaceNormals();
		//this.computeTangents();

		function init_vertices( buffer ) {

			var x, y, z, i, il = buffer.length;

			for( i = 0; i < il; i += 3 ) {

				x = buffer[ i ];
				y = buffer[ i + 1 ];
				z = buffer[ i + 2 ];

				vertex( scope, x, y, z );

			}

		};

		function init_normals( buffer ) {

			var x, y, z, i, il = buffer.length;

			for( i = 0; i < il; i += 3 ) {

				x = buffer[ i ];
				y = buffer[ i + 1 ];
				z = buffer[ i + 2 ];

				normals.push( x, y, z );

			}

		};

		function init_colors( buffer ) {

			var r, g, b, a, i, il = buffer.length;

			for( i = 0; i < il; i += 4 ) {

				r = buffer[ i ];
				g = buffer[ i + 1 ];
				b = buffer[ i + 2 ];
				a = buffer[ i + 3 ];

				var color = new THREE.Color();
				color.setRGB( r, g, b );

				colors.push( color );

			}

		};


		function init_uvs( buffer ) {

			var u, v, i, il = buffer.length;

			for( i = 0; i < il; i += 2 ) {

				u = buffer[ i ];
				v = buffer[ i + 1 ];

				uvs.push( u, v );

			}

		};

		function init_faces( buffer ) {

			var a, b, c,
				u1, v1, u2, v2, u3, v3,
				m, face,
				i, il = buffer.length;

			m = 0; // all faces defaulting to material 0

			for( i = 0; i < il; i += 3 ) {

				a = buffer[ i ];
				b = buffer[ i + 1 ];
				c = buffer[ i + 2 ];

				if ( hasNormals ){

					face = f3n( scope, normals, a, b, c, m, a, b, c );

				} else {

					face = f3( scope, a, b, c, m );

				}

				if ( hasColors ) {

					face.vertexColors[ 0 ] = colors[ a ];
					face.vertexColors[ 1 ] = colors[ b ];
					face.vertexColors[ 2 ] = colors[ c ];

				}

				if ( hasUvs ) {

					u1 = uvs[ a * 2 ];
					v1 = uvs[ a * 2 + 1 ];

					u2 = uvs[ b * 2 ];
					v2 = uvs[ b * 2 + 1 ];

					u3 = uvs[ c * 2 ];
					v3 = uvs[ c * 2 + 1 ];

					uv3( scope.faceVertexUvs[ 0 ], u1, v1, u2, v2, u3, v3 );

				}

			}

		}

	};

	function vertex ( scope, x, y, z ) {

		scope.vertices.push( new THREE.Vector3( x, y, z ) );

	};

	function f3 ( scope, a, b, c, mi ) {

		var face = new THREE.Face3( a, b, c, null, null, mi );

		scope.faces.push( face );

		return face;

	};

	function f3n ( scope, normals, a, b, c, mi, nai, nbi, nci ) {

		var nax = normals[ nai * 3     ],
			nay = normals[ nai * 3 + 1 ],
			naz = normals[ nai * 3 + 2 ],

			nbx = normals[ nbi * 3     ],
			nby = normals[ nbi * 3 + 1 ],
			nbz = normals[ nbi * 3 + 2 ],

			ncx = normals[ nci * 3     ],
			ncy = normals[ nci * 3 + 1 ],
			ncz = normals[ nci * 3 + 2 ];

		var na = new THREE.Vector3( nax, nay, naz ),
			nb = new THREE.Vector3( nbx, nby, nbz ),
			nc = new THREE.Vector3( ncx, ncy, ncz );

		var face = new THREE.Face3( a, b, c, [ na, nb, nc ], null, mi );

		scope.faces.push( face );

		return face;

	};

	function uv3 ( where, u1, v1, u2, v2, u3, v3 ) {

		var uv = [];
		uv.push( new THREE.UV( u1, v1 ) );
		uv.push( new THREE.UV( u2, v2 ) );
		uv.push( new THREE.UV( u3, v3 ) );
		where.push( uv );

	};

	Model.prototype = Object.create( THREE.Geometry.prototype );

	callback( new Model() );

};
