var MODELVIEWER = {}

MODELVIEWER.viewPointMesh = new THREE.Object3D();
MODELVIEWER.compassMesh = new THREE.Object3D();

MODELVIEWER.AssetLoader = function() {

	var colladaLoader;
	var viewPointModelPath = 'content/models/camera.dae';
	var compassModelPath = 'content/models/compass.dae';
	var onComplete;
	var nModels = 2;


	this.load = function( callback ) {
		onComplete = callback
		colladaLoader = new THREE.ColladaLoader();
		loadViewPointMesh();
		loadCompassMesh();
	}

	function loadViewPointMesh() {
		//camera looking in the +Z direction and up is +Y
		var scale = 0.1;
		colladaLoader.load( viewPointModelPath, function ( collada ) {

			MODELVIEWER.viewPointMesh = collada.scene;
			MODELVIEWER.viewPointMesh.traverse( function ( child ) {
				if ( child.geometry ) {
					child.geometry.scale( scale, scale, scale );							
				}
			});

	    	checkLoadStatus();
	    });	

	}

	function loadCompassMesh() {
		//camera looking in the +Z direction and up is +Y
		var scale = 40.0;
		colladaLoader.load( compassModelPath, function ( collada ) {

			MODELVIEWER.compassMesh = collada.scene;
			MODELVIEWER.compassMesh.traverse( function ( child ) {
				if ( child.geometry ) {
					child.geometry.scale( scale, scale, scale );
					child.material = new THREE.MeshBasicMaterial( { color: child.material.color } );							
				}
			});

	    	checkLoadStatus();
	    });	

	}

	var loadedModels = 0;
	function checkLoadStatus() {
		loadedModels++;
		if ( loadedModels === nModels ) {
			onComplete();
		}
	}

}

