package org.papervision3d.cameras
{
	import flash.geom.Rectangle;
	
	import org.papervision3d.core.culling.FrustumCuller;
	import org.papervision3d.core.geom.renderables.Vertex3D;
	import org.papervision3d.core.geom.renderables.Vertex3DInstance;
	import org.papervision3d.core.math.Matrix3D;
	import org.papervision3d.core.proto.CameraObject3D;
	import org.papervision3d.core.render.data.RenderSessionData;
	import org.papervision3d.objects.DisplayObject3D;
	
	/**
	 * Camera3D
	 * <p>
	 * Camera3D is the basic camera used by Papervision3D.
	 * </p>
	 * 
	 * @author Tim Knip
	 */ 
	public class Camera3D extends CameraObject3D
	{	
		/**
		 * Constructor.
		 * 
		 * @param	fov		This value is the vertical Field Of View (FOV) in degrees.
		 * @param	near	Distance to the near clipping plane.
		 * @param	far		Distance to the far clipping plane.
		 * @param	useCulling		Boolean indicating whether to use frustum culling. When true all objects outside the view will be culled.
		 * @param	useProjection 	Boolean indicating whether to use a projection matrix for perspective.
		 */ 
		public function Camera3D(fov:Number=60, near:Number=10, far:Number=5000, useCulling:Boolean=false, useProjection:Boolean=false)
		{
			super(near, 40);
			
			_prevFocus = 0;
			_prevZoom = 0;
			_prevOrtho = false;
			_prevUseProjection = false;
			_useCulling = useCulling;
			_useProjectionMatrix = useProjection;
			_far = far;
			_focusFix = Matrix3D.IDENTITY;
		}
		
		/**
		 * Orbits the camera around the specified target. If no target is specified the 
		 * camera's #target property is used. If this camera's #target property equals null
		 * the camera orbits the origin (0, 0, 0).
		 * 
		 * @param	pitch	Rotation around X=axis (looking up or down).
		 * @param	yaw		Rotation around Y-axis (looking left or right).
		 * @param	useDegrees 	Whether to use degrees for pitch and yaw (defaults to 'true').
		 * @param	target	An optional target to orbit around.
		 */ 
		public override function orbit(pitch:Number, yaw:Number, useDegrees:Boolean=true, target:DisplayObject3D=null):void
		{
			target = target || _target;
			target = target || DisplayObject3D.ZERO;

			if(useDegrees)
			{
				pitch *= (Math.PI/180);
				yaw *= (Math.PI/180);
			}
			
			// Number3D.sub
			var dx 			:Number = target.world.n14 - this.x;
			var dy 			:Number = target.world.n24 - this.y;
			var dz 			:Number = target.world.n34 - this.z;
			
			// Number3D.modulo
			var distance 	:Number = Math.sqrt(dx*dx+dy*dy+dz*dz);

			// Rotations
			var rx :Number = Math.cos(yaw) * Math.sin(pitch);
			var rz :Number = Math.sin(yaw) * Math.sin(pitch);
			var ry :Number = Math.cos(pitch);
			
			// Move to specified location
			this.x = target.world.n14 + (rx * distance);
			this.y = target.world.n24 + (ry * distance);
			this.z = target.world.n34 + (rz * distance);
			
			this.lookAt(target);
		}
		
		/**
		 * Projects vertices.
		 * 
		 * @param	object
		 * @param	renderSessionData
		 */ 
		public override function projectVertices(object:DisplayObject3D, renderSessionData:RenderSessionData):Number
		{
			if(!object.geometry || !object.geometry.vertices)
				return 0;
				
			var view		:Matrix3D = object.view,
				vertices	:Array = object.geometry.vertices,
				m11 		:Number = view.n11,
				m12 		:Number = view.n12,
				m13 		:Number = view.n13,
				m21 		:Number = view.n21,
				m22 		:Number = view.n22,
				m23 		:Number = view.n23,
				m31 		:Number = view.n31,
				m32 		:Number = view.n32,
				m33 		:Number = view.n33,
				m41 		:Number = view.n41,
				m42 		:Number = view.n42,
				m43 		:Number = view.n43,
				vx			:Number,
				vy			:Number,
				vz			:Number,
				s_x			:Number,
				s_y			:Number,
				s_z			:Number,
				s_w			:Number,
				vertex		:Vertex3D, 
				screen		:Vertex3DInstance,
				persp 		:Number,
				i        	:int    = vertices.length,
				focus    	:Number = renderSessionData.camera.focus,
				fz       	:Number = focus * renderSessionData.camera.zoom,
				vpw			:Number = viewport.width / 2,
				vph			:Number = viewport.height / 2,
				far			:Number = renderSessionData.camera.far,
				fdist		:Number = far - focus;
			
			while( vertex = vertices[--i] )
			{
				// Center position
				vx = vertex.x;
				vy = vertex.y;
				vz = vertex.z;
				
				s_z = vx * m31 + vy * m32 + vz * m33 + view.n34;
				
				screen = vertex.vertex3DInstance;
				
				if(_useProjectionMatrix)
				{
					s_w = vx * m41 + vy * m42 + vz * m43 + view.n44;
					// to normalized clip space (0.0 to 1.0)
					// NOTE: can skip and simply test (s_z < 0) and save a div
					s_z /= s_w;
					
					// is point between near- and far-plane?
					if( screen.visible = (s_z > 0 && s_z < 1) )
					{
						// to normalized clip space (-1,-1) to (1, 1)
						s_x = (vx * m11 + vy * m12 + vz * m13 + view.n14) / s_w;
						s_y = (vx * m21 + vy * m22 + vz * m23 + view.n24) / s_w;

						// project to viewport.
						screen.x = s_x * vpw;
						screen.y = s_y * vph;
						
						// NOTE: z not linear, value increases when nearing far-plane.
						screen.z = s_z * s_w;
					}
				}
				else
				{
					if(screen.visible = ( focus + s_z > 0 ))
					{
						s_x = vx * m11 + vy * m12 + vz * m13 + view.n14;
						s_y = vx * m21 + vy * m22 + vz * m23 + view.n24;
						
						persp = fz / (focus + s_z);
						screen.x = s_x * persp;
						screen.y = s_y * persp;
						screen.z = s_z;
					}
				}
			}

			return 0;
		}
		
		/**
		 * Updates the internal camera settings.
		 * 
		 * @param	viewport
		 */ 
		public function update(viewport:Rectangle):void
		{
			if(!viewport)
				throw new Error("Camera3D#update: Invalid viewport rectangle! " + viewport);
	
			this.viewport = viewport;

			// used to detect value changes
			_prevFocus = this.focus;
			_prevZoom = this.zoom;
			_prevWidth = this.viewport.width;
			_prevHeight = this.viewport.height;

			if(_prevOrtho != this.ortho)
			{
				if(this.ortho)
				{
					_prevOrthoProjection = this.useProjectionMatrix;
					this.useProjectionMatrix = true;	
				}
				else
					this.useProjectionMatrix = _prevOrthoProjection;
			}
			else if(_prevUseProjection != _useProjectionMatrix)
			{
				this.useProjectionMatrix = this._useProjectionMatrix;
			}	
			
			_prevOrtho = this.ortho;
			_prevUseProjection = _useProjectionMatrix;
			
			this.useCulling = _useCulling;
		}
		
		/**
		 * [INTERNAL-USE] Transforms world coordinates into camera space.
		 * 
		 * @param	transform	An optional transform.
		 */ 
		public override function transformView(transform:Matrix3D=null):void
		{	
			// check whether camera internals need updating
			if(	ortho != _prevOrtho || _prevUseProjection != _useProjectionMatrix || 
				focus != _prevFocus || zoom != _prevZoom || viewport.width != _prevWidth || viewport.height != _prevHeight)
			{
				update(viewport);
			}
			
			// handle camera 'types'
			if(_target)
			{
				// Target camera...
				lookAt(_target);
			}
			else if(_transformDirty)
			{
				// Free camera...
				updateTransform();
			}
			
			if(_useProjectionMatrix)
			{
				super.transformView();
				this.eye.calculateMultiply4x4(_projection, this.eye);
			}
			else
			{
				_focusFix.copy(this.transform);
				_focusFix.n14 += focus * this.transform.n13;
				_focusFix.n24 += focus * this.transform.n23;
				_focusFix.n34 += focus * this.transform.n33;
				super.transformView(_focusFix);
			}
			
			// handle frustum if available
			if(culler is FrustumCuller)
			{
				// The frustum culler simply uses the camera transform
				FrustumCuller(culler).transform.copy(this.transform);
			}
		}
		
		/**
		 * Whether this camera uses frustum culling.
		 * 
		 * @return Boolean
		 */ 
		public override function set useCulling(value:Boolean):void
		{
			super.useCulling = value;
			
			if(_useCulling)
			{
				if(!this.culler)
					this.culler = new FrustumCuller();
					
				FrustumCuller(this.culler).initialize(this.fov, this.viewport.width/this.viewport.height, this.focus/this.zoom, _far);
			}
			else
				this.culler = null;	
		}
		
		/**
		 * Whether this camera uses a projection matrix.
		 */
		public override function set useProjectionMatrix(value:Boolean):void
		{	
			if(value)
			{
				if(this.ortho)
				{
					var w:Number = viewport.width / 2;
					var h:Number = viewport.height / 2;	
					_projection = createOrthoMatrix(-w, w, -h, h, -_far, _far);	
					_projection = Matrix3D.multiply(_orthoScaleMatrix, _projection);
				}
				else
					_projection = createPerspectiveMatrix(fov, viewport.width/viewport.height, this.focus, this.far);
			}
			else
			{
				if(this.ortho)
					value = true;
			}
			super.useProjectionMatrix = value;
		}
		
		/**
		 * Sets the distance to the far plane.
		 * 
		 * @param	value
		 */ 
		public override function set far(value:Number):void
		{
			if(value > this.focus)
			{
				_far = value;
				this.update(this.viewport);
			}
		}
		
		/**
		 * Sets the distance to the near plane (note that this is simply an alias for #focus).
		 * 
		 * @param	value
		 */  
		public override function set near(value:Number):void
		{
			if(value > 0)
			{
				this.focus = value;
				this.update(this.viewport);
			}
		}

		public override function set orthoScale(value:Number):void
		{
			super.orthoScale = value;
			this.useProjectionMatrix = this.useProjectionMatrix;
			_prevOrtho = !this.ortho;
			this.update(this.viewport);	
		}
		
		/**
		 * Creates a transformation that produces a parallel projection.
		 * 
		 * @param	left
		 * @param	right
		 * @param	bottom
		 * @param	top
		 * @param	near
		 * @param	far
		 * @return
		 */
		public static function createOrthoMatrix( left:Number, right:Number, bottom:Number, top:Number, near:Number, far:Number):Matrix3D
		{
			var tx:Number = (right+left)/(right-left);
			var ty:Number = (top+bottom)/(top-bottom);
			var tz:Number = (far+near)/(far-near);
				
			var matrix:Matrix3D = new Matrix3D( [
				2/(right-left), 0, 0, tx,
				0, 2/(top-bottom), 0, ty,
				0, 0, -2/(far-near), tz,
				0, 0, 0, 1 
			] );
			
			matrix.calculateMultiply(Matrix3D.scaleMatrix(1,1,-1), matrix);
			
			return matrix;
		}
			
		/**
		 * Creates a transformation that produces a perspective projection.
		 * 
		 * @param	fov
		 * @param	aspect
		 * @param	near
		 * @param	far
		 * @return
		 */
		public static function createPerspectiveMatrix( fov:Number, aspect:Number, near:Number, far:Number ):Matrix3D
		{
			var fov2:Number = (fov/2) * (Math.PI/180);
			var tan:Number = Math.tan(fov2);
			var f:Number = 1 / tan;
			
			return new Matrix3D( [
				f/aspect, 0, 0, 0,
				0, f, 0, 0,
				0, 0, -((near+far)/(near-far)), (2*far*near)/(near-far),
				0, 0, 1, 0 
			] );
		}
		
		protected var _projection				: Matrix3D;
		private var _prevFocus				: Number;
		private var _prevZoom				: Number;
		private var _prevWidth				: Number;
		private var _prevHeight				: Number;
		private var _prevOrtho				: Boolean;
		private var _prevOrthoProjection	: Boolean;
		private var _prevUseProjection		: Boolean;
		private var _focusFix				: Matrix3D;
	}
}