// A Module for camera utilites.
//
// This file groups useful functions to wrap a camera
// This includes:
//	_ Camera manipulation using the mouse (simply create the manipulator and forward the mouse events to its functions)
//
//
o3djs.provide( 'patapi.camera' )

o3djs.require('o3djs.math');

patapi.camera = patapi.camera || {};

var Math3D = o3djs.math;	// O3D Math


// Class to hold Camera Manipulator information
//
patapi.cameraManipulator = function( _ViewMatrixUpdateCallback, _CameraPosition, _TargetPosition, _Up )
{
	// Initialize members
	this.m_UpdateCallback = _ViewMatrixUpdateCallback;
	this.m_CameraTransform = Math3D.matrix4.identity();
	this.m_CameraTargetDistance = 0.0;
	this.m_NormalizedTargetDistance = 0.0;
	this.m_ButtonsDown = 0;
	this.m_bPushingTarget = false;
	this.m_bLastManipulationWasFirstPerson = false;
	this.m_bUseAlt = true;
	
	// Camera manipulation parameters
	this.m_ManipulationRotationSpeed	= 1.0;
	this.m_ManipulationPanSpeed			= 1.0;
	this.m_ManipulationZoomSpeed		= 0.8;
	this.m_ManipulationZoomAcceleration	= 0.8;


	// Initialize data
	this.Initialize( _CameraPosition, _TargetPosition, _Up );
};