MODELVIEWER.JSONTo3DConverter = function() {
//This code extracts the polygon data from the Lightstanza json file and
//	transforms it into a threejs mesh

//the data structure of the json file is as follows:
//	json
//		illuminanceGrids
//		layers
//		materials
//		model
//		polygons
//		viewpoints
//	 	windows

//this is what it looks like inside the polygon object
//	geometry
//	layer
//	material
//	materialType
//	textureID


	// private variables -----------------------------------------------------------

	var materials;

	// public methods --------------------------------------------------------------

	this.initMaterials = function( inMaterials ) {
		materials = inMaterials;
	};

	this.extractStructure = function( json ) {
		var structure = new THREE.Group;
		json.polygons.forEach( function( polygon ) {

			if (polygon.geometry) {

				var loops = parsePolygon( polygon ); //extract loops from polygons, the loop object is a private class defined below.
				var geometry = triangulateLoops( loops ); //get THREE.Geometry from the loop array

				var merged = false;

				structure.traverse( function( c ) {
					if ( c.userData.name === polygon.material && c.userData.layer == polygon.layer && !merged ) {
						c.geometry.merge( geometry );
						merged = true;
					}
				});


				if ( !merged ) {
					var material = generateMaterial( { materialName: polygon.material, 
													   materialType: polygon.materialType,
													   materials: materials } );

					var mesh = new THREE.Mesh( geometry, material );

					mesh.userData = { 
										name: polygon.material,
										materialType: polygon.materialType, 
										layer: polygon.layer
									};
					
					structure.add( mesh );
				}
			}
		});

		return structure;
	};

	this.extractWindowGroups = function( json ) {
		var windowMeshes = new THREE.Object3D();
		var windows = json.windows;

		for ( var window_ in windows ) {
			var verts = parseLoop( windows[ window_ ].geometry[ 'outerLoop' ] );
			var loop = new Loop( verts, 'outerLoop' );
			loop.prepareVerts();
			var geometry = triangulateLoops( [ loop ] );
			//var material = new THREE.MeshPhongMaterial( { color: 0x00ff00, side: THREE.DoubleSide, transparent: true, depthWrite: false } );
			var material = generateMaterial();

			var mesh = new THREE.Mesh( geometry, material );

			mesh.userData = { 
								name: "Window Assembly: " + windows[ window_ ].group,
								materialType: "glass"
							};

			windowMeshes.add( mesh );
		}

		return windowMeshes;
	};

	this.extractIlluminanceGrids = function( json ) {
		//grab IllumGrids from the json and add them to illumGridPlanes object
		var illumGridPlanes = new THREE.Object3D();
		var illumGrids = json.illuminanceGrids;

		for ( var illumGrid in illumGrids ) {
			var verts = parseLoop( illumGrids[ illumGrid ].geometry[ 'outerLoop' ] );
			var loop = new Loop( verts, 'outerLoop' );
			loop.prepareVerts();
			var geometry = triangulateLoops( [ loop ] );
			var material = new THREE.MeshPhongMaterial( { color: 0xFFF125, side: THREE.DoubleSide, opacity: 0.6, transparent: true, depthWrite: true } );
			var mesh = new THREE.Mesh( geometry, material );

			mesh.userData.illumGrid = true;
			mesh.userData.name = illumGrids[ illumGrid ].name;
			mesh.userData.normal = new THREE.Vector3().fromArray( illumGrids[ illumGrid ].geometry.vec );

			illumGridPlanes.add( mesh );
		}

		return illumGridPlanes;
	};

	this.extractViewpoints = function( json ) {
		//grab viewpoints from the json and add them to viewpointSpheres object
		var viewpointsLayer = new THREE.Object3D();
		var viewpoints = json.viewpoints;

		for ( var viewpoint in viewpoints ) {

			var viewpointCamera = viewpoints[ viewpoint ].camera;

			var position = new THREE.Vector3().fromArray( viewpointCamera.position );
			var target = new THREE.Vector3().fromArray( viewpointCamera.target );
			var cameraUp = new THREE.Vector3().fromArray( viewpointCamera.up ).normalize();

			var mesh = MODELVIEWER.viewPointMesh.clone();

			mesh.traverse( function( child ) {
				if ( child.material ) {
					child.material = child.material.clone();
					child.material.needsUpdate = true;
				}
				child.userData.id = viewpoints[ viewpoint ].name;
			});

			mesh.position.copy( position );
			mesh.up.copy( cameraUp );
			mesh.lookAt( target );

			mesh.userData.type = "viewpoint";
			mesh.userData.viewpoint = {
				vecX: viewpoints[ viewpoint ].camera.vec[0],
				vecY: viewpoints[ viewpoint ].camera.vec[1],
				vecZ: viewpoints[ viewpoint ].camera.vec[2],

				targetX: viewpoints[ viewpoint ].camera.target[0],
				targetY: viewpoints[ viewpoint ].camera.target[1],
				targetZ: viewpoints[ viewpoint ].camera.target[2],

				posX: viewpoints[ viewpoint ].camera.position[0],
				posY: viewpoints[ viewpoint ].camera.position[1],
				posZ: viewpoints[ viewpoint ].camera.position[2],

				upX: viewpoints[ viewpoint ].camera.up[0],
				upY: viewpoints[ viewpoint ].camera.up[1],
				upZ: viewpoints[ viewpoint ].camera.up[2]
			};

			viewpointsLayer.add( mesh );

		}

		return viewpointsLayer;
	};

	this.generateViewpointRaycastTargets = function( viewpoints ) {
		var targets = [];
		var geo = new THREE.SphereBufferGeometry( 0.5 );
		var mat = new THREE.MeshBasicMaterial( { visible:false } );

		viewpoints.children.forEach( function( c ) {
			var mesh = new THREE.Mesh( geo, mat );
			mesh.userData.id = c.userData.id;
			c.add( mesh );
			targets.push( mesh );
		});

		return targets;
	}

	// private methods --------------------------------------------------------------

	function parsePolygon( polygon ) {
		//extracts an array of loops from a polygon. One of the loops must have type 'outerLoop'

		var loops = [];

		for( var loopType in polygon.geometry ) {

			var verts = parseLoop( polygon.geometry[ loopType ] );
			var loop = new Loop( verts, loopType );
			loop.prepareVerts(); //gets the loop object ready for triangulation
			loops.push( loop );

		}

		return loops;
	}

	function parseLoop( loop ) {
		//extracts the vertices from a loop
		var verts = [];
		for (var property in loop) {
		    if (loop.hasOwnProperty(property)) {

		        verts.push( loop[ property ] );

		    }
		}

		return verts;
	};

	function triangulateLoops( loops ) {
		//this method accepts an array of loops and returns THREE.Geometry
		var verts3D = []; //needed to build up geometry
		var verts2D = []; //needed to triangulate polygon
		var innerLoopIndices = [];

		//earcut expects a flat of array of 2d vertices, with the outerLoop vertices leading, 
		//this loop grabs the outerLoop vertices so they're first in line.
		loops.forEach( function( loop ) {
			if ( loop.type === 'outerLoop' ) {
				verts2D = verts2D.concat( loop.verts2D );
				verts3D = verts3D.concat( loop.verts );
			}
		});

		//now tack on the innerLoop vertices
		loops.forEach( function( loop ) {
			if ( loop.type !== 'outerLoop' ) {
				var start = verts2D.length / 2;

				//innerLoopIndices gives the index of the start vertex of the innerLoops, 
				//NOTE: It's the index of the vertex, not the index in the array itself, that's why 
				//start is verts2D.length / 2
				innerLoopIndices.push( start );
				verts2D = verts2D.concat( loop.verts2D );
				verts3D = verts3D.concat( loop.verts );
			}
		});

		//perform the triangulation
		var triangles = earcut( verts2D, innerLoopIndices, 2 );

		//and build up the geometry. The triangles from the 2d triangulation still coorespond to the 2d vertices 
		//as well as the 3d vertices, so use the 3d vertices to build the geometry
		var geometry = createGeometry( triangles, verts3D );

		if ( triangles.length === 0 ) {
			//this means earcut could not triangulate the polygon.
			console.log( 'triangulation error: could not triangulate polygon' );
		}

	  	return geometry;
	};

	function createGeometry( triangles, verts ) {
		//build the geometry using the triangles and vertices.
		var geometry = new THREE.Geometry();

		for ( var i = 0; i < verts.length; i++ ) {
			var vec = new THREE.Vector3().fromArray( verts[i] );
			geometry.vertices.push( vec );
		}

		for ( var i = 0; i < triangles.length; i = i + 3 ) {
			geometry.faces.push( new THREE.Face3( triangles[i], triangles[i+1], triangles[i+2] ) );
		}

		//these calls are necessary since the geometry's vertices/faces were updated.
		geometry.verticesNeedsUpdate = true;
		geometry.elementsNeedUpdate = true;
		geometry.computeFaceNormals();

	  	return geometry;
	}

	function generateMaterial( params ) {
		params = params || {};

		/*
			params options: 
				materialName (optional),
				materialType (optional), 
				materials (optional)
		
		*/
		var color, opacity;

		//this grabs the material color from the materials object in the json
		if ( params.materials && params.materialName ) {
			for ( var material in params.materials ) {
				if ( params.materials[ material ].name === params.materialName ) {
					var m = params.materials[material];
					color = new THREE.Color( m.rgba[0] / 255, m.rgba[1] / 255, m.rgba[2] / 255 );
					opacity = 1 - m.rgba[3] / 255;
					break;
				}
			};	
		}
/*
		var shader = THREE.ShaderLib[ "spot" ]; //defined in spotShader.js
		var uniforms = THREE.UniformsUtils.clone( shader.uniforms );

		uniforms.dotIntensity.value = 0.0 //from 0.0 to 1.0
		uniforms.dotSpacing.value = 0.01; //in meters
		uniforms.dotColor.value = new THREE.Color( 0.2, 0.2, 0.8 ); //this color works like transmittance: white = clear

		var material = new THREE.ShaderMaterial( { 
			fragmentShader: shader.fragmentShader, 
			vertexShader: shader.vertexShader, 
			uniforms: uniforms,
			lights: true,

			opacity: opacity || 1,
			side: THREE.DoubleSide
		} );

		if ( params.materialType && params.materialType === "Glass" ) {
			material.transparent = true;
			material.depthWrite = false;
		}

		color ? uniforms.diffuse.value.copy( color ) : uniforms.diffuse.value.copy( new THREE.Color( 1, 1, 1 ) );
*/

	 	var material = new THREE.MeshPhongMaterial( {
			opacity: opacity || 1,
			side: THREE.DoubleSide,
			color: color ? color : new THREE.Color( 1, 1, 1 )
		} );

		if ( params.materialType && params.materialType === "Glass" ) {
			material.transparent = true;
			material.depthWrite = false;
		}



		material.needsUpdate = true;


		return material;
	}


	// private Classes ---------------------------------------------------------------------------------

	function Loop( verts, type ) {
	/*

			The loop class is for storing a loop's array of vertices and the type.
		The vertices are expected to be [ [ x0, y0, z0 ], [ x1, y1, z1 ], ...].
		The type is either 'outerLoop' or something like 'loop1', 'loop2', ect. Everthing
		except 'outerLoop' is treated like an inner loop.

			The loop class also contains an algorithm to prepare the vertice for triangulation.
		We couldn't find a good library that supported 3D vertices, Earcut.js claims to but I found
		out the hard way that it actually doesn't: https://github.com/mapbox/earcut/issues/60 
		The algorithm in the loop class projects the vertices from verts 2 dimensions (XY), and stores them
		in the verts2D array. Later, verts2D is fed to earcut and it handles them usually without problem.

			One tricky thing about the algorithm: before it projects the vertices onto the XY plane,
		it performs a reflection transformation so that the side with the greatest area is facing the 
		XY plane. This is necessary because, otherwise, a polygon orthoganol to the XY plane would get projected
		as a line of points which cannot be triangulated. 

	*/

		this.verts = verts;

		this.type = type;

		//tempVerts is a clone of verts, so the vertices in tempVerts can be manipulated and the 
		//original vertices persist in verts.
		var tempVerts = [];

		//cloning verts into temp verts.
		for ( var i = 0; i < this.verts.length; i++ ) {
			tempVerts.push( this.verts[i].slice(0) );
		}

		//a flat array of 2D vertices, eventually fed to earcut for triangulation
		this.verts2D = [];

		this.prepareVerts = function() {
			//must be called before triangulation. This method carries out the algorithm described above
			//and populates verts2D

			var side = this._getSideWithGreatestArea();
			if ( side.index !== 2 ) { //side.index == 2 means the polygon is already facing the XY plane.
				flipVerts( 2, side.index );
			}
			this._flattenArrayAndProject();
		};


		this._getSideWithGreatestArea = function() {
			//this method figures out which orthoganol projection has the greatest area.
			// it returns an object, 'max', which contains the index ( 0 = x, 1 = y, 2 = z ) and the
			// value of the dot product.
			var n = this._calculateNormal();
			
			var xHat = new THREE.Vector3( 1, 0, 0 );			
			var yHat = new THREE.Vector3( 0, 1, 0 );			
			var zHat = new THREE.Vector3( 0, 0, 1 );

			//get the dot products.
			var dots = [
					 Math.abs( n.dot( xHat ) ), 
					 Math.abs( n.dot( yHat ) ),
					 Math.abs( n.dot( zHat ) )
				   ];

			var max = { index: 0, value: 0 }; //initialize max

			//figure out which dot product has the highest value.
			for( var i = 0; i < dots.length; i++ ) {
				if ( dots[i] > max.value ) {
					max.index = i;
					max.value = dots[i];
				}
			}

			return max;

		};

		this._calculateNormal = function() {
			// This is a little trickier than it might seem. The complicating factor is dealing with collinear points.

			var n = new THREE.Vector3()
			var i = 0;

			var k = 1;
			//k only comes into play when, for example, there's a polygon made up of only 3 nearly collinear points.
			//something like A--------------B-----\
			//											\--------C  where C is only slightly out of line with AB.
			//If the code tries to get the angle ABC it might be below tolerance and not get a very good normal,
			//K will allow the loop to wrap around 1 more and check angle BCA which will give a much better normal.

			while ( Math.abs(n.x) <= 0.00001 && Math.abs(n.y) <= 0.00001 && Math.abs(n.z) <= 0.00001 ) { //while loop to deal with collinear vertices
				if ( i >= this.verts.length - 1 && i !== 0 ) {
					console.log( 'cannot triangulate polygon', this.verts.length );
					n = new THREE.Vector3( 0, 0, 1 ); //so nothing breaks later on.
					break;
				}

				if ( i == this.verts.length - 2 ) k = 0;

				var p1 = new THREE.Vector3().fromArray( this.verts[0 + i] );
				var p2 = new THREE.Vector3().fromArray( this.verts[1 + i] );
				var p3 = new THREE.Vector3().fromArray( this.verts[ ( 2 + i ) * k ] );

				var A = new THREE.Vector3();
				var B = new THREE.Vector3();

				//get the normalized vectors between p1->p2 and p2->p3
				A.subVectors( p2, p1 ).normalize();
				B.subVectors( p3, p2 ).normalize();

				//now take the cross product to get the normal.
				var n = A.cross( B );

				i++;
			}


			return n.normalize();
		};

		this._flattenArrayAndProject = function() {
			//to project onto the xy plane is easy, simply omit the z coordinate.
			for ( var i = 0; i < tempVerts.length; i++ ) {
				this.verts2D = this.verts2D.concat( [ tempVerts[i][0], tempVerts[i][1] ] );
			}

		}


		function flipVerts( a, b ) {
			//this method takes two indices representind dimentions xyz ( x = 0, y = 1, z = 2 )
			// and it flips those dimensions in the tempVerts array, thus performing a reflection transformation
			var holder;
			for ( var i = 0; i < tempVerts.length; i++ ) {
				holder = tempVerts[i][a];
				tempVerts[i][a] = tempVerts[i][b];
				tempVerts[i][b] = holder;
			}
		}

	};

};

MODELVIEWER.JSONTo3DConverter.prototype = {
	constructor: MODELVIEWER.JSONTo3DConverter
};