/**
 * Logic to control the camera element itself.
 
Copyright 2009 Palm, Inc.  All rights reserved.

*/


CameraControl = Class.create({
	
	initialize: function(appController, sceneAssistant){
		
		// When we have stuff happening that takes a while, we store a frob here that keeps track of how far
		// through the event we are.  Sometimes, when attempting to start another activity, it is necessary to 
		// check for existing frobs and if they exist, delay/abort doing that new activity.  Delaying an activity is 
		// usually acheived by adding to the active frob.
		/// TODO: When in debug mode, poll _frobs to ensure that frobs are activities are finishing in a timely manner.  
		this._frobs = {};
	
		this.setScene(appController, sceneAssistant);
		
		llog("CameraControl constructor completed");
	},

	/**
	 * Perform still capture.
	 * 
	 * @return   Returns true if the capture started successfully.
	 */
	stillCapture: function(){
		llog("stillCapture: Capture button pressed");
		
		// User activity, continue preventing the sceen from dimming
		this.scene.resetIdleCheck();
		
		
		if (this._haveFrobs()) {
			if (this._frobs.capture){
				llog("Already have an active capture");
			}
			else {
				llog("Not setup, or some other thing active");
			}
			llog("  capture ignored");
			return false;
		}
		
		var cam = this.getCameraInstance();
		if ( (!cam) || (!cam.preview) ){
			llog("Capture aborted, camera plugin not ready yet");
			return;
		}

		// Remove the controls so people know we're actively capturing
		this.scene.controller.sceneElement.addClassName('recording');

		// put the spinner on the photoroll icon to show it's not uptodate.  
		this.scene.setPhotorollUpdating(true);


		if (this.scene.defaultFilename){
			// A (default) filename was passed in - presumably we've been sublaunched and the caller
			// knows where they want to save the file.
			cam.imageDestination = this.scene.defaultFilename;
		}

		// Call the capture state machine.		
		this._frobs.capture = {state:'init', camera: cam};
		
		
		// Tracking how long it takes to capture an image...
		if (CameraControl.__debug){
			
			this._frobs.capture.captureTime = new Date().getTime();
			var elemC = this.scene.controller.get('sagar_console');
			if (elemC) {
				elemC.innerHTML = this._frobs.capture.captureTime;
			}
		}

		CameraControl._seqStillCapture.call(this);
		
		return true;
	},
	

	/**
	 * Perform video capture.
	 * 
	 * @return   Returns true if the capture toggled successfully.
	 */
	videoCapture: function(addVibrate){
		llog("Capture button pressed");
		var cam = this.getCameraInstance();
		

		if (this._haveFrobs()) {
			if (this._frobs.capture){
				// In capture, user is requesting stop - Check that the camera thinks it's recording...
				llog("Already have an active capture, setting 'stop' (recording is ", cam.recording, ")");
				
				this.videoCaptureStop();

				return true;
			}
			else {
				llog("frobs exist, but not capture.  Probably still doing setup");
				return false;
			}

		}
		
		
		// User activity, continue preventing the sceen from dimming
		this.scene.resetIdleCheck();
		

		if ( (!cam) || (!cam.preview) ){
			llog("Capture aborted, camera not ready yet");
			return;
		}

		if (this.scene.defaultFilename){
			// A (default) filename was passed in - presumably we've been sublaunched and the caller
			// knows where they want to save the file.
			cam.destination = this.scene.defaultFilename;
		}

		// Call the capture state machine.		
		this._frobs.capture = {state:'init', camera: cam, vibrate: addVibrate};
		
		
		// Tracking how long it takes to capture an image...
		if (CameraControl.__debug){
			
			this._frobs.capture.captureTime = new Date().getTime();
			var elemC = this.scene.controller.get('sagar_console');
			if (elemC) {
				elemC.innerHTML = this._frobs.capture.captureTime;
			}
		}

		CameraControl._seqVideoCapture.call(this);
		
		return true;
	},


	/** 
	 *  If you know capture is active and you want to stop it, call this.
	 *  
	 */ 
	videoCaptureStop: function(){
		var retVal = false;
		if (this._haveFrobs()) {
			if (!this._frobs.capture) {
				llog("Something is active, but it isn't capture.  Don't do anything");
			}
			else {
				this._frobs.capture.stop = true;

				// force the immediate state handling, so stop happens now.
				this._frobs.capture.callback({type:'stop-capture', target: this.getCameraInstance()});

				retVal = true;
			}
		}
			
		return retVal;
	},

	/** 
	 * @returns
	 *   - false 
	 *   - CameraControl.IN_CAPTURE_STILL
	 *   - CameraControl.IN_CAPTURE_VIDEO
	 */	
	inCapture: function(){
		var retVal = false;

		if ( (undefined !== this._frobs.capture) && (null !== this._frobs.capture) ) {
			if (this.scene.isVideoCapture){
				return CameraControl.IN_CAPTURE_VIDEO;
			}
			else {
				return CameraControl.IN_CAPTURE_STILL;
			}
		}

		return false;
	},

	/**
	 * Used internally to track the instance of the browser plugin.
	 */
	getCameraInstance: function(isSetup){
		if (!this._cameraInstance) {
			if (!isSetup){
				try{
					throw new Error("CameraControl::getCameraInstance() instance is null");
				}
				catch(e){
					llog(e.message, ": ", Object.toJSON(e));
				}
			}
		}

		return this._cameraInstance;
	},
	
	/** 
	 * Used internally to track the instance of the browser plugin
	 * 
	 * @param {Camera} instance   Instance of the camera
	 */
	setCameraInstance: function(instance){
		if (!!this._cameraInstance && !!instance) {
			Mojo.Log.error("Camera instance already exists, should have been dumped before setting new one.");
			this.handleFatal();
		}

		this._cameraInstance = instance;
	},
	
	/**
	 * Used to remove the camera instance can do clean-up.
	 */
	clearCameraInstance: function(){
		if (!this._cameraInstance){
			llog("clearCameraInstance called when there was no instance.");
			return;
		}
		
		this._cameraInstance.finalize();
		this._cameraInstance = null;
	},

	
	/**
	 * Quick check function to see whether there is anything in the  "frobs" list.
	 */
	_haveFrobs: function(){
		var haveFrobs = false;
		for(var f in this._frobs){
			if (null !== this._frobs[f]){
				haveFrobs = true;
			}
			break;
		}

		return haveFrobs;
	},

	/**
	 *
	 * deliver settings to the component.
	 * 
	 * @param {Object} prefs  Contains the necessary preferences for the 
	 *                        camera.  The structure must contain the values
	 *                        defined in CameraControl.PREFS
	 */
	setPrefs: function(prefs){
		this.prefs = prefs;

		if (CameraControl.__debug){
			if (!this.prefs){
				llog("Prefs is not set");
			}
			else {
				llog("prefs contains...");
				for (var key in prefs) {
					var obj = prefs[key];
					if (typeof(obj) != 'function'){
						llog(key + ": " + obj);
					}
				}
			}
				
			for(var key in CameraControl.PREFS){
				var k2 = CameraControl.PREFS[key];
				if (!this.prefs[k2]){
					llog("Missing preference: ", k2);
				}
			}
		}		
	},


	/**
	 * Called at start-up to instantiate the camera object 
	 * 
	 * @param {Object} elem   The HTML Element that should be used to
	 *                        display the camera preview.
	 */
	setupCamera: function(){
		var cam = this.getCameraInstance(true/*isSetup*/);

		if (cam){
			Mojo.Log.error("setupCamera: A camera instance already exists");
			llog("setupCamera: PREVIEW: ", cam.preview, " CAPTURE: ", cam.recording, " ERROR: ", cam.error, " LAST: ", cam.lastDestination, "/", cam.lastImageDestination);

			this.scene.controller.sceneElement.removeClassName("preview-off");
		}
		else if (this._frobs.setupCamera){
			llog("setupCamera: Set-up already started");
		}
		else {
			var session = {camera: null, state: 'createInstance'};
			
			this._frobs.setupCamera = session;

			session.title = "CameraControl._seqSetupCamera";
			CameraControl._seqSetupCamera.call(this);	
		}
	},
	
	/**
	 * Close the camera and remove the reference to it from CameraControl.
	 * 
	 * Call this from the scene clean-up method, so the scene can be removed and the CameraControl
	 * singlton can be re-used.
	 */
	closeCamera: function(){
		this.clearCameraInstance();

		this._frobs = {};
	},

	restartCamera: function(){
		if (this._frobs.setupCamera){
			llog("Trying to restart camera, but it is already starting...");
			return;
		}

		Mojo.Log.error("FIXME: CameraControl::restartCamera(): clearout _frobs");

		var cam = this.getCameraInstance();
		if (!!cam){
			cam.stop();
			cam.stopPreview();
			this.clearCameraInstance();	
		}

		var vidElem = this.scene.controller.get('capture-preview');
		if (!!vidElem){
			vidElem.parentNode.removeChild(vidElem);
		}
		else {
			Mojo.Log.error("restartCamera called when there was no video div in the page");
		}

		this.setupCamera();

	},
	
	setScene: function(appController, scene){
		this.appController = appController;
		this.scene = scene;
	},
	
	/**
	 * This is called when we get the 'onBlur' event
	 * 
	 * @private
	 */
	freezePreview: function(){
		var cam = this.getCameraInstance();

		// Update the URL on the scrim shot to the last captured picture.
		this.scene.setScrimImage();

		if (!cam) {
			llog("freezePreview: Camera instance not yet available.");
		}
		else {
			var captureMode = this.inCapture();
			if (CameraControl.IN_CAPTURE_VIDEO === captureMode){
				this._frobs.capture.freezePreview = true;

				// stop recording.  At end of recording sequence there is a check to see if 
				// we are focused or blurred, hence, if we're still blurred that point,
				// the capture sequence will call stop preview.
				this.videoCaptureStop();

				llog("freezePreview: Stopping video recording before stopping preview.");
			}
			else if (CameraControl.IN_CAPTURE_STILL === captureMode){
				this._frobs.capture.freezePreview = true;

				llog("freezePreview: Still capture active.");
			}
			else{
				this.removeCameraElement();
			} 
		}

		this.scene.controller.sceneElement.addClassName('preview-off');
			

		// We're not looking at the camera, so we don't need to keep the display on.
		this.scene.stopIdleCheck();
		
		if (this.scene.sublaunch){
			this.scene.captureComplete(false, null);
		}
	},
	
	
	/**
	 * This is called when we get the 'onFocus' event
	 * 
	 * @private
	 */
	resumePreview: function(){
		// Remove the last captured image, so we can see the progress of restarting the pipeline.
		var sshot = this.scene.controller.get('scrim-shot');
        var func = function() {
            sshot.style.display = "none";
            sshot.removeEventListener("webkitTransitionEnd", func);
        };
        
        sshot.addEventListener("webkitTransitionEnd", func);        
		sshot.style.opacity = "0.0";
		sshot.style.zIndex = "99";

		if (!!this._frobs.capture && this._frobs.capture.freezePreview){
			this._frobs.capture.freezePreview = false;
		}
		else{
			this.setupCamera();
		}
	},
	
	removeCameraElement: function(){
		llog("entering removeCameraElement");
		var retVal = false;

		var cam = this.getCameraInstance();
		if (cam){
			if (!!this._frobs.capture){
				llog("Capture was active when MediaCapture element removed");
			}

			if (cam.preview){
				cam.stopPreview();
			}			
		} 
		
		this.clearCameraInstance();
		
		// Get the video preview object, and hence the camera too, out of the DOM.
		var videoElem = this.scene.controller.get('capture-preview');
		var videoElemPar = videoElem.parentNode;
		videoElemPar.removeChild(videoElem);
		
		retVal = true;
		
		return retVal;
	},
	
	onDisplayOn: function(){
		llog("onDisplayOn: Attempt to restart camera");
		var cam = this.getCameraInstance();
		
		if (cam && cam.preview){
			llog("Camera preview is already running, doing nothing");
		}
		else {
			// Only restart if preview is currently off.  i.e., transition from off, not from dim.
			this.scene.controller.window.setTimeout(function(){
				// We need a short delay here to allow for the driver to come up
	
				if (!this.scene.isFocused()){
					llog("Don't have focus, not restarting preview");
				}
				else{ 
					this.restartCamera();
				}
			}.bind(this), 500);
		}
	},
	onDisplayOff: function(){
		this.freezePreview();
		GeoControl.stopTracking();
	},
	
	onMsmStart: function(){
		llog("onMsmStart: entering MSM");
		this.onDisplayOff();
	},
	onMsmStop: function(){
		llog("onMsmStop: Back to normal");

		this.onDisplayOn();
	},
	
	_onPreviewSuspend: function(event){
		llog("In _onPreviewSuspend", Object.toJSON(event));

		if (!this._haveFrobs()){
			if (event.type !== Camera.Event.PREVIEW){
				Mojo.Log.error("CameraControl::_onPreviewSuspend got event"+event.type+".  This can't happen!");
				return;
			}
			
			if (event.target.preview){
				// Preview is on, remove scrim
				this.scene.controller.sceneElement.removeClassName("preview-off");
			}
			else {
				// Preview is off, cover the ugly blackness
				this.scene.controller.sceneElement.addClassName("preview-off");
			}
			
		}
	},

	setupPreviewWatch: function(){
		// Watch for mediaserver preview off
		if (undefined === this._onPreviewSuspendHandler){
			this._onPreviewSuspendHandler = this._onPreviewSuspend.bind(this);
		}
		var cam = this.getCameraInstance();
		if (!!cam) {
			cam.addEventListener(Camera.Event.PREVIEW, this._onPreviewSuspendHandler, false);
		}
	},
	removePreviewWatch: function(){
		if (undefined === this._onPreviewSuspendHandler) {
			var cam = this.getCameraInstance();
			if (!!cam) {
				cam.removeEventListener(Camera.Event.PREVIEW, this._onPreviewSuspendHandler, false);
			}
		}
	},
	
	
	/**
	 * This callback is bound when the listener is added. 
	 */
	_onCameraDisconnect: function(){
		var haveFrobs = this._haveFrobs();
	
		if ( haveFrobs ){
			llog("Camera app is busy, but attempting reset anyway");
		}
	
		Mojo.Log.error("mediaserver disconnected");
		this.handleFatal();
	},
	
	_onGenericError: function(){
		if (this._haveFrobs()) {
			Mojo.Log.error("App is busy, error will be dealt with in active task");
		}
		else{
			this.handleFatal();
		}
	},
	
	/**
	 * No connection to mediaserver
	 * 
	 * Generally called when the mediaserver has become unresponsive or IPC has
	 * disconnected from the mediaserver.  
	 */
	handleFatal: function(){
		if (this.scene.sublaunch){
			// Called by another app to take a picture.  Pass back the details.
			this.scene.captureComplete(false, null);
		}
		else{
			// Some things need to be restored after restarting the scene.
			AppAssistant.restartParams = {
				captureMode: this.prefs[CameraControl.PREFS.CAPTURE_MODE]
			}

			this.clearCameraInstance();
			var stageController = this.scene.controller.stageController; 
			
			// We will relaunch this scene, after a delay.
			this.scene.controller.window.setTimeout(function(){ 
				this.pushScene(AppAssistant.SCENE_NAME_CAPTURE, {}); 
			}.bind(stageController), 500);

			// Back out this scene
			stageController.popScenesTo();
		}	
	}
});


CameraControl.PREFS = {
	CAPTURE_MODE:'CAPTURE_MODE',
	CAPTURETIME: 'LAST_CAPTURE_TIME',
	FIXTIME:     'GPS-FIX-TIME',
	FIXMAXAGE:   'GPS-FIX-MAX-AGE',
	FLASH:       'FLASH',
	GPSLOC:      'GPS-LOCATION',
	GEOMETER:    'GPS-ACCURACY-METER',
	GEOTAG:      'GEOTAGGING',
	HAPTIC:      'RECORDING_HAPTIC',
	IDLEMAX:     'CAPTURE_IDLE_TIME_MAX',
	MAKE:        'CAMERA-MAKE',
	MODEL:       'CAMERA-MODEL',
	REVIEW:      'CAPTURE_REVIEW_DELAY',
	SOUNDS:      'SOUNDS',
	WBAL:        'WHITE_BALANCE_DELAY'
};


// Any sequences that I have will timeout if they have not completed in this many millisecs.
CameraControl.SEQUENCE_TIMEOUT = 20000;

// Do extra stuff to debug.
//CameraControl.__debug = true;


/**
 * Used to progress through the actions needed to perform still capture.
 */
CameraControl._seqStillCapture = function(){
	if ( (!this._frobs)||(!this._frobs.capture) ) {
		Mojo.Log.error("Problem in CameraControl._seqStillCapture - no frob");
		return;
	}

	// 'this' will be the hash created in capture() containing the state necessary to perform the capture sequence
	var session = this._frobs.capture;
	
	var cam = session.camera;
	var cb = session.callback;

	var isDone = false;
	var captureFailed = false;
	
	var event;
	llog("In CameraControl._seqStillCapture.  State is: "+session.state);
	
	switch(session.state){
		case 'init':
		default:
			session._title = "CameraControl._seqStillCapture";

			// Initialize the callback to this function+context.
			session.callback = CameraControl._seqStillCapture.bind(this);
			cb = session.callback;


			session.errorCB = function(session){
				llog("Error hit!!");

				session.failState = session.state; 
				session.state = 'error'; 
				session.callback(); 
				
			}.bind(this, session);
			
			session.timeoutCB = function(session){
				llog("Timeout hit!!");

				session.failState = session.state; 
				session.state = 'timeout'; 
				session.callback(); 
			}.bind(this, session);


			CameraControl.addEventListenerWrapper(cam, Camera.Event.ERROR, session.errorCB, false, session, session.state);

			// Set-up a timeout, which will call this function, but force the state to 'timeout'				
			session.captureTimeout = this.scene.controller.window.setTimeout( session.timeoutCB, CameraControl.SEQUENCE_TIMEOUT);

			
			session.state = 'capture';
			session.callback.defer();

			break;



		case 'capture':
			// FIXME: lastImageDestination is firing on both edges!  Applying a little tape and joint compound (NOV-88022)  
			CameraControl.addEventListenerWrapper(cam, 'lastImageDestination', cb, false, session, session.state);

			session.state = 'imagesaved';

			// Create a date/time stamp in the format required for Exif
			var datetimeTag = CameraControl._formatDateTime(new Date());		

			var exifData = {
				make: this.prefs[CameraControl.PREFS.MAKE],
				model: Mojo.Environment.DeviceInfo.modelNameAscii,
				datetime: datetimeTag,
				orientation: this.exifOrientation||Exif.Orientation.UP,
				geotag:{}
			};


			if ('disabled' === this.prefs[CameraControl.PREFS.GEOTAG]) {
				llog("Geotagging disabled in camera's prefs.");
			}
			else {
				var gpsData = GeoControl.getLocation();
				var isStale = true;
				
				if ((gpsData) && (gpsData.time)) {
					if ('' === this.prefs[CameraControl.PREFS.FIXMAXAGE]) {
						isStale = false;
					}
					else {
						var age = new Date().getTime();
						age -= gpsData.time;
						age /= 1000;
						
						if (age < +this.prefs[CameraControl.PREFS.FIXMAXAGE]) {
							isStale = false;
						}
					}
				}
				
				if (isStale) {
					llog("Clear the GPS data");
				}
				else {
					llog("Setting GPS data");
					
					var exifLat = gpsData.latitude;
					var exifLong = gpsData.longitude;
					var exifLatRef = 'N';
					var exifLongRef = 'E';
					
					
					if ((+exifLat) < 0) {
						exifLatRef = 'S';
						exifLat = -exifLat;
					}
					if ((+exifLong) < 0) {
						exifLongRef = 'W';
						exifLong = -exifLong;
					}
					
					
					var latRationals = GeoControl.toDMS(exifLat);
					var longRationals = GeoControl.toDMS(exifLong);
					exifLat = [latRationals[0], 1, latRationals[1], 1, latRationals[2],1];
					exifLong = [longRationals[0], 1 , longRationals[1], 1, longRationals[2], 1];
					llog("Exif lat: ", exifLat, "; Exif long: ", exifLong);
					
					exifData.geotag = {
						version: '2.2.0.0',
						latitude: exifLat,
						latitudeRef: exifLatRef,
						longitude: exifLong,
						longitudeRef: exifLongRef
					};
					
				}
			} // GEOTAG 
			
			cam.exifData = Object.toJSON(exifData);
			llog("Set the following Exif Data: ", cam.exifData);

			cam.captureImage();

			break;



		case 'imagesaved':
			event = arguments[0];
			if ( (event) && (event.type) ){
				llog("capturecomplete got event for ", event.type);
			}

			if ('lastImageDestination' === event.type){
				if (cam.lastImageDestination && (""!== cam.lastImageDestination)){
	
					if (this.scene.captureBurst){
						// The user has held down the capture button for sooooo long that we have managed to 
						// save the image for the current capture.  They must be expecting us to do another capture
						// right away.  hence turn back on the preview, not that the user will see anything, 
						// however, capture requires a running preview.
						session.doBurst = true;
					}
					else {
						session.doBurst = false;
					}
	
					isDone = true;
				}
			}
			else {
				llog("imagesaved ignoring event");
			}
			break;



		case 'timeout':
			llog("Image capture timedout, expected state "+session.failState+" to be handled");
			delete session.captureTimeout;
			
			isDone = true;
			captureFailed = true;

			break;



		case 'error':
			isDone = true;
			captureFailed = true;
			break;
	}

	if (isDone){
		if (!!session.captureTimeout){
			this.scene.controller.window.clearTimeout(session.captureTimeout);
			session.captureTimeout = null;
		}

		CameraControl.removeEventListenerWrapper(cam, Camera.Event.ERROR, session.errorCB, false, session, session.state);

		CameraControl.removeEventListenerWrapper(cam, 'lastImageDestination', cb, false, session, session.state);

		if (CameraControl.__debug) {
			CameraControl.dumpEventListeners(session);
		}

		this._frobs.capture = null;

		if (captureFailed){
			Mojo.Log.error("Capture failed!");

			var errorMsg = "";
			if (Camera.Error.SAVE === cam.error){
				errorMsg = $L("Disk is full");
			}

			this.scene.controller.showAlertDialog({
				title: $L("Unable to take picture"),
				message: errorMsg,
				choices: [{label: $L("OK"), type: "dismiss"}],
				onChoose: function(){ this.handleFatal(); }.bind(this)				 	
			});
		}
		else {

			if (this.scene.sublaunch){
				// Called by another app to take a picture.  Pass back the details.
				this.scene.captureComplete(true, cam.lastImageDestination);
			}


			var seconds = new Date().getTime();
			seconds /= 1000;
			seconds = Math.floor(seconds);
	
			CameraPrefs.updatePref(this.prefs, CameraControl.PREFS.CAPTURETIME, seconds);
		}


		// It's valid for user to hit 'minimize' 
		// before we finished capture.  Accomodate that scenario.
		if (this.scene.isFocused()){
			// Only bring controls back if we're focused
	
			// Show the controls so people know we're actively capturing
			this.scene.controller.sceneElement.removeClassName("preview-off");
			llog("Controls are up");

		}
		else {
			// The pipeline will still be open (we never close pipeline while capture is active, 
			// so we must close it now.
			this.freezePreview();
			session.freezePreview = false;
		}
		
		// Check for burst mode.  Only do a subsequent capture if the still has focus
		if (!session.doBurst) {
			// Not burst mode, so remove the recording class.
			this.scene.controller.sceneElement.removeClassName('recording');
		}
		else {
			llog("Press and Hold ... Checking whether we should do another capture or put up the review frame...")
			if (this.scene.isFocused()){
				llog(" ... App still has focus, doing another capture");

				// Start capture on exit of this routine (plus a bit of a delay)
				this.stillCapture.bind(this).defer(); 
			}
			else {
				llog(" ... App has lost focus");
				this.scene.captureBurst = false;
			}
		}
	}		
};