patapi.cameraManipulator.prototype = 
{
	// Initializes the manipulator with the given camera and target positions
	//
	Initialize : function( _CameraPosition, _TargetPosition, _Up )
	{
		// Build the camera matrix
		var At = Math3D.subVector( _CameraPosition, _TargetPosition );	// The At is actually misnamed as it points from the target toward the camera
		var	TempCameraTargetDistance = Math3D.lengthSquared( At );
		
		if ( TempCameraTargetDistance > 1e-2 )
		{	// Normal case
			TempCameraTargetDistance = Math.sqrt( TempCameraTargetDistance );
			At = Math3D.divVectorScalar( At, TempCameraTargetDistance );
		}
		else
		{	// Special bad case
			TempCameraTargetDistance = 0.01;
			At = [0.0, 0.0, 1.0];
		}

		var	Ortho = Math3D.normalize( Math3D.cross( _Up, At ) );
			
		var	CameraMat = [	Ortho.concat( 0 ),							// RIGHT
							Math3D.cross( At, Ortho ).concat( 0 ),		// UP
							At.concat( 0 ),								// AT
							_CameraPosition.concat( 1 ) ];				// TRANS


		this.m_CameraTargetDistance = TempCameraTargetDistance;
		this.setCameraTransform( CameraMat );

		// Setup the normalized target distance
		this.m_NormalizedTargetDistance = this.NormalizeTargetDistance_( this.getCameraTargetDistance() );
	},

	// Attaches the manipulator to O3D mouse events
	//
	AttachToEvents : function( _O3DWebElement )
	{
		var TempManipulator = this;

		o3djs.event.addEventListener( _O3DWebElement, 'mousedown', function( _Event ) { TempManipulator.MouseDown( _Event ); } );
		o3djs.event.addEventListener( _O3DWebElement, 'mousemove', function( _Event ) { TempManipulator.MouseMove( _Event ); } );
		o3djs.event.addEventListener( _O3DWebElement, 'mouseup', function( _Event ) { TempManipulator.MouseUp( _Event ); } );
		o3djs.event.addEventListener( _O3DWebElement, 'wheel', function( _Event ) { TempManipulator.MouseWheel( _Event ); } );
	},


	////////////////////////////////////////////////////////////////////////////
	// PROPERTIES
	////////////////////////////////////////////////////////////////////////////
	
	// Gets or sets the CameraTransform matrix
	//
	getCameraTransform : function() { return Math3D.copyMatrix( this.m_CameraTransform ); },
	setCameraTransform : function( value )
	{
		this.m_CameraTransform = Math3D.copyMatrix( value );

		// Notify of the update
		// NOTE: We pass the inverse of the camera matrix as the update takes the "View" matrix
		//	which is, according to standards, the transform from WORLD to CAMERA
		this.m_UpdateCallback( Math3D.inverse4( value ) );
	},

	// Gets the CameraTargetTransform matrix
	//
	getCameraTargetTransform : function()
	{
		var Result = Math3D.matrix4.identity();
			Result[3] = Math3D.addVector( this.getCameraTransform()[3].slice( 0, 3 ), Math3D.mulScalarVector( -this.getCameraTargetDistance(), this.getCameraTransform()[2].slice( 0, 3 ) ) ).concat( 1 );

		return	Result;
	},

	// Gets or sets the distance from camera to target
	//
	getCameraTargetDistance : function()	{ return this.m_CameraTargetDistance; },
	setCameraTargetDistance : function( value )
	{
		var TargetMat = this.getCameraTargetTransform();	// Get current target matrix before changing distance
			
		this.m_CameraTargetDistance = value;

		// Move the camera along its axis to match the new distance
		var Temp = this.getCameraTransform();			// Get current camera matrix before changing distance
			Temp[3] = Math3D.addVector( TargetMat[3].slice( 0, 3 ), Math3D.mulScalarVector( value, this.getCameraTransform()[2].slice( 0, 3 ) ) ).concat( 1 );

		this.setCameraTransform( Temp );
		
		this.m_NormalizedTargetDistance = this.NormalizeTargetDistance_( this.m_CameraTargetDistance );
	},


	// Gets or sets the "Linux" flag
	//
	getUseAlt : function()				{ return this.m_bUseAlt; },
	setUseAlt : function( value )		{ this.m_bUseAlt = value; },


	// Gets the "manipulable" state
	CanManipulate_ : function( _Event )	{ return !this.m_bUseAlt || _Event.altKey; },


	////////////////////////////////////////////////////////////////////////////
	// EVENTS HANDLERS
	////////////////////////////////////////////////////////////////////////////

	// Handles the mouse down event
	MouseDown : function( _Event )
	{
 		if ( !this.CanManipulate_( _Event ) || _Event.type != 'mousedown' )
 			return;

		this.SaveCurrentState_( _Event.x, _Event.y );
		
		this.m_ButtonsDown |= 1 << _Event.button;	// Add this button

//		this.PrintDebug( "DOWN MouseButtonsDown = " + this.m_ButtonsDown )
	},

	// Handles the mouse up event
	MouseUp : function( _Event )
	{
 		if ( _Event.type != 'mouseup' )
 			return;
 			
		this.SaveCurrentState_( _Event.x, _Event.y );
		
		if ( this.CanManipulate_( _Event ) )
			this.m_ButtonsDown &= ~(1 << _Event.button);	// Remove only this button
		else
			this.m_ButtonsDown = 0;							// Remove all buttons

//		this.PrintDebug( "UP MouseButtonsDown = " + this.m_ButtonsDown )
	},

	// Handles the mouse move event
	MouseMove : function( _Event )
	{
		if ( !this.CanManipulate_( _Event ) || _Event.type != 'mousemove' )
			return;
		if ( this.m_ButtonDownTransform == null )
			return;		// Can't manipulate...

		var	MousePos = this.ComputeNormalizedScreenPosition_( _Event.x, _Event.y );

//		this.PrintDebug( "MOVE MouseButtonsDown = " + this.m_ButtonsDown )

// 		this.PrintDebug2(	"CameraTransform = " + this.getCameraTransform() + "\n" +
// 							"TargetTransform = " + this.getCameraTargetTransform() + "\n" +
// 							"TargetDistance = " + this.getCameraTargetDistance() );

		// Check for FIRST PERSON switch
		if ( this.m_bLastManipulationWasFirstPerson ^ _Event.shiftKey )
		{	// There was a switch so we need to copy the current matrix and make it look like the button was just pressed...
			this.SaveCurrentState_( _Event.x, _Event.y );
		}
		this.m_bLastManipulationWasFirstPerson = _Event.shiftKey;

		// Actual handling
		if ( !_Event.shiftKey )
		{
			//////////////////////////////////////////////////////////////////////////
			// MAYA MANIPULATION MODE
			//////////////////////////////////////////////////////////////////////////
			//
			switch ( this.m_ButtonsDown )
			{
				// ROTATE
				case	1:		// LEFT BUTTON
				{
					var	fAngleX = (this.m_ButtonDownMousePosition[1] - MousePos[1]) * 2.0 * Math.PI * this.m_ManipulationRotationSpeed;
					var	fAngleY = (this.m_ButtonDownMousePosition[0] - MousePos[0]) * 2.0 * Math.PI * this.m_ManipulationRotationSpeed;

					// Compose the rotation matrix
					var	RotX = Math3D.matrix4.axisRotation( Math3D.negativeVector( this.m_ButtonDownTransform[0] ), fAngleX );
					var	RotY = Math3D.matrix4.axisRotation( [0.0, 1.0, 0.0], fAngleY );
					var	Rot = Math3D.mulMatrixMatrix4( RotX, RotY );

					// Apply the rotation
					var	Rotated = Math3D.mulMatrixMatrix4( this.m_ButtonDownTransform, this.m_InvButtonDownTargetMatrix );	// Transform camera into TARGET space
						Rotated = Math3D.mulMatrixMatrix4( Rotated, Rot );													// Apply rotation about target
						Rotated = Math3D.mulMatrixMatrix4( Rotated, this.m_ButtonDownTargetMatrix );						// Transform back into WORLD space

					this.setCameraTransform( Rotated );
					

					break;
				}

				// DOLLY => Simply translate along the AT axis
				case	4:		// RIGHT BUTTON
				case	1 | 2:	// LEFT + MIDDLE BUTTONS
				{
					var	fTrans = this.m_ButtonDownMousePosition[0] - this.m_ButtonDownMousePosition[1] - MousePos[0] + MousePos[1];

					this.m_NormalizedTargetDistance = this.m_ButtonDownNormalizedTargetDistance + 4.0 * this.m_ManipulationZoomSpeed * fTrans;
					var	fTargetDistance = Math.sign( this.m_NormalizedTargetDistance ) * this.DeNormalizeTargetDistance_( this.m_NormalizedTargetDistance );
					if ( fTargetDistance > 0.1 )
					{	// Okay! We're far enough so we can reduce the distance anyway
						this.setCameraTargetDistance( fTargetDistance );
						this.m_bPushingTarget = false;
					}
					else
					{	// Too close! Let's move the camera forward and clamp the target distance... That will push the target along.
						this.m_CameraTargetDistance = 0.1;
						this.m_NormalizedTargetDistance = this.NormalizeTargetDistance_( this.m_CameraTargetDistance );

						if ( !this.m_bPushingTarget )
						{	// First push! Store data
							this.m_ButtonDownNormalizedTargetDistance = this.m_NormalizedTargetDistance;
							fTrans = 0.0;
							this.m_bPushingTarget = true;
						}

						this.m_ButtonDownMousePosition = MousePos;

						// Translate the camera
						var	DollyCam = this.getCameraTransform();
							DollyCam[3] = Math3D.addVector( DollyCam[3].slice( 0, 3 ), Math3D.mulScalarVector( 2.0 * this.m_ManipulationZoomSpeed * fTrans, DollyCam[2].slice( 0, 3 ) ) ).concat( 1 );

						this.setCameraTransform( DollyCam );
					}
					break;
				}

				// PAN
				case	2:		// MIDDLE BUTTON
				{
					var	fTransFactor = this.m_ManipulationPanSpeed * Math.max( 2.0, this.getCameraTargetDistance() );

					var	Trans = [	(this.m_ButtonDownMousePosition[0] - MousePos[0]) * fTransFactor,
									(this.m_ButtonDownMousePosition[1] - MousePos[1]) * fTransFactor,
									0.0,
									0.0
								];

					// Make the camera pan
					var	PanCam = Math3D.copyMatrix( this.m_ButtonDownTransform );
						PanCam[3] = Math3D.addVector( PanCam[3], Math3D.mulVectorMatrix( Trans, this.m_ButtonDownTransform ) );

					this.setCameraTransform( PanCam );
					
					break;
				}
			}
		}
		else
		{
			var CameraMatrixBeforeBaseCall = this.getCameraTransform();
		
			//////////////////////////////////////////////////////////////////////////
			// UNREAL MANIPULATION MODE
			//////////////////////////////////////////////////////////////////////////
			//
			switch ( this.m_ButtonsDown )
			{
				// TRANSLATE IN THE ZX PLANE (WORLD SPACE)
				case	1:		// LEFT BUTTON
				{
					var	fTransFactor = this.m_ManipulationPanSpeed * Math.max( 4.0, this.getCameraTargetDistance() );

					// =============== Compute translation in the view direction =============== 
					var	Trans = CameraMatrixBeforeBaseCall[2];
						Trans[1] = 0.0;	// Clear the Y component of the vector
					var fSLength = Math3D.lengthSquared( Trans );
					
					if ( fSLength < 1e-4 )
					{	// The camera is certainly pointing toward the ground, Z is not usable => Better use Y instead...
						Trans = CameraMatrixBeforeBaseCall[1];
						Trans[1] = 0.0;
						fSLength = Math3D.lengthSquared( Trans );
					}

					Trans = Math3D.mulScalarVector( 1.0 / Math.sqrt( fSLength ), Trans );

					var	NewPosition = Math3D.addVector( CameraMatrixBeforeBaseCall[3], Math3D.mulScalarVector( fTransFactor * (this.m_ButtonDownMousePosition[1] - MousePos[1]), Trans ) ).slice( 0, 3 ).concat( 1 );

					this.m_ButtonDownMousePosition[1] = MousePos[1];	// Translation is a cumulative operation in that mode...

					// =============== Compute rotation about the Y WORLD axis =============== 
					var	fAngleY = (this.m_ButtonDownMousePosition[0] - MousePos[0]) * 2.0 * Math.PI * this.m_ManipulationRotationSpeed * 0.2;	// [PATAPATCH] Multiplied by 0.2 as it's REALLY too sensitive otherwise!
					if ( this.m_ButtonDownTransform[1][1] < 0.0 )
						fAngleY = -fAngleY;		// Special "head down" case...

					var	RotY = Math3D.matrix4.rotationY( fAngleY );

					var	FinalMatrix = Math3D.copyMatrix( this.m_ButtonDownTransform );
						FinalMatrix[3] = [0,0,0,1];			// Clear translation...
						FinalMatrix = Math3D.mulMatrixMatrix4( FinalMatrix, RotY );
						FinalMatrix[3] = NewPosition;

					this.setCameraTransform( FinalMatrix );

					break;
				}

				// ROTATE ABOUT CAMERA
				case	4:		// RIGHT BUTTON
				{
					var fAngleY = (this.m_ButtonDownMousePosition[0] - MousePos[0]) * 2.0 * Math.PI * this.m_ManipulationRotationSpeed;
					var fAngleX = (this.m_ButtonDownMousePosition[1] - MousePos[1]) * 2.0 * Math.PI * this.m_ManipulationRotationSpeed;

					var	Euler = Math3D.matrix4.getEulerAngles( this.m_ButtonDownTransform );

					var	RotateMatrix = Math3D.matrix4.rotationZYX( [Euler[0] - fAngleX, Euler[1] + fAngleY, Euler[2]] );
						RotateMatrix[3] = this.getCameraTransform()[3];

					this.setCameraTransform( RotateMatrix );

					break;
				}

					// Translate in the ( Z-world Y-camera ) plane
				case	2:		// MIDDLE BUTTON
				case	1 | 4:	// LEFT + RIGHT BUTTONS
				{
					var	fTransFactor = this.m_ManipulationPanSpeed * Math.max( 4.0, this.getCameraTargetDistance() );

					var	NewMatrix = Math3D.copyMatrix( this.m_ButtonDownTransform );
						NewMatrix[3] = Math3D.addVector( NewMatrix[3], Math3D.mulScalarVector( fTransFactor * (this.m_ButtonDownMousePosition[1] - MousePos[1]), [0.0, 1.0, 0.0, 0.0] ) );
						NewMatrix[3] = Math3D.addVector( NewMatrix[3], Math3D.mulScalarVector( fTransFactor * (this.m_ButtonDownMousePosition[0] - MousePos[0]), this.m_ButtonDownTransform[0] ) );

					this.setCameraTransform( NewMatrix );

					break;
				}
			}
		};
	},

	// Handles the mouse wheel event
	MouseWheel : function( _Event )
	{
		var	deltaY = Math.sign( _Event.deltaY ) * 120;	// Use the Windows default value

		this.m_NormalizedTargetDistance -= 0.004 * this.m_ManipulationZoomSpeed * deltaY;

		var fTargetDistance = this.DeNormalizeTargetDistance_( this.m_NormalizedTargetDistance );
		if ( fTargetDistance > 0.1 )
		{	// Okay! We're far enough so we can reduce the distance anyway
			this.setCameraTargetDistance( fTargetDistance );
		}
		else
		{	// Too close!
			// Clamp distance
			this.m_CameraTargetDistance = 0.1;
			this.m_NormalizedTargetDistance = this.NormalizeTargetDistance_( this.m_CameraTargetDistance );

			// Let's move the camera forward without changing the target distance...
			var DollyCam = this.getCameraTransform();
				DollyCam[3] = Math3D.addVector( DollyCam[3].slice( 0, 3 ), Math3D.mulScalarVector( -0.004 * this.m_ManipulationZoomSpeed * deltaY, DollyCam[2].slice( 0, 3 ) ) ).concat( 1 );

			this.setCameraTransform( DollyCam );
		}

		// Update "cached" data
		this.MouseDown( _Event );
	},


	////////////////////////////////////////////////////////////////////////////
	// HELPERS
	////////////////////////////////////////////////////////////////////////////

	SaveCurrentState_ : function( _ClientX, _ClientY )
	{
		// Keep a track of the mouse and camera states when button was pressed
		this.m_ButtonDownTransform = this.getCameraTransform();
		this.m_ButtonDownTargetMatrix = this.getCameraTargetTransform();
		this.m_InvButtonDownTargetMatrix = Math3D.inverse4( this.m_ButtonDownTargetMatrix );
 		this.m_ButtonDownCameraTargetDistance = this.getCameraTargetDistance();
 		this.m_ButtonDownMousePosition = this.ComputeNormalizedScreenPosition_( _ClientX, _ClientY );
 		this.m_ButtonDownNormalizedTargetDistance = this.NormalizeTargetDistance_( this.m_ButtonDownCameraTargetDistance );
	},

	ComputeNormalizedScreenPosition_ : function( _X, _Y )
	{
		var WIDTH = 1024.0;
		var HEIGHT = 768.0;
		var ASPECT_RATIO = WIDTH / HEIGHT;

		return [ASPECT_RATIO * (2.0 * _X - WIDTH) / WIDTH, 1.0 - 2.0 * _Y / HEIGHT];
	},

	// (De)Normalization of the target distance is used to simulate an "acceleration" of the camera zoom based on distance to target (i.e. non linear zoom)
	// This attempts to slow down the camera when it gets closer to the target so we obtain a fine motion when fully zoomed in
	// On the other hand, it tends to accelerate when the camera gets away from the target so we can use ample movements when viewing large portions of a scene
	//
	GetDenormalizationFactor_ : function()
	{
		var	TARGET_DISTANCE_DENORMALIZED_MAX = 100.0;
		var TARGET_DISTANCE_POWER			 = 4.0;
		
		var	fMaxDeNormalizedDistance = TARGET_DISTANCE_DENORMALIZED_MAX / this.m_ManipulationZoomSpeed;			// Here, we reduce the max denormalized distance based on the zoom speed
		var	fMaxNormalizedDistance = fMaxDeNormalizedDistance * (1.0 - this.m_ManipulationZoomAcceleration);	// This line deduces the max normalized distance from the max denormalized distance
		
		return	fMaxDeNormalizedDistance / Math.pow( fMaxNormalizedDistance, TARGET_DISTANCE_POWER );
	},

	NormalizeTargetDistance_ : function( _fDeNormalizedTargetDistance )
	{
		var TARGET_DISTANCE_POWER			 = 4.0;

		return	Math.pow( _fDeNormalizedTargetDistance / this.GetDenormalizationFactor_(), 1.0 / TARGET_DISTANCE_POWER );
	},

	DeNormalizeTargetDistance_ : function( _fNormalizedTargetDistance )
	{
		var TARGET_DISTANCE_POWER = 4.0;

		return	this.GetDenormalizationFactor_() * Math.pow( _fNormalizedTargetDistance, TARGET_DISTANCE_POWER );
	}
};

