// 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_firstperson_walk' )

o3djs.require('o3djs.math');

patapi.camera = patapi.camera || {};

var Math3D = o3djs.math;	// O3D Math


// Class to hold Camera Manipulator information
//
patapi.cameraFirstPersonWalk = function( _ViewMatrixUpdateCallback, _CharacterHeight, _CharacterPosition, _ForwardAxis, _optCharacterWalkSpeed, _optCharacterTurnSpeed )
{
	_ForwardAxis = Math3D.normalize( _ForwardAxis );
	
	// Initialize members
	this.m_UpdateCallback = _ViewMatrixUpdateCallback;

	this.m_CharacterHeight = _CharacterHeight;
	this.m_CharacterPosition = _CharacterPosition;
	this.m_CharacterAngle = Math.atan2( Math3D.dot( _ForwardAxis, [1,0,0] ), Math3D.dot( _ForwardAxis, [0,0,1] ) );
	this.m_ViewAngleX = 0.0;
	this.m_ViewAngleY = 0.0;

	this.m_CharacterWalkSpeed = _optCharacterWalkSpeed ? _optCharacterWalkSpeed : 1.0;
	this.m_CharacterTurnSpeed = _optCharacterTurnSpeed ? _optCharacterTurnSpeed : 1.0;

	this.m_ButtonsDown = 0;
	this.m_bFrozen = false;
	
	// Camera manipulation parameters
	this.m_QWERTY = false;
	this.m_ManipulationZoomSpeed = 1.0;
	this.m_MaxHeadTurnX = 0.5 * Math.PI;
	this.m_MaxHeadTurnY = 0.45 * Math.PI;

	// Keys controlling direction
	this.m_KeysDown = {};
	this.m_Walk = 0;
	this.m_Turn = 0;

	// Browser-specific offsets
	this.m_IE = o3djs.base.IsMSIE();
};

patapi.cameraFirstPersonWalk.prototype = 
{
	////////////////////////////////////////////////////////////////////////////
	// PROPERTIES
	////////////////////////////////////////////////////////////////////////////
	
	// Gets or sets QWERTY mode
	getQWERTY : function()			{ return this.m_QWERTY; },
	setQWERTY : function( _Value )	{ this.m_QWERTY = _Value; },

	// Gets the currently pressed buttons
	getButtonsDown : function()		{ return this.m_ButtonsDown; },

	// Gets the left button state
	getButtonLeft : function()		{ return this.m_ButtonsDown & 1; },

	// Gets the middle button state
	getButtonMiddle : function()	{ return this.m_ButtonsDown & 2; },

	// Gets the right button state
	getButtonRight : function()		{ return this.m_ButtonsDown & 4; },

	// Gets the last CAMERA->WORLD matrix
	getCamera2World : function()	{ return this.m_CameraMatrix; },


	////////////////////////////////////////////////////////////////////////////
	// METHODS
	////////////////////////////////////////////////////////////////////////////

	// Attaches the manipulator to O3D mouse events
	//
	AttachToEvents : function( _Document, _O3DWebElement )
	{
		var that = this;

		// Mouse events
		o3djs.event.addEventListener( _O3DWebElement, 'mousedown', function( _Event ) { that.MouseDown( _Event ); } );
		o3djs.event.addEventListener( _O3DWebElement, 'mousemove', function( _Event ) { that.MouseMove( _Event ); } );
		o3djs.event.addEventListener( _O3DWebElement, 'mouseup', function( _Event ) { that.MouseUp( _Event ); } );
		o3djs.event.addEventListener( _O3DWebElement, 'wheel', function( _Event ) { that.MouseWheel( _Event ); } );

		// Key events
		o3djs.event.addEventListener( _O3DWebElement, 'keydown', function( _Event ) { that.KeyDown( _Event ); } );
		o3djs.event.addEventListener( _O3DWebElement, 'keyup', function( _Event ) { that.KeyUp( _Event ); } );

		// Also listen to key presses outside the O3D window
		patapi.helpers.AddEventListener( _Document, 'keydown', function( _Event ) { that.KeyDown( _Event ); }, true );
		patapi.helpers.AddEventListener( _Document, 'keyup', function( _Event ) { that.KeyUp( _Event ); }, true );
	},

	// Attaches Walk-Zone buffer to the manipulator
	//
	SetWalkZoneData : function( _Buffer, _BufferDimensions, _XZOffset, _XZScale, _HeightColorOffset, _HeightColorScale )
	{
		this.m_WalkBuffer = _Buffer;
		this.m_BufferDimensions = _BufferDimensions;
		this.m_XZOffset = _XZOffset;
		this.m_XZScale = _XZScale;
		this.m_HeightColorOffset = _HeightColorOffset;
		this.m_HeightColorScale = _HeightColorScale;
	},

	// Updates the camera matrix based on current character orientation & view matrix
	//
	OnRender : function( _RenderEvent )
	{
		var	fAngleX = this.m_CharacterAngle - this.m_ViewAngleX;
		var	fAngleY = this.m_ViewAngleY;

		var	ForwardAxis = Math3D.mulScalarVector( -this.m_Walk, [Math.sin( fAngleX ), 0, Math.cos( fAngleX )] );
		var	PreviewedCharacterPosition = null;

		// Check for collisions & height based on walk-zone buffer (if available)
		if ( this.m_WalkBuffer && this.m_Walk != 0 )
		{
			// Convert current (X,Z) position on the walking plane into buffer position
			var	BufferPosition = [
				this.m_CharacterPosition[0] * this.m_XZScale[0] + this.m_XZOffset[0],
				this.m_CharacterPosition[2] * this.m_XZScale[1] + this.m_XZOffset[1]
			];

			var	X = (BufferPosition[0] | 0);
			var	u = BufferPosition[0] - X;
				X = Math.max( 0, X );
				X = Math.min( this.m_BufferDimensions[0]-2, X );
			var	Y = (BufferPosition[1] | 0);
			var	v = BufferPosition[1] - Y;
				Y = Math.max( 0, Y );
				Y = Math.min( this.m_BufferDimensions[1]-2, Y );

			// Get the buffer values at current position
			var	Values0 = this.m_WalkBuffer.getAt( Y*this.m_BufferDimensions[0]+X, 2 );
			var	Values1 = this.m_WalkBuffer.getAt( (Y+1)*this.m_BufferDimensions[0]+X, 2 );

			// Interpolate height value
			var	Height0 = Values0[3*0+1] * (1-u) + Values0[3*1+1] * u;
			var	Height1 = Values1[3*0+1] * (1-u) + Values1[3*1+1] * u;
			var	Height = Height0 * (1-v) + Height1 * v;

			this.m_CharacterPosition[1] = Height * this.m_HeightColorScale + this.m_HeightColorOffset;


			// Check for collision
			function GetValue( _X, _Z )
			{
				var BufferPosition = [
					_X * this.m_XZScale[0] + this.m_XZOffset[0],
					_Z * this.m_XZScale[1] + this.m_XZOffset[1]
				];

				var	X = (BufferPosition[0] | 0);
					X = Math.max( 0, X );
					X = Math.min( this.m_BufferDimensions[0]-2, X );
				var	Y = (BufferPosition[1] | 0);
					Y = Math.max( 0, Y );
					Y = Math.min( this.m_BufferDimensions[1]-2, Y );

				return this.m_WalkBuffer.getAt( Y*this.m_BufferDimensions[0]+X, 1 );
			}
			function GetCollision( _X, _Z )
			{
				return	GetValue.call( this, _X, _Z )[0] / 255.0;
			}

 			PreviewedCharacterPosition = Math3D.addVector( this.m_CharacterPosition, Math3D.mulScalarVector( this.m_CharacterWalkSpeed * _RenderEvent.elapsedTime, ForwardAxis ) );			
 			if ( GetCollision.call( this, PreviewedCharacterPosition[0], PreviewedCharacterPosition[2] ) < 0.99 )
 			{
// 				PreviewedCharacterPosition = this.m_CharacterPosition;	// Prevent walking there...

				// Use distance gradients to determine which way we should "slide" along boundary
				var	GradientAngle = GetValue.call( this, this.m_CharacterPosition[0], this.m_CharacterPosition[2] )[2] * 2.0 * Math.PI / 255.0;
				var	SlideVector = [Math.cos( GradientAngle ), 0, -Math.sin( GradientAngle )];
				
					// Ponder by dot product with forward axis so we don't slide at all if we're facing the obstacle and so we slide reverse if walking backward
				SlideVector = Math3D.mulScalarVector( Math3D.dot( SlideVector, ForwardAxis ), SlideVector );
					// Move a little away from obstacle
				SlideVector = Math3D.addVector( SlideVector, Math3D.mulScalarVector( 0.1, [-SlideVector[2], 0, SlideVector[0]] ) );
				
				PreviewedCharacterPosition = Math3D.addVector( this.m_CharacterPosition, Math3D.mulScalarVector( this.m_CharacterWalkSpeed * _RenderEvent.elapsedTime, SlideVector ) );
 			}
			


// 			var	LeftAxis = [ForwardAxis[2], 0, -ForwardAxis[0]];	// This is the orthogonal displacement left of walker
// 
// 			var	SumAxis = [0,0,0];
// 			var	Collision = 0;
// 
// 			// Compute collision forward
// 			PreviewedCharacterPosition = Math3D.addVector( this.m_CharacterPosition, Math3D.mulScalarVector( this.m_CharacterWalkSpeed * _RenderEvent.elapsedTime, ForwardAxis ) );			
// 			Collision = GetCollision.call( this, PreviewedCharacterPosition[0], PreviewedCharacterPosition[2] );
// 			SumAxis = Math3D.addVector( SumAxis, Math3D.mulScalarVector( Collision, ForwardAxis ) );
// 
// 			// Compute collision left
// 			PreviewedCharacterPosition = Math3D.addVector( this.m_CharacterPosition, Math3D.mulScalarVector( this.m_CharacterWalkSpeed * _RenderEvent.elapsedTime, LeftAxis ) );			
// 			Collision = GetCollision.call( this, PreviewedCharacterPosition[0], PreviewedCharacterPosition[2] );
// 			SumAxis = Math3D.addVector( SumAxis, Math3D.mulScalarVector( Collision, LeftAxis ) );
// 
// 			// Compute collision right
// 			PreviewedCharacterPosition = Math3D.addVector( this.m_CharacterPosition, Math3D.mulScalarVector( -this.m_CharacterWalkSpeed * _RenderEvent.elapsedTime, LeftAxis ) );			
// 			Collision = GetCollision.call( this, PreviewedCharacterPosition[0], PreviewedCharacterPosition[2] );
// 			SumAxis = Math3D.addVector( SumAxis, Math3D.mulScalarVector( -Collision, LeftAxis ) );
// 
// 			// Normalize final axis and recompute ideal position
// 			if ( Math3D.length( SumAxis ) > 1e-3 )
// 				SumAxis = Math3D.normalize( SumAxis );
//  			PreviewedCharacterPosition = Math3D.addVector( this.m_CharacterPosition, Math3D.mulScalarVector( this.m_CharacterWalkSpeed * _RenderEvent.elapsedTime, SumAxis ) );

// var	InfosElement = document.getElementById( "Status" );
// 	InfosElement.innerHTML = "X,Y = " + X + "," + Y + "  -  Value=" + Values0[0] + " " + Values0[1];
		}
		else
			PreviewedCharacterPosition = Math3D.addVector( this.m_CharacterPosition, Math3D.mulScalarVector( this.m_CharacterWalkSpeed * _RenderEvent.elapsedTime, ForwardAxis ) );			
		
		// Perform walking
		this.m_CharacterPosition = PreviewedCharacterPosition;

		// Perform turning
		this.m_CharacterAngle -= this.m_Turn * this.m_CharacterTurnSpeed * _RenderEvent.elapsedTime;

		// Rebuild camera matrix
		this.m_CameraMatrix = Math3D.matrix4.rotationZYX( [fAngleY, fAngleX, 0] );
		this.m_CameraMatrix[3] = Math3D.addVector( this.m_CharacterPosition, Math3D.mulScalarVector( this.m_CharacterHeight, [0,1,0] ) ).concat( 1 );

		// 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( this.m_CameraMatrix ) );
	},


	////////////////////////////////////////////////////////////////////////////
	// EVENTS HANDLERS
	////////////////////////////////////////////////////////////////////////////

	// Handles the mouse down event
	MouseDown : function( _Event )
	{
 		if ( _Event.type != 'mousedown' )
 			return;

		var	Button = _Event.button;
		if ( this.m_IE )
			switch ( Button )
			{
			case 1:
				Button = 0;
				break;
			case 2:
				Button = 2;
				break;
			case 4:
				Button = 1;
				break;
			};

		this.m_ButtonsDown |= 1 << Button;		// Add this button
	},

	// Handles the mouse up event
	MouseUp : function( _Event )
	{
 		if ( _Event.type != 'mouseup' )
 			return;

		var	Button = _Event.button;
		if ( this.m_IE )
			switch ( Button )
			{
			case 1:
				Button = 0;
				break;
			case 2:
				Button = 2;
				break;
			case 4:
				Button = 1;
				break;
			};

		this.m_ButtonsDown &= ~(1 << Button);	// Remove only this button
	},

	// Handles the mouse move event
	MouseMove : function( _Event )
	{
		if ( _Event.type != 'mousemove' )
			return;

// DEBUG
// if ( this.m_bFrozen )
// 	return;	// FROZEN !
// DEBUG

		var	MousePos = this.ComputeNormalizedScreenPosition_( _Event.x, _Event.y );

			MousePos[0] = Math.max( -1, Math.min( 1, MousePos[0] ) );
			MousePos[1] = Math.max( -1, Math.min( 1, MousePos[1] ) );

		this.m_ViewAngleX = MousePos[0] * this.m_MaxHeadTurnX;
		this.m_ViewAngleY = MousePos[1] * this.m_MaxHeadTurnY;
	},

	// 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].splice( 0, 3 ), Math3D.mulScalarVector( 0.004 * this.m_ManipulationZoomSpeed * deltaY, DollyCam[2].splice( 0, 3 ) ) ).concat( 1 );
// 
// 			this.setCameraTransform( DollyCam );
// 		}
// 
// 		// Update "cached" data
// 		this.MouseDown( _Event );
	},

	KeyDown : function( _Event )
	{
		this.m_KeysDown[_Event.keyCode] = true;
		this.UpdateDirections_();
		
// DEBUG CAMERA FREEZE
// if ( _Event.keyCode == 32 && !('synthetic' in _Event) )
// 	this.m_bFrozen = !this.m_bFrozen;
// DEBUG CAMERA FREEZE
	},

	KeyUp : function( _Event )
	{
		this.m_KeysDown[_Event.keyCode] = false;
		this.UpdateDirections_();
	},

	////////////////////////////////////////////////////////////////////////////
	// HELPERS
	////////////////////////////////////////////////////////////////////////////

	// Update directions based on currently pressed keys
	//
	UpdateDirections_ : function()
	{
		this.m_Walk = 0;
		this.m_Turn = 0;

		if ( this.m_QWERTY )
		{
			if ( this.m_KeysDown[37] || this.m_KeysDown[65] ) { this.m_Turn--; }	// A
			if ( this.m_KeysDown[39] || this.m_KeysDown[68] ) { this.m_Turn++; }	// D
			if ( this.m_KeysDown[38] || this.m_KeysDown[87] ) { this.m_Walk++; }	// W
			if ( this.m_KeysDown[40] || this.m_KeysDown[83] ) { this.m_Walk--; }	// S
		}
		else	// AZERTY
		{
			if ( this.m_KeysDown[37] || this.m_KeysDown[81] ) { this.m_Turn--; }	// Q
			if ( this.m_KeysDown[39] || this.m_KeysDown[68] ) { this.m_Turn++; }	// D
			if ( this.m_KeysDown[38] || this.m_KeysDown[90] ) { this.m_Walk++; }	// Z
			if ( this.m_KeysDown[40] || this.m_KeysDown[83] ) { this.m_Walk--; }	// S
		}
	},

	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];
	}
};