/**
 * Used to progress through the actions needed to perform a capture.
 */
CameraControl._seqVideoCapture = function(){
	if ( (!this._frobs)||(!this._frobs.capture) ) {
		Mojo.Log.error("Problem in CameraControl._seqVideoCapture - no frob");
		return;
	}

	// 'this' will be the hash created in capture() containing the state necessary to perform the capture sequence
	var session = this._frobs.capture;
	
	var cam = session.camera;
	var cb = session.callback;

	var isDone = false;
	var captureFailed = false;
	
	var event;
	llog("In CameraControl._seqVideoCapture.  State is: "+session.state);
	
	switch(session.state){
		case 'init':
		default:
			session._title = "CameraControl._seqVideoCapture";

			// Initialize the callback to this function+context.
			session.callback = CameraControl._seqVideoCapture.bind(this);
			cb = session.callback;


			session.errorCB = function(session){
				llog("_seqVideoCapture: Error hit!!");

				session.failState = session.state; 
				session.state = 'error'; 
				session.callback(); 
				
			}.bind(this, session);
			
			session.timeoutCB = function(session){
				llog("_seqVideoCapture: timeout hit!!");

				session.failState = session.state; 
				session.state = 'timeout'; 
				session.callback(); 
			}.bind(this, session);

			session.progressCB = function(session){

				var stamp = (new Date()).getTime();
				stamp -= session.progressTimestamp;
				stamp %= 1000;
				stamp = 1000-stamp;

				session.progressTimer = this.scene.controller.window.setTimeout(session.progressCB, stamp);

				session.callback({type:'progress', target:session.camera});

			}.bind(this, session);
			
			CameraControl.addEventListenerWrapper(cam, Camera.Event.ERROR, session.errorCB, false, session, "CameraControl._seqVideoCapture error handler");

			session.progressTimer = this.scene.controller.window.setTimeout(session.progressCB, 1000);
			session.progressTimestamp = (new Date()).getTime();
			
			session.captureTimeout = this.scene.controller.window.setTimeout(session.timeoutCB, CameraControl.SEQUENCE_TIMEOUT);

			session.state = 'capture';
			session.callback.defer();

			break;   
			 
		case 'capture':
			CameraControl.addEventListenerWrapper(cam, Camera.Event.SAVE, cb, false, session, session.state);
			CameraControl.addEventListenerWrapper(cam, Camera.Event.STOP, cb, false, session, session.state);
			CameraControl.addEventListenerWrapper(cam, Camera.Event.RECORDING, cb, false, session, session.state);

			session.state = 'recording';
			
			var timeElem = this.scene.controller.get('capture-time');
			timeElem.innerHTML = CameraUtil.timeFormatter(0);
						
			// stash the capturetime element.  i update it every second, so i dont want to keep looking it up. 
			session.rtElem = timeElem;

			if (session.stop) {
				// Somehow someone hit stop before recording even got chance to start.
				// Don't start the recorder, instead transition to the next state, where 
				// the stop can be actioned and clear-up take place.
				session.callback.defer();
			}
			else {
				cam.videoTitle = Mojo.Format.formatDate( new Date(), { date: 'medium', time: 'short' });
				cam.start();
			}

			break;


		case 'recording':
			event = arguments[0]||{};

			if ( event.type ){
				llog(session.state, " got event ", event.type);
			}

			if (Camera.Event.SAVE === event.type){
				if (true !== session.camera.saved) {
					llog("Got a saved event, but it was false");
				}
				else {
					isDone = true;
				}
			}
			else if (Camera.Event.STOP === event.type){
				llog("Recording stopped ... waiting for 'save event");
				// TODO: Might want to set some kind of timeout here, 
				// just in case the SAVE event never arrives.  That would
				// be an error/failure.
			}
			else if (Camera.Event.RECORDING === event.type){
				if (cam.recording) {
					this.scene.controller.sceneElement.addClassName('recording');

					// Once the user starts recording, never show the "Turn your device round" message again. 
					this.scene.controller.get('rotate-container').setAttribute("style", "-webkit-transition-property: opacity, color; -webkit-transition-duration: 1s; opacity: 0; color: rgba(255, 255, 255, 0);");
					this.scene.controller.get('rotate-message').setAttribute("style", "-webkit-transition-property: opacity, color; -webkit-transition-duration: 1s; opacity: 0; color: rgba(255, 255, 255, 0);");


					// put the spinner on the photoroll icon to show it's not uptodate - 
					// it will get removed when the indexer returns the new video.
					this.scene.setPhotorollUpdating(true);
					
					// Recording started, remove the watchdog.
					if (session.captureTimeout){
						this.scene.controller.window.clearTimeout(session.captureTimeout);
						delete session.captureTimeout;
					}
					
					if ( (session.vibrate) && ("enabled" === this.prefs[CameraControl.PREFS.HAPTIC])){
						new Mojo.Service.Request(
							"palm://com.palm.vibrate",
							{
								method:"vibrate",
								parameters: { period:0, duration:100 }
							});						
					}
					
				}
				else {
					this.scene.controller.sceneElement.removeClassName('recording');
				}
			}
			else if ('progress' === event.type) {
				var rtTime = Math.floor(session.camera.currentTime);
				session.rtElem.innerHTML = CameraUtil.timeFormatter(rtTime);

				this.scene.resetIdleCheck();
			}
			
			// Always check this, regardless of event type.
			if (session.stop){
				session.state = 'record-stopping';

				// If we don't get a good exit then put up the fail dialog
				session.captureTimeout = this.scene.controller.window.setTimeout(session.timeoutCB, CameraControl.SEQUENCE_TIMEOUT);
				
				cam.stop();
			}
			break;
			
		case 'record-stopping':
			event = arguments[0]||{};

			if ( event.type ){
				llog(session.state, " got event ", event.type);
			}

			if (Camera.Event.SAVE === event.type){
				if (true !== session.camera.saved) {
					llog("Got a saved event, but it was false");
				}
				else {
					isDone = true;
				}
			}
			else if (Camera.Event.STOP === event.type){
				llog("Recording stopped ... waiting for 'save event");
				// TODO: Might want to set some kind of timeout here, 
				// just in case the SAVE event never arrives.  That would
				// be an error/failure.
			}
			else if (Camera.Event.RECORDING === event.type){
				if (cam.recording) {
					this.scene.controller.sceneElement.addClassName('recording');

					// put the spinner on the photoroll icon to show it's not uptodate - 
					// it will get removed when the indexer returns the new video.
					this.scene.setPhotorollUpdating(true);
					
					// Recording started, remove the watchdog.
					if (session.captureTimeout){
						this.scene.controller.window.clearTimeout(session.captureTimeout);
						delete session.captureTimeout;
					}
					
				}
				else {
					this.scene.controller.sceneElement.removeClassName('recording');
				}
			}
			else if ('progress' === event.type) {
				var rtTime = Math.floor(session.camera.currentTime);
				session.rtElem.innerHTML = CameraUtil.timeFormatter(rtTime);
			}
			
			break;
			

		case 'timeout':
			llog("Image capture timedout, expected state ", session.failState, " to be handled");
			delete session.captureTimeout;
			
			isDone = true;
			captureFailed = true;

			break;

		case 'error':
			isDone = true;
			captureFailed = true;
			break;
	}

	if (isDone){
		if (!!session.captureTimeout){
			this.scene.controller.window.clearTimeout(session.captureTimeout);
		}

		if (session.progressTimer){
			this.scene.controller.window.clearTimeout(session.progressTimer);			
		}

		CameraControl.removeEventListenerWrapper(cam, Camera.Event.SAVE, cb, false, session);
		CameraControl.removeEventListenerWrapper(cam, Camera.Event.STOP, cb, false, session);
		CameraControl.removeEventListenerWrapper(cam, Camera.Event.RECORDING, cb, false, session);

		if (CameraControl.__debug) {
			CameraControl.dumpEventListeners(session);
		}

		delete this._frobs.capture;
		
		if (captureFailed){
			Mojo.Log.error("Capture failed!");

			var errorMsg = "";
			if (Camera.Error.SAVE === cam.error){
				errorMsg = $L("Disk is full");
			}

			this.scene.controller.showAlertDialog({
				title: $L("Unable to capture video"),
				message: errorMsg,
				choices: [{label: $L("OK"), type: "dismiss"}],
				onChoose: function(){ this.handleFatal(); }.bind(this)				 	
			});
		}
		else {

			if (this.scene.sublaunch){
				// Called by another app to take a picture.  Pass back the details.
				this.scene.captureComplete(true, cam.lastDestination);
			}


			var seconds = new Date().getTime();
			seconds /= 1000;
			seconds = Math.floor(seconds);
	
			CameraPrefs.updatePref(this.prefs, CameraControl.PREFS.CAPTURETIME, seconds);
		}
		



		// It's valid for user to hit 'minimize' 
		// before we finished capture.  Accomodate that scenario.
		if (this.scene.isFocused()){
			// Only bring controls back if we're focused
	
			// Show the controls so people know we're actively capturing
			this.scene.controller.sceneElement.removeClassName("preview-off");
			llog("Controls are up");

		}
		else {
			// Was carded, remove the camera now that capture has gracefully completed. 
			this.freezePreview();
			session.freezePreview = false;
		}
		
	}		
};
	

CameraControl._seqSetupCamera = function(){

	var event;
	if (0 !== arguments.length) {
		event = arguments[0];
		if (event.type){
			llog("CameraControl._seqSetupCamera got event "+event.type);
		}
		
		if (event.data){
			llog("CameraControl._seqSetupCamera, got event data "+Object.toJSON(event.data));
		}
	}
	

	if ( (!this._frobs)||(!this._frobs.setupCamera) ) {
		llog("************* Problem in CameraControl._seqSetupCamera - no frob");
		return;
	}

		
	var session = this._frobs.setupCamera;
	var cam = session.camera;
	var cb = session.callback;
	
	var isDone = false;
	var setupFailed = false;

	llog("_seqSetupCamera state - "+session.state);

	
	switch (session.state){

		case 'createInstance':
			session.state = 'startpreview';
			cb = session.callback = CameraControl._seqSetupCamera.bind(this);

			session.timeoutCB = function(session){
				llog("_seqSetupCamera timeout triggered");
				session.failedState = session.state;
				session.state = 'timeout';
				session.callback();
			}.bind(this, session);

			var vidParElem = this.scene.controller.get('capture-preview-container');
			var doc = vidParElem.ownerDocument;
			var vidElem = doc.createElement('video');
			for (var idx = 0; vidParElem.attributes.length != idx; ++idx){
				// I stashed some attributes for the camera onto it's parent element in the div - just for sake of laziness
				// copy them across now. 
				var n = vidParElem.attributes[idx].nodeName;
				var v = vidParElem.attributes[idx].nodeValue;

				if (0 === n.indexOf("xxx-")){
					var n2 = n.substr("xxx-".length);
					vidElem.setAttribute(n2, v);
				}
			}
			vidParElem.appendChild(vidElem);


			try {
				Media.MediaCapture.getInstance(session.callback, AppProperties.VideoCapture.INSTANCE_PARAMS);
			}
			catch(e){
				llog("MediaCapture.getInstance threw: ", Object.toJSON(e));
				
				isDone = true;
				setupFailed = true;
			}
			
			// Watchdog to stop us getting stuck if no response from mediaserver
			session.setupTimeout = this.scene.controller.window.setTimeout(session.timeoutCB, CameraControl.SEQUENCE_TIMEOUT);

			session._title = "CameraControl._seqSetupCamera";

			break;

		case 'startpreview':
			session.camera = cam = arguments[0];
			if ( (undefined === cam) || (null === cam) ){
				isDone = true;
				setupFailed = true;
			}
			else {
			
				session.errorCB = function(session){
					session.failedState = session.state;
					session.state = "error";
					session.callback();
				}.bind(this, session);
				CameraControl.addEventListenerWrapper(cam, Camera.Event.ERROR, session.errorCB, false, session, "_seqSetupCamera error handler");


				if (!this._onCameraDisconnectHandler) {
					this._onCameraDisconnectHandler = this._onCameraDisconnect.bind(this);
				}
				if (!this._onGenericErrorHandler){
					this._onGenericErrorHandler = this._onGenericError.bind(this);
				}

				cam.addEventListener(Camera.Event.X_PALM_DISCONNECT, this._onCameraDisconnectHandler, false);
				cam.addEventListener(Camera.Event.ERROR, this._onGenericErrorHandler, false);
		
				this.setCameraInstance(cam);

				// Let the mediaserver choose the destination for capture
				cam.destination = null;

				// still capture specific settings
				cam.quality = 90;
				cam.reviewDuration = +this.prefs[CameraControl.PREFS.REVIEW];
				

				if (!this.scene.isFocused()) {
					isDone = true;					
				}
				else {
					// Only start the preview if we have focus.

					if (cam.preview){
						// Preview already active, no need to start.  If we have another state set 
						// it here, or go to 'done',
						isDone = true;
					}
					else {
						CameraControl.addEventListenerWrapper(cam, Camera.Event.PREVIEW, cb, false, session, "in _seqSetupCamera/startpreview");

						session.state = "preview";
						cam.startPreview();
					}				
				}
			}
			break;


			
		case 'preview':
			
			if (cam.preview) {
				// ensure it was a <<preview === true>> transition.		
				isDone = true;
			}
			else {
				llog("_seqSetupCamera, in preview state: Waiting for preview to go true before continuing setup");
			}
			break;

		case 'timeout':
			llog("Hit timeout waiting for state "+session.failedState);
			delete session.setupTimeout;
			
			setupFailed = true;
			isDone = true;
			break;


		case 'error':
			llog("Hit error waiting for state "+session.failedState);
			setupFailed = true;
			isDone = true;
			break;
			
		default:
			llog("_seqSetupCamera called without a state");
			isDone = true;
			setupFailed = true;
			break;
	}
	
	if (isDone){
		if (!!cam) {
			CameraControl.removeEventListenerWrapper(cam, Camera.Event.PREVIEW, cb, false, session);
			CameraControl.removeEventListenerWrapper(cam, Camera.Event.ERROR, session.errorCB, false, session);
		}

		if (session.setupTimeout){
			this.scene.controller.window.clearTimeout(session.setupTimeout);
			delete session.setupTimeout;
		}


		if (CameraControl.__debug) {
			CameraControl.dumpEventListeners(session);
		}

		if ( !this.scene.isFocused() ){
			if (this.scene.sublaunch) {
				// Normally we wouldn't get here, but there is a possibility 
				// that when we're sublaunched and carded that we do.
				this.scene.captureComplete(false, null);
			}
			else {
				if (cam.preview) {
					// Camera not focused, but preview on.  Turn it off.
					this.scene.controller.window.setTimeout(function(){ this.freezePreview(); }.bind(this), 10);
				}
			}
		}


		delete this._frobs.setupCamera;				

		if (setupFailed){
			this.handleFatal();
			Mojo.Log.error("Error initializing camera");
		}
		else {
			// Set-up all the other stuff, like prefs, GPS, shutter sound, etc.
			this.scene.deferredSetup();
		}

	}
	
	
};


/**
 * @private
 * @param {Date} date
 */
CameraControl._formatDateTime = function(date){
	var YYYY = date.getFullYear();
	var MM = (1+date.getMonth()); if (MM<10) { MM = "0"+MM; }
	var DD = (0+date.getDate()); if (DD<10) { DD = "0"+DD; }

	var hh = date.getHours(); if (hh < 10) { hh = "0"+hh; }
	var mm = date.getMinutes(); if (mm < 10) { mm = "0"+mm; }
	var ss = date.getSeconds(); if (ss < 10) { ss = "0"+ss; }

	var datetimeTag = "";
	datetimeTag += YYYY;
	datetimeTag += ":";
	datetimeTag += MM;
	datetimeTag += ":";
	datetimeTag += DD;
	datetimeTag += " ";
	datetimeTag += hh;
	datetimeTag += ":";
	datetimeTag += mm;
	datetimeTag += ":";
	datetimeTag += ss;
	
	return datetimeTag;		
};	



CameraControl.addEventListenerWrapper = function(obj, evt, cb, dir, session, msg){
	obj.addEventListener(evt, cb, dir);
	
	if (CameraControl.__debug){
		llog("adding listener for ", evt, " in ", session._title);

		if (!session._evtListeners){
			llog("  adding evtListeners structure");
			session._evtListeners = {};
		}
	
		var tmp = {count:0, msg:""};
	
		if (session._evtListeners[evt]){
			tmp = session._evtListeners[evt];
		}
		++tmp.count;
		tmp.msg = msg||tmp.msg;
		session._evtListeners[evt] = tmp;
	
		llog("   ", session._evtListeners[evt].count, " listeners now");
	}
};
CameraControl.removeEventListenerWrapper = function(obj, evt, cb, dir, session){

	obj.removeEventListener(evt, cb, dir);

	if (CameraControl.__debug){
		llog("removing listener for ", evt, " in ", session._title);
	
		if ( (session) && (session._evtListeners) && (session._evtListeners[evt]) ){
			--session._evtListeners[evt].count;
			llog("   ", session._evtListeners[evt].count, " listeners now");
			if (0 >= session._evtListeners[evt].count){
				llog("No more listeners for ", evt, " in ", session._title);
				delete session._evtListeners[evt];
			}
		}
		else{
			llog("   there were no listeners!");
		}
	}	
};
CameraControl.dumpEventListeners = function(session){
	if (CameraControl.__debug) {
	
		llog("***** dumpEventListeners: Dumping addEventListener hash keys in ",  session._title);
		
		for (var key in session._evtListeners) {
			llog(key, "(", session._evtListeners[key].count, "): ", session._evtListeners[key].msg);
		}
		llog("***** dumpEventListeners:    dumped!");
		
	}
}


CameraControl.__debug = false;
CameraControl.IN_CAPTURE_STILL = 1;
CameraControl.IN_CAPTURE_VIDEO = 2;
	 
