/**
* VisageAR
* <br/><br/>
* @constructor
*/
function VisageAR() {

	var v_width;
	var v_height;
	var v_renderer;
	var v_scene, v_backgroundScene, v_maskScene, v_candideScene;
	var v_camera, v_backgroundCamera, v_maskCamera, v_candideCamera;
	var v_time;

	var v_ppixels;
	var v_pixels;
	
	var v_video;
	var v_videoCanvas;
	var v_videoContext;
	var v_videoTexture;
	var v_frameCanvas ;
	var v_frameContext ;

	var v_glasses;
	var v_mask, v_candideMask;
	
	var v_z_offset;
	var v_z_increment;
	var v_scale_factor;
	var v_scale_increment;

	this.v_tracker_pom = 0;
	var v_tracker;
	var v_faceData;
	var v_trackingStatus;
	var v_tracking = false;
	var v_faceDataArray;

	var v_container;
	var v_urlOccluder;
	var v_urlObject;

	/*
	* Calculates parameters that change related to filling (croping) strategy
	*/
	var setupViewingFrustrum = function(input_w, input_h, output_w, output_h) {
		var parameters = [];

		var input_aspect = input_w / input_h;
		var output_aspect = output_w / output_h;

		parameters.perspective = [];
		parameters.orto = [];

		parameters.perspective.output_aspect = output_aspect;

		if(input_aspect >= output_aspect){
			//fillY
			parameters.orto.left = - (input_h * output_aspect) / 2;
			parameters.orto.right = (input_h * output_aspect) / 2;
			parameters.orto.top = input_h / 2;
			parameters.orto.bottom = - input_h / 2;

			if(input_w >= input_h){
				parameters.perspective.fov = 2 * Math.atan( 1 / 3 ) * 180 / Math.PI;

			} else {
				parameters.perspective.fov = 2 * Math.atan(1 / input_aspect / 3) * 180 / Math.PI;
			}

		} else {
			//fillX
			parameters.orto.left = - input_w / 2;
			parameters.orto.right = input_w / 2;
			parameters.orto.top = (input_w * 1 / output_aspect) / 2;
			parameters.orto.bottom = - (input_w * 1 / output_aspect) / 2;


			if(input_w >= input_h){
				parameters.perspective.fov = 2 * Math.atan( 2 * input_w / input_h * 1 / output_aspect / (2 * 3) ) * 180 / Math.PI;

			} else {
				parameters.perspective.fov = 2 * Math.atan( 2 * 1 / output_aspect / (2 * 3) ) * 180 / Math.PI;

			}
		}
		return parameters;
	}

	var initialize_ = function(container, video) {
		// get camera stream size
		v_width = video.videoWidth;
		v_height = video.videoHeight;

		// get output size
		output_w = video.width;
		output_h = video.height;

		v_container = container;
		
		
		v_ppixels = VisageModule._malloc(v_width*v_height*4);
		v_pixels = new Uint8Array(VisageModule.HEAPU8.buffer, v_ppixels, v_width*v_height*4);

		// create webcam canvas
		v_video = video;

		v_videoCanvas = document.createElement('canvas');
		v_videoCanvas.width = v_width;
		v_videoCanvas.height = v_height;
		v_videoContext = v_videoCanvas.getContext('2d',  {willReadFrequently: true});
		v_videoCanvas.setAttribute('style', 'display: none');
		
		v_frameCanvas = document.createElement('canvas');
		v_frameCanvas.width = v_width;
		v_frameCanvas.height = v_height;
		v_frameContext = v_frameCanvas.getContext('2d',  {willReadFrequently: true});
		v_frameCanvas.setAttribute('style', 'display: none; z-index: 1');
		v_videoTexture = new THREE.Texture(v_frameCanvas);

		// init tracker
		this.v_tracker_pom = new VisageModule.VisageTracker("Head Tracker.cfg");
		v_faceDataArray = new VisageModule.FaceDataVector();
		v_faceDataArray.push_back(new VisageModule.FaceData());
		
		v_tracker = this.v_tracker_pom;

		// setup renderer
		v_renderer = new THREE.WebGLRenderer({antialias: true});
		
		v_renderer.setClearColor(0x0055FF, 1);
		v_renderer.autoClearDepth = false;
		v_renderer.autoClear = false;
		v_renderer.autoClearColor = false;
		v_renderer.setSize(output_w, output_h);
		v_renderer.sortObject = false;
		v_renderer.toneMappingExposure = 1;
		v_renderer.shadowMap.enabled = true; 
		v_renderer.setPixelRatio( window.devicePixelRatio );
		container.appendChild(v_renderer.domElement);

		// setup scenes
		v_scene = new THREE.Scene();
		v_backgroundScene = new THREE.Scene();
		v_maskScene = new THREE.Scene();
		v_candideScene = new THREE.Scene();
		v_time = new THREE.Clock(true);

		var parameters = setupViewingFrustrum(v_width, v_height, output_w, output_h);

		var left = parameters.orto.left;
		var right = parameters.orto.right;
		var top = parameters.orto.top;
		var bottom = parameters.orto.bottom;


		// setup video plane camera
		v_backgroundCamera = new THREE.OrthographicCamera( left, right, top, bottom, 0.1, 1000);
		v_backgroundCamera.lookAt(new THREE.Vector3(0, 0, -1));
		v_backgroundScene.add(v_backgroundCamera);

		// setup video plane
		var plane = new THREE.Mesh(new THREE.PlaneGeometry(v_width, v_height, 1, 1), new THREE.MeshBasicMaterial({color: 0xFFFFFF, map: v_videoTexture}));
		plane.position.set(0, 0, -500);
		v_backgroundScene.add(plane);

		var fov = parameters.perspective.fov;
		var output_aspect = parameters.perspective.output_aspect;

		// setup glasses camera
		v_camera = new THREE.PerspectiveCamera(fov, output_aspect, 0.1, 1000);
		v_camera.lookAt(new THREE.Vector3(0, 0, 1));
		v_scene.add(v_camera);

		// setup mask camera
		v_maskCamera = new THREE.PerspectiveCamera(fov, output_aspect, 0.1, 1000);
		v_maskCamera.lookAt(new THREE.Vector3(0, 0, 1));
		v_maskScene.add(v_maskCamera);
		v_candideCamera = new THREE.PerspectiveCamera(fov, output_aspect, 0.1, 1000);
		v_candideCamera.lookAt(new THREE.Vector3(0, 0, 1));
		v_candideScene.add(v_candideCamera);

		// setup masking cube
		var geometry = new THREE.CubeGeometry(0.1, 1, 0.2);
		var geometry2 = new THREE.CubeGeometry(0.5, 1, 0.2);
		var material = new THREE.MeshBasicMaterial({color: 0x000000, opacity: 0, transparent: true});
		v_mask = new THREE.Object3D();
		var v_maskChild = new THREE.Mesh(geometry, material);
		v_maskChild.position.set(0, 0, -0.1);
		var v_maskChild2 = new THREE.Mesh(geometry2, material);
		v_maskChild2.position.set(0, 0, -0.18);
		v_maskScene.add(v_mask);

		// setup ambient light
		var ambientLight = new THREE.AmbientLight(0xFFFFFF);
		v_scene.add(ambientLight);

		// setup point light
		var pointLight = new THREE.PointLight(0xA0A0A0, 0.1, 0.0, 2);
		pointLight.position.set(0, 0, 0);
		v_scene.add(pointLight);

	}

	this.getScene = function () {
		return v_scene;
	}
	
	/** 
	*  Initializes Visage AR and sets up rendering and tracking. The video resolution is used for the canvas resolution.
	*  @param {element}container - The HTML element in which to put the rendering canvas.
	*  @param {element}video - The HTML video element required for camera access.
	*/
	this.initialize = initialize_;
	
	function splitString(url)
	{
		var string = url;
		var rest = string.substring(0, string.lastIndexOf("/") + 1);
		var last = string.substring(string.lastIndexOf("/") + 1, string.length); 
		return [rest, last];     
	}
	
	var loadOccluder_ = function(urlOccluder) {

		v_urlOccluder = urlOccluder;
	
		var maskLoader = new THREE.OBJLoader();
		var manager = new THREE.LoadingManager();
		var mtlLoader = new THREE.MTLLoader(manager);

		var temp = splitString(urlOccluder);
		var pathMtl = temp[0];
		var model = temp[1];
		
		mtlLoader.setPath(pathMtl);
		mtlLoader.load( model + ".mtl", function( materials ) {

			materials.preload();

			maskLoader.setMaterials( materials );

			maskLoader.load(urlOccluder + ".obj", function ( object ) 
			{
				if (v_candideMask)
					v_mask.remove(v_candideMask);
				
				if (typeof object.children === 'undefined' || object.children.length != 1)
				{
					console.log ("Error - occluder object invalid");
				}
		
				object.children[0].material.transparent = true;
				object.children[0].material.opacity = 0;
				
				v_candideMask = new THREE.Object3D();
				v_candideMask.add(object.children[0]);						
				v_mask.add(v_candideMask);

				//remove the occluder object from original glasses
				object.remove(object.children[0]);
			});
		});
	}

	/**
	*  Loads the occlusion mask in OBJ format and its material in MTL format. 
	*  <br><br
	*  @param {string} url - The URL from which to load occlusion mask, without the extension.
	*/
	this.loadOccluder = loadOccluder_;
	
	/**
	*  Clears the current active occlusion mask and removes it from the occlusion scene. 
	*/
	this.clearOccluder = function()
	{
		if (v_mask && v_candideMask)
			v_mask.remove(v_candideMask);
	}

	var loadObject_ = function(urlObject) {
		//Show "Glasses Loading" text
		var glass_text = document.getElementById("glass_text");
		if (glass_text)
			glass_text.hidden = false;

		v_urlObject = urlObject;

		//Remove old glasses
		v_scene.remove(v_glasses);
		
		v_glasses = new THREE.Object3D();
		
		var glassesLoader = new THREE.OBJLoader();
		var mtlLoader = new THREE.MTLLoader();

		var temp = splitString(urlObject);
		var pathMtl = temp[0];
		var model = temp[1];

		mtlLoader.setPath(pathMtl);
		mtlLoader.load( model + ".mtl", function( materials ) {

			materials.preload();

			glassesLoader.setMaterials( materials );
			v_glasses.position.set(0, 1, -5);
			v_scene.add(v_glasses);
			glassesLoader.setPath(pathMtl);
			glassesLoader.load(model + ".obj", function ( object ) {
			
				v_glasses.children.length = 0;
				v_glasses.add(object);
			
				//Hide "Glasses Loading" text
				if (glass_text)
					glass_text.hidden = true;
			});
		});	
	}

	/** 
	*  Loads the 3D object and adds it to the main scene to be rendered from the given URL and makes it active. The object should be in OBJ format and the material should be in MTL format. The MTL file should have the same name as the OBJ file.
	*  See the <a href="eyewear_model_guide.html">modeling guide</a> on how to prepare models for use with VisageAR.
	*  @param {string} urlObject - The URL from which to load the model, without the extension.
	*/
	this.loadObject = loadObject_;

	/**
	*  Starts tracking the face and displaying (rendering) any 3D objects loaded using loadObject() function. Object is 
	*  overlayed on the face.
	*/
	this.startTracking = function() {
		v_tracking = true;
	}

	/**
	*  Stops tracking.
	*/
	this.stopTracking = function() {
		v_tracking = false;
	}
	
	this.setIPD = function(ipd) {
		if (v_tracking = true && v_tracker)
		{
			  //convert to meters
			  v_tracker.setIPD(ipd/1000);
		}
	}

	/*
	*  Updates the tracker with a new video image.
	*/
	var updateTracker = function() {

		// update video texture
		if (v_video.readyState === v_video.HAVE_ENOUGH_DATA) {
			v_videoContext.drawImage(v_video, 0, 0, v_width, v_height);
			v_videoTexture.needsUpdate = true;
		}

		if (!v_tracking)
			return;
		
		// fetch image data from canvas
		var dataBuffer = v_videoContext.getImageData(0, 0, v_width, v_height);
		var imageData = dataBuffer.data;
		
		// 
		v_pixels.set(imageData);
		
		v_frameContext.putImageData(dataBuffer,0 ,0);
		// 
		v_trackingStatus = v_tracker.track(
			v_width, 
			v_height, 
			v_ppixels, 
			v_faceDataArray,
			VisageModule.VisageTrackerImageFormat.VISAGE_FRAMEGRABBER_FMT_RGBA.value,
			VisageModule.VisageTrackerOrigin.VISAGE_FRAMEGRABBER_ORIGIN_TL.value, 
			0,
			-1
		);
		// fetch faceData of the first face
		v_faceData = v_faceDataArray.get(0);
	}

	/*
	*  Updates the glasses model position and the face mask position.
	*/
	var update = function() {

		if (v_tracking && v_trackingStatus[0] === VisageModule.VisageTrackerStatus.TRACK_STAT_OK.value) 
		{
			// move mask
			v_mask.position.set(v_faceData.getFaceTranslation()[0], v_faceData.getFaceTranslation()[1], v_faceData.getFaceTranslation()[2]);
			v_mask.rotation.set(v_faceData.getFaceRotation()[0], v_faceData.getFaceRotation()[1] + 3.14, v_faceData.getFaceRotation()[2]);
			v_mask.rotation.order = "YXZ";

			// move glasses
			v_glasses.position.set(v_faceData.getFaceTranslation()[0], v_faceData.getFaceTranslation()[1], v_faceData.getFaceTranslation()[2]);
			v_glasses.rotation.set(v_faceData.getFaceRotation()[0], v_faceData.getFaceRotation()[1] + 3.14, v_faceData.getFaceRotation()[2]);
			v_glasses.rotation.order = "YXZ";
		} else {

			v_mask.position.set(0, 0, -5);
			v_glasses.position.set(0, 0, -5);
		}

	}

	/*
	*  Renders the scene.
	*/
	var render = function() {
		v_renderer.clear(1, 1, 1);
		v_renderer.render(v_backgroundScene, v_backgroundCamera);
		v_renderer.clear(0, 1, 1);
		v_renderer.render(v_maskScene, v_maskCamera);
		v_renderer.render(v_scene, v_camera);
	}

	var clear_ = function() {
		//dispose three.js objects
		if(v_backgroundScene != undefined)
			v_backgroundScene.remove.apply(v_backgroundScene, v_backgroundScene.children);

		if(v_scene != undefined)
			v_scene.remove.apply(v_scene, v_scene.children);
		
		if(v_maskScene != undefined)
			v_maskScene.remove.apply(v_maskScene, v_maskScene.children);

		if(v_candideScene != undefined)
			v_candideScene.remove.apply(v_candideScene, v_candideScene.children);
		
		//free v_ppixels
		if(this.v_ppixels) {
			VisageModule._free(this.v_ppixels);
		}

		//remove canvas
		if(v_renderer != undefined)
			v_renderer.domElement.remove();
	}

	/*
	*  Checks if video dimension have changed and reinitalizes scene, 3D objects, pixels buffer and canvas.
	*/
	var updateOrientation = function() {
		if(v_video.videoWidth == v_width 
			&& v_video.videoHeight == v_height ){
				return;
		}

		clear_();
		initialize_(v_container, v_video);
		loadOccluder_(v_urlOccluder);
		loadObject_(v_urlObject);
		
	}

	/**
	* 
	*  Clears the scene, 3D objects, pixels buffer and canvas for displaying the frame
	*/
	this.clear = clear_;

	/*
	*  Main loop.
	*/
	var loop = function() {
		var requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame ||
								  window.webkitRequestAnimationFrame || window.msRequestAnimationFrame;
		requestAnimationFrame(loop);
		
			if (!v_video)
				return;

			updateOrientation();
			updateTracker();
			update();
			render();
	};
	/**
	*<br/><br/>
	*<br/><br/>
	*<br/><br/>
	*<br/><br/>
	*<br/><br/>
	*<br/><br/>
	*<br/><br/>
	*<br/><br/>
	*<br/><br/>
	*<br/><br/>
	*<br/><br/>
	*<br/><br/>
	*<br/><br/>
	*<br/><br/>
	*<br/><br/>
	*/
	loop();
	
}
