/* 
   render/camera.cc
   This file is part of the Osirion project and is distributed under 
   the terms and conditions of the GNU General Public License version 2 
*/

#include "core/core.h"
#include "math/mathlib.h"
#include "math/matrix4f.h"
#include "render/camera.h"
#include "render/gl.h"
#include "sys/sys.h"

using math::degrees360f;
using math::degrees180f;

namespace render
{

const float 		MIN_DELTA = 10e-10;

const float 		pitch_track = -15.0f;
const float 		pitch_overview = -75.0f;

float 			Camera::camera_aspect = 1.0f;
math::Vector3f 		Camera::camera_eye;
math::Vector3f 		Camera::camera_target;
math::Axis 		Camera::camera_axis;
Camera::Mode 		Camera::camera_mode;

// current and target yaw angle in XZ plane, positive is looking left
float Camera::direction_current;
float Camera::direction_target;
float Camera::target_direction;

// current and target pitch angle in XY, positive is looking up
float Camera::pitch_current;
float Camera::pitch_target;
float Camera::target_pitch;

float Camera::distance;

void Camera::init()
{
	camera_aspect = 1.0f;

	direction_current = 0;
	direction_target = 0; 

	pitch_current = pitch_track * 2; 
	pitch_target = pitch_track; 

	target_pitch = 0.0f;
	target_direction = 0.0f;

	distance = 0.4f;

	set_mode(Track);

	camera_axis.clear();
	camera_eye.clear();
	camera_target.clear();
	
}

void Camera::shutdown()
{
}

void Camera::set_aspect(float aspect)
{
	camera_aspect = aspect;
}

void Camera::set_mode(Mode newmode) {

	direction_target = 0;
	direction_current = direction_target;
	pitch_target = pitch_track;
	pitch_current = pitch_target;

	target_direction = 0.0f;
	target_pitch = 0.0f;
	distance = 0.4f;

	camera_axis.clear();

	switch(newmode) {
	case Track:
		// switch camera to Track mode
		camera_mode = Track;
		if (core::localcontrol()) {
			if (core::localcontrol()->state())
				camera_axis.assign(core::localcontrol()->state()->axis());
			else
				camera_axis.assign(core::localcontrol()->axis());
		}
		break;

	case Free:
		// switch camera to Free mode
		camera_mode = Free;
		pitch_target = 2.0 * pitch_track;
		pitch_current = pitch_target;
		break;

	case Cockpit:
		camera_mode = Cockpit;
		break;

	case Overview:
		// switch camera to Overview mode
		camera_mode = Overview;

	default:
		break;
	}

}

void Camera::next_mode()
{
	
 	if (!core::localcontrol()) {
		set_mode(Overview);
		return;
	}

	switch(camera_mode) {
	case Free:
		// switch camera to Track mode
		set_mode(Track);
		core::application()->notify_message(std::string("view: track"));
		break;

	case Track:
		// switch camera to Cockpit mode
		set_mode(Cockpit);
		core::application()->notify_message(std::string("view: cockpit"));
		break;
	
	case Cockpit:
		// switch camera to Free mode
		set_mode(Free);
		core::application()->notify_message(std::string("view: free"));
		break;
	
	default:
		break;
	}
}

void Camera::draw(float seconds) 
{	
	math::Matrix4f matrix;
	math::Axis target_axis;
	float d = 0;

	if (!core::localcontrol()) {

		if (camera_mode != Overview) {
			set_mode(Overview);
		}
		
		camera_eye.clear();
		camera_target.clear();
		camera_axis.clear();
		pitch_current = pitch_overview;
		camera_axis.change_pitch(pitch_current);

		distance = 20.0f;

	} else {
		if (mode() == Overview)
			set_mode(Track);

		if (core::localcontrol()->state()) {
			camera_target.assign(core::localcontrol()->state()->location());
			target_axis.assign(core::localcontrol()->state()->axis());
		} else {
			camera_target.assign(core::localcontrol()->location());
			target_axis.assign(core::localcontrol()->axis());
		}
				
		if (core::localcontrol()->model()) {
			distance = core::localcontrol()->model()->radius();
		} else {
			distance = 1.0f;
		}

		if (mode() == Track) {
			float cosangle;
			float angle;
			float side;
			float u;
			//const float camspeed = 90.0f * seconds; // 180 degrees per second

			math::Vector3f n;
			math::Vector3f p;

			// camera axis: pitch

			// project target_axis.up() into the plane with axis->left() normal
			n = camera_axis.left();
			p = target_axis.up();
			u = p[0]*n[0] + p[1]*n[1] + p[2]*n[2] / (-n[0]*n[0] - n[1]*n[1] - n[2] * n[2]);
			p = target_axis.up() + u * n;
	
			side = camera_axis.forward().x * p.x + 
				camera_axis.forward().y * p.y +
				camera_axis.forward().z * p.z;

			if ((fabs(side) - MIN_DELTA > 0)) {
				
				cosangle = math::dotproduct(p, camera_axis.up());
				if (fabs(cosangle) + MIN_DELTA < 1 ) {
					angle = acos(cosangle) * 180.0f / M_PI;
					angle = math::sgnf(side)  * angle * seconds;
					camera_axis.change_pitch(-angle);
				}
			}

			// camera axis: direction

			// project target_axis.forward() into the plane with axis.up() normal
			n = camera_axis.up();
			p = target_axis.forward();
			u = p[0]*n[0] + p[1]*n[1] + p[2]*n[2] / (-n[0]*n[0] - n[1]*n[1] - n[2] * n[2]);
			p = target_axis.forward() + u * n;

			side = camera_axis.left().x * p.x + 
				camera_axis.left().y * p.y +
				camera_axis.left().z * p.z;

			if ((fabs(side) - MIN_DELTA > 0)) {
				
				cosangle = math::dotproduct(p, camera_axis.forward());
				if (fabs(cosangle) + MIN_DELTA < 1 ) {
					angle = acos(cosangle) * 180.0f / M_PI;
					angle = math::sgnf(side)  * angle * seconds;
					camera_axis.change_direction(angle);
				}
			}

			// camera axis: roll

			// project target_axis.up() into the plane with axis.forward() normal
			n = camera_axis.forward();
			p = target_axis.up();
			u = p[0]*n[0] + p[1]*n[1] + p[2]*n[2] / (-n[0]*n[0] - n[1]*n[1] - n[2] * n[2]);
			p = target_axis.up() + u * n;

			side = camera_axis.left().x * p.x + 
				camera_axis.left().y * p.y +
				camera_axis.left().z * p.z;

			if ((fabs(side) - MIN_DELTA > 0)) {
				
				cosangle = math::dotproduct(p, camera_axis.up());
				if (fabs(cosangle) + MIN_DELTA < 1 ) {
					angle = acos(cosangle) * 180.0f / M_PI;
					angle = math::sgnf(side)  * angle * seconds;
					camera_axis.change_roll(angle);
				}
			}

			if (core::localcontrol()->model()) {
 				camera_target -= (core::localcontrol()->model()->maxbbox().x + 0.1f) * camera_axis.forward();
				camera_target += (core::localcontrol()->model()->maxbbox().z + 0.1f ) * camera_axis.up();
			}

		} else if (mode() == Free) {
			camera_axis.assign(target_axis);

			direction_target =  direction_current - 90 * target_direction;
			pitch_target = pitch_current - 90 * target_pitch;

			// adjust direction
			d = degrees180f(direction_current - direction_target);
			direction_current = degrees360f( direction_current -  d * seconds);
			camera_axis.change_direction(direction_current);

			// adjust pitch 
			d = degrees180f(pitch_current - pitch_target);
			pitch_current = degrees360f(pitch_current -  d * seconds);
			camera_axis.change_pitch(pitch_current);
		
		} else if (mode() == Cockpit) {

			camera_axis.assign(target_axis);

			if (core::localcontrol()->state()) {
				if (core::localcontrol()->model()) {
					camera_target += (core::localcontrol()->model()->maxbbox().x+0.05) *
						core::localcontrol()->state()->axis().forward();
				} else  {
					camera_target += (core::localcontrol()->radius() + 0.05) *
						core::localcontrol()->state()->axis().forward();
				}
			}
			distance = 0.0f;
		}
	}

	// Change to the projection matrix and set our viewing volume.
	gl::matrixmode(GL_PROJECTION);
	gl::loadidentity();

	const float frustum_size = 0.5f;
	const float frustum_front = 1.0f;
	distance += frustum_front;
	gl::frustum(-frustum_size*aspect(), frustum_size*aspect(), -frustum_size, frustum_size, frustum_front, 1024.0f);

	// model view
	gl::matrixmode(GL_MODELVIEW);
	gl::loadidentity();

	// map world coordinates to opengl coordinates
	gl::rotate(90.0f, 0, 1.0, 0);
	gl::rotate(-90.0f, 1.0f , 0, 0);

	// assign transformation matrix
	matrix.assign(camera_axis);

	// apply the transpose of the axis transformation (the axis is orhtonormal)
	gl::multmatrix(matrix.transpose());

	// match the camera with the current target
	gl::translate(-1.0f * camera_target);
	
	// apply camera offset
	gl::translate(distance * camera_axis.forward());

	// calculate eye position
	camera_eye = camera_target - (distance * camera_axis.forward());
}

void Camera::set_direction(float direction)
{
	target_direction = direction;
	math::clamp(target_direction, -1.0f, 1.0f);
}

void Camera::set_pitch(float pitch)
{
	target_pitch = pitch;
	math::clamp(target_pitch, -1.0f, 1.0f);
}

void Camera::reset()
{
	set_mode(camera_mode);
}

}