/**
 * package for the camera classes
 */
package simogl.camera;

import java.util.ArrayList;

import javax.media.opengl.GL;
import javax.media.opengl.glu.GLU;

import simogl.collision.CollisionFinder;
import simogl.geometry.Point3D;
import simogl.geometry.Triangle;
import simogl.level.DemoLevel;
import simogl.util.Calc;

/**
 * This class represents the camera. It is pretend to move the camera
 * through the world, but actually the world is moved, when a method to move
 * the camera is called. The camera always stays at the same position.
 * By pressing the specified key, the camera can be switched into an
 * orthographic viewing mode (2D mode).
 * @author Frank Bruns
 *
 */
public class Camera
{
	private float x = 0f;
	private float y = 0f;
	private float z = 0f;
	private float heightOverGround = 0f;
	
	private float x2D = 0f;
	private float y2D = 0f;
	private float z2D = 10f;
	
	private float zoom2D = 30.0f;
	private float viewingAngle = 0f;
	private float aspectRatio = 0f;
	private float nearPlane = 0f;
	private float farPlane = 0f;
	private final static float MIN_ZOOM = 10.0f;
	private final static float MAX_ZOOM = 100.0f;
	
	private float rotX = 0f;
	private float rotY = 0f;
	
	private float sensitivityFactor = 1f;
	
	private boolean orthoMode = false;
	private boolean projectionMode = true;
	private boolean freeLookMode = false;
	
	ArrayList<Triangle> collTrigs = null;
	CollisionFinder collFinder = null;
	
	/**
	 * Creates a camera object. The camera will initially be located
	 * at the specified coordinates and be able to look the specified
	 * amount of units down the negative z-axis (viewing range).
	 * @param x x-coordinate
	 * @param y y-coordinate
	 * @param z z-coordinate
	 * @param heightOverGround specifies the height at that the camera
	 * ought to hover over the ground when not in free look mode 
	 * @param angle the viewing angle of the camera (45f is a good value)
	 * @param near the near plane of the camera
	 * @param far far plane of the camera
	 */
	public Camera(float x, float y, float z, float heightOverGround, float angle, float near, float far)
	{
		this.x = x;
		this.y = y;
		this.z = z;
		this.x2D = x;
		this.y2D = z;
		this.heightOverGround = heightOverGround;
		this.viewingAngle = angle;
		this.nearPlane = near;
		this.farPlane = far;
		this.collFinder = new CollisionFinder();
	}
	
	/**
	 * Sets the position of the camera
	 * @param x x-coordinate
	 * @param y y-coordinate
	 * @param z z-coordinate
	 */
	public void setCamPosition(float x, float y, float z)
	{
		this.x = x;
		this.x2D = x;
		this.y = y;
		this.z = z;		
		this.y2D = z;

	}
	
	/**
	 * Sets the x-coordinate of the camera's position
	 * @param x x-coordinate
	 */
	public void setX(float x)
	{
		this.x = x;
		this.x2D = x;
	}
	
	/**
	 * Sets the y-coordinate of the camera's position
	 * @param y y-coordinate
	 */
	public void setY(float y)
	{
		this.y = y;
	}
	
	/**
	 * Sets the z-coordinate of the camera's position
	 * @param z z-coordinate
	 */
	public void setZ(float z)
	{
		this.z = z;
		this.y2D = z;
	}
	
	/**
	 * Returns the current position of the camera as a Point3D object.
	 * <br>
	 * <br>
	 * <b>Note: </b>
	 * Don't use the returned object's methods to perform changes on 
	 * the camera's position. They will have no effect on the camera. 
	 * Use the camera's setX/Y/Z()- or setCamPosition()-methods 
	 * to perform position changes.
	 * @return camera position
	 */
	public Point3D getPosition()
	{
		return new Point3D(this.x, this.y, this.z);
	}
	
	/**
	 * Returns the x-rotation of the camera
	 * @return x-rotation of the camera
	 */
	public float getRotX()
	{
		return this.rotX;
	}
	
	/**
	 * Returns the y-rotation of the camera
	 * @return y-rotation of the camera
	 */
	public float getRotY()
	{
		return this.rotY;
	}
	
	/**
	 * Applies the camera with its current properties. This method must be
	 * called and must be called within the GLEventListener's display()-method.
	 * @param gl
	 * @param glu
	 * @param aspect apect ratio of the display window
	 */
	public void use(GL gl, GLU glu, float aspect)
	{	
		// assign aspect ration to make it availlabe for the getAspectRatio()-method
		this.aspectRatio = aspect;
		
		if(this.projectionMode)
		{		
			gl.glMatrixMode(GL.GL_PROJECTION );  
			gl.glLoadIdentity(); 
			glu.gluPerspective(this.viewingAngle, this.aspectRatio, this.nearPlane, this.farPlane);
			gl.glMatrixMode(GL.GL_MODELVIEW);
			
			// if cam not in free look mode, attach it to the ground
			if(!this.freeLookMode) this.attachCamToGround();
				
			gl.glRotatef(-this.rotX, 1f,0f,0f);
			gl.glRotatef(-this.rotY, 0f,1f,0f);
			gl.glTranslatef(-this.x, -this.y, -this.z);				
		}
		else if(this.orthoMode)
		{
			gl.glMatrixMode(GL.GL_PROJECTION);
			gl.glLoadIdentity();
			gl.glOrtho(-this.zoom2D, this.zoom2D, -this.zoom2D, this.zoom2D, -this.farPlane, this.farPlane);
			
			gl.glMatrixMode(GL.GL_MODELVIEW);
			
			// rotate 90 degrees to get the bird view
			gl.glRotatef(90.0f, 1f, 0f, 0f);
			
			gl.glTranslatef(-this.x2D, -this.z2D, -this.y2D);
		}
	}
	
	/**
	 * Sets the mouse sensitivity. I recommend to use values between 0.01f and 2.0f.
	 * But feel free to try some values that are a little greater than that.
	 * @param sensitivity
	 */
	public void setMouseSensitivity(float sensitivity)
	{
		this.sensitivityFactor = sensitivity;
	}
	
	/**
	 * Moves the cam forward by the specified amount
	 * @param amount amount of movement
	 * @param elapsedTime time between two frames
	 */
	public void moveForward(float amount, long elapsedTime)
	{
		if(this.projectionMode)
		{
			this.x -= (float)Calc.sin(this.rotY) * (amount*elapsedTime);
			this.y -= (float)-Calc.sin(this.rotX) * (amount*elapsedTime);			
	        this.z -= (float)Calc.cos(this.rotY) * (amount*elapsedTime);
	        
	        this.y2D = this.z - (float)Calc.cos(this.rotY) * (amount*elapsedTime);
	        this.x2D += (float)Calc.sin(this.rotY+180f) * (amount*elapsedTime);
		}
		else if(this.orthoMode) 
		{
			this.y2D -= (amount*elapsedTime);
		}
	}
	
	/**
	 * Moves the cam backward by the specified amount
	 * @param amount amount of movemente
	 * @param elapsedTime time between two frames
	 */
	public void moveBackward(float amount, long elapsedTime)
	{
		if(this.projectionMode)
		{
			this.x += (float)Calc.sin(this.rotY) * (amount*elapsedTime);
			this.y += (float)-Calc.sin(this.rotX) * (amount*elapsedTime);
	        this.z += (float)Calc.cos(this.rotY) * (amount*elapsedTime);
	        
	        this.y2D = this.z + (float)Calc.cos(this.rotY) * (amount*elapsedTime);
	        this.x2D -= (float)Calc.sin(this.rotY+180f) * (amount*elapsedTime);
		}
		else if(this.orthoMode) 
		{
			this.y2D += (amount*elapsedTime);
		}
	}
	
	/**
	 * Moves the cam left by the specified amount
	 * @param amount amount of movement
	 * @param elapsedTime time between two frames
	 */
	public void moveLeft(float amount, long elapsedTime)
	{
		if(this.projectionMode)
		{
			this.x -= (float)Calc.sin(this.rotY+90f) * (amount*elapsedTime);			
			this.z -= (float)Calc.cos(this.rotY+90f) * (amount*elapsedTime);
			
			this.x2D = this.x - (float)Calc.sin(this.rotY+90f) * (amount*elapsedTime);
			this.y2D -= (float)Calc.cos(this.rotY+90f) * (amount*elapsedTime);
		}
		else if(this.orthoMode) this.x2D -= (amount*elapsedTime);
	}
	
	/**
	 * Moves the cam right by the specified amount
	 * @param amount amount of movement
	 * @param elapsedTime time between two frames
	 */
	public void moveRight(float amount, long elapsedTime)
	{		
		if(this.projectionMode)
		{
			this.x += (float)Calc.sin(this.rotY+90f) * (amount*elapsedTime);		
			this.z += (float)Calc.cos(this.rotY+90f) * (amount*elapsedTime);
			
			this.x2D = this.x + (float)Calc.sin(this.rotY+90f) * (amount*elapsedTime);
			this.y2D += (float)Calc.cos(this.rotY+90f) * (amount*elapsedTime);
		}
		else if(this.orthoMode) this.x2D += (amount*elapsedTime);
	}
	
	/**
	 * Moves the cam up by the specified amount
	 * @param amount amount of movement
	 * @param elapsedTime time between two frames
	 */
	public void moveUp(float amount, long elapsedTime)
	{
		if(this.projectionMode) this.y += amount * elapsedTime;
	}
	
	/**
	 * Moves the cam down by the specified amount
	 * @param amount amount of movement
	 * @param elapsedTime time between two frames
	 */
	public void moveDown(float amount, long elapsedTime)
	{
		if(this.projectionMode) this.y -= amount * elapsedTime;
	}
	
	/**
	 * Rotates the cam up by the specified degrees
	 * @param amount amount of movement
	 * @param elapsedTime time between two frames
	 */
	public void rotateUp(float amount, long elapsedTime)
	{	
		this.rotX += amount * elapsedTime * this.sensitivityFactor;
		
		// limit the rotation to a certain angle
		if(this.rotX > 90f) this.rotX = 90f;
	}
	
	/**
	 * Rotates the cam down by the specified degrees
	 * @param amount amount of movement
	 * @param elapsedTime time between two frames
	 */
	public void rotateDown(float amount, long elapsedTime)
	{
		this.rotX -= amount * elapsedTime * this.sensitivityFactor;
		
		// limit the rotation to a certain angle
		if(this.rotX < -90f) this.rotX = -90f;
	}
	
	/**
	 * Rotates the cam left by the specified degrees
	 * @param amount amount of movement
	 * @param elapsedTime time between two frames
	 */
	public void rotateLeft(float amount, long elapsedTime)
	{
		this.rotY += amount * elapsedTime * this.sensitivityFactor;
		this.rotY %= 360f;
	}
	
	/**
	 * Rotates the cam right by the specified degrees
	 * @param amount amount of movement
	 * @param elapsedTime time between two frames 
	 */
	public void rotateRight(float amount, long elapsedTime)
	{
		this.rotY -= amount * elapsedTime * this.sensitivityFactor;
		this.rotY %= 360f;
	}
	
	/**
	 * Switches camera from projection mode to ortho mode, or vice versa.
	 *
	 */
	public void switchProjectionMode()
	{
		if(this.projectionMode)
		{
			this.projectionMode = false;
			this.orthoMode = true;
		}
		else if(this.orthoMode)
		{
			this.orthoMode = false;
			this.projectionMode = true;
		}
	}
	
	/**
	 * Zooms the camera in by the specified amount, if in 2D camera mode 
	 * @param amount amount of zoom
	 */
	public void zoomIn2D(float amount)
	{		
		if(this.orthoMode)this.zoom2D -= amount;
		
		// stop zoom, if it gets to close
		if(this.zoom2D <= MIN_ZOOM) this.zoom2D=MIN_ZOOM;
	}
	
	/**
	 * Zooms the camera out by the specified amount, if in 2D camera mode
	 * @param amount amount of zoom
	 */
	public void zoomOut2D(float amount)
	{		
		if(this.orthoMode)this.zoom2D += amount;
		
		// stop zoom, if it gets to far away
		if(this.zoom2D >= MAX_ZOOM) this.zoom2D=MAX_ZOOM;
	}
	
	public void SetHeightOverGround(float height)
	{
		this.heightOverGround = height;
	}
	
	/**
	 * Returns the height at that the camera hovers over the ground
	 * @return height at that the camera hovers over the ground
	 */
	public float getHeightOverGround()
	{
		return this.heightOverGround;
	}
	
	/**
	 * Returns the far plane of camera
	 * @return far plane of the camera
	 */
	public float getFarPlane()
	{
		return this.farPlane;
	}
	
	/**
	 * Sets the far plane of the camera to the specified value
	 * @param far far plane of the camera
	 */
	public void setFarPlane(float far)
	{
		this.farPlane = far;
	}
	
	/**
	 * Returns the near plane of the camera
	 * @return near plane of the camera
	 */
	public float getNearPlane()
	{
		return this.nearPlane;
	}
	
	/**
	 * Sets the near plane of the camera to the specified value
	 * @param near near plane of the camera
	 */
	public void setNearPlane(float near)
	{
		this.nearPlane = near;
	}
	
	/**
	 * Returns the current aspect ratio of the camera (as used in 
	 * gluPerspective()-method)
	 * @return current aspect ratio of the camera
	 */
	public float getAspectRatio()
	{
		return this.aspectRatio;
	}
	
	/**
	 * Returns the viewing angle of the camera (as used in 
	 * gluPerspective()-method)
	 * @return viewing angle of the camera
	 */
	public float getViewingAngle()
	{
		return this.viewingAngle;
	}
	
	/**
	 * Sets the viewing angle of the camera to the specified value.
	 * Usually 45f is a good value.
	 * @param angle viewing angle for the camera
	 */
	public void setViewingAngle(float angle)
	{
		this.viewingAngle = angle;
	}
	
	/**
	 * Switches the movement mode of the camera from planar to free look
	 * and vice versa.
	 */
	public void toggleFreeLook()
	{
		this.freeLookMode = !this.freeLookMode;
	}
	
	/**
	 * Returns whether the 3D cam is in free look mode or attached to
	 * the ground.
	 * @return is cam in free look mode? --> yes = true, no = false
	 */
	public boolean isInFreeLook()
	{
		return this.freeLookMode;
	}
	
	/**
	 * Returns the triangle(s) the camera is actually over 
	 * @return triangle(s) on which the camera is over
	 */
	public ArrayList<Triangle> getCollisionTrigs()
	{
		return this.collTrigs;
	}
	
	/**
	 * This method makes sure that the camera can only move along the
	 * x- and z-axis. Movement along the y-axis (up/down) is prevented.
	 * As a result the camera moves along the terrain. 
	 * 
	 */
	private void attachCamToGround()
	{
		// adapt camera y-position to current terrain height
		this.collTrigs = this.collFinder.collided(this.getPosition(), DemoLevel.terrain);
		for(Triangle trig : this.collTrigs)			
		{						
			// find the highest y-coordinate in the triangle's points and 
			// set the camera's y-value to that coordinate.
			float newY = Math.max(trig.getVertex3().getY(),
					Math.max(trig.getVertex1().getY(),trig.getVertex2().getY()));
			
			// cam hovers at the specified height over the ground
			this.setY(newY + this.heightOverGround);
		}
	}
}
