/* -*- mia-c++ -*-
 *
 * This file is part of viewitgui - a library and program for the
 * visualization of 3D data sets. 
 *
 * Copyright (c) Leipzig, Madrid 1999-2013 Mirco Hellmann, Gert Wollny
 *
 * viewitgui is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * viewitgui is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with viewitgui; if not, see <http://www.gnu.org/licenses/>.
 */


#include <viewit/camera.hh>
#include <GL/gl.h>
#include <GL/glu.h>
#include <math.h>
#include <iostream>
extern "C" {
#include <viewit/trackball.h>
}
#include <mia.hh>
using namespace mia; 
using namespace std; 

static CWatch watch; 



#define FAR_CLIP_PLANE  800.0f
#define NEAR_CLIP_PLANE 200.0f

#define DEG_TO_RAD(x) ( ( M_PI * (x) ) / 180.0 )

class TCameraData {
	friend class TCamera;

	float distance;
	C4DFVector rot; 
	float rotm[4][4];
	
	float zoom;

	C2DBounds viewport;
	C3DFVector location;
	C3DFVector dist_helper; 
	C3DFVector mid_shift; 
	GLdouble mm[16];
	GLdouble pm[16];
	
	float mm_inv[16];
	C2DFVector m_loc;
	bool low_detail; 
	GLfloat w,h; 
	double start_time;
	double time_diff;
	
	TCameraData(float _distance, const C4DFVector& _rot,
		    float _zoom,const C3DFVector& _mid_shift);
	
	void zooming(float factor);
	void move(const float delta);
	void pitch(float angle);
	void heading(float angle);
	void stride(const C2DFVector& delta);

	
	void set_mid_shift(const C3DFVector& new_shift);
	void do_gl_set_fustrum();
	void gl_set_fustrum(const C2DBounds& new_viewport);
	void update_location();
	void gl_use_camera();
	bool do_handle_mouse_event(int button,int x, int y);
	
	void final_measure_time();
	void set_location(const TCamera::TLocation& loc);
	void get_location(TCamera::TLocation& loc);	
	
	const C3DFVector get_spacecoord(const C2DFVector& screen_coord, float zvalue)const;
	const C3DFVector get_spacecoord(unsigned int x, unsigned int y)const;
	
	void heading_pitch_function(int x, int y);
	void zoom_dist_function(int x, int y);
	void mid_shift_function(int x, int y);
	bool get_low_detail()const;

	
	typedef void (*MouseMoveFunc)(TCameraData *data,int x, int y);
	MouseMoveFunc mouse_move_func; 
	
	friend void data_heading_pitch_function(TCameraData *data,int x, int y);
	friend void data_zoom_dist_function(TCameraData *data,int x, int y); 
	friend void data_mid_shift_function(TCameraData *data,int x, int y);

};

void data_heading_pitch_function(TCameraData *data,int x, int y)
{
	data->heading_pitch_function(x,y);
}

void data_zoom_dist_function(TCameraData *data,int x, int y)
{
	data->zoom_dist_function(x,y);
}
void data_mid_shift_function(TCameraData *data,int x, int y)
{
	data->mid_shift_function(x,y);
}


TCameraData::TCameraData(float _distance, const C4DFVector& _rot,float _zoom,
			 const C3DFVector& _mid_shift):
	 distance(_distance), 
	 rot(_rot),
	 zoom(_zoom),
	 mid_shift(_mid_shift),
	 low_detail(false),
	 mouse_move_func(0)
{
	//?	update_location();
	build_rotmatrix(rotm, &rot.x);
}

void TCameraData::set_location(const TCamera::TLocation& loc)
{
	distance = loc.distance;
	zoom = loc.zoom; 
	rot = loc.rot; 
	mid_shift = loc.mid_shift;
	update_location();
	do_gl_set_fustrum();
	build_rotmatrix(rotm, &rot.x);
}

void TCameraData::get_location(TCamera::TLocation& loc)
{
	loc.distance	= distance;
	loc.rot	= rot;
	loc.zoom	= zoom;
	loc.mid_shift	= mid_shift;
}

void TCameraData::gl_set_fustrum(const C2DBounds& new_viewport)
{
	if (viewport != new_viewport) {
		viewport = new_viewport;
		do_gl_set_fustrum();
	}
}
void TCameraData::do_gl_set_fustrum()
{
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	
	if (viewport.x > viewport.y) {
		w = float(viewport.x) / float(viewport.y) * zoom;
		h = zoom; 
	} else {
		w = zoom;
		h = float(viewport.y) / float(viewport.x) * zoom;
	}
	
	glFrustum( -w, w, -h, h, NEAR_CLIP_PLANE, FAR_CLIP_PLANE);
	
	glGetDoublev(GL_PROJECTION_MATRIX,pm);
	
	glMatrixMode(GL_MODELVIEW);
}

const C3DFVector TCameraData::get_spacecoord(const C2DFVector& screen_coord, float zvalue)const
{
	T3DVector<GLdouble> res;
	int vp[4];
	glGetIntegerv(GL_VIEWPORT, vp);
	
	gluUnProject(screen_coord.x, screen_coord.y, zvalue, 
		     mm, pm, vp, &res.x, &res.y, &res.z);
	
	return C3DFVector(res);
		     
}

const C3DFVector TCameraData::get_spacecoord(unsigned int x, unsigned int y)const
{
	T3DVector<GLdouble> res;
	int vp[4];
	float zvalue; 
	
	if (x < viewport.x && y < viewport.y) {
		
		glReadBuffer(GL_BACK);
		glReadPixels(x,y,1, 1, GL_DEPTH_COMPONENT, GL_FLOAT, &zvalue);
		if (zvalue < 1) { // hit something
			glGetIntegerv(GL_VIEWPORT, vp);
			gluUnProject(x, y, zvalue, mm, pm, vp, &res.x, &res.y, &res.z);
			
			return C3DFVector(res);
		}
	}
	return C3DFVector(-1,-1,-1);
}

const C3DFVector TCamera::get_spacecoord(int x, int y)const
{
	return data->get_spacecoord(x,y);
}

const C3DFVector TCamera::get_spacecoord(const C2DFVector& screen_coord, float zvalue)const
{
	return data->get_spacecoord(screen_coord, zvalue);
}


void TCameraData::heading_pitch_function(int x, int y)
{
	C4DFVector new_rot; 
	
	trackball(&new_rot.x , 
		  (2.0f * m_loc.x - viewport.x) / viewport.x, 
		  (viewport.y - 2.0f * m_loc.y) / viewport.y , 
		  (2.0f * x - viewport.x) / viewport.x, 
		  (viewport.y - 2.0f * y) / viewport.y,
		  1.0f);

	add_quats(&new_rot.x, &rot.x, &rot.x);
	
	build_rotmatrix(rotm, &rot.x);
	
	
	
}

void TCameraData::heading(float angle) 
{
	C2DFVector save_m_loc = m_loc; 
	m_loc.x = 0; 
	m_loc.y = viewport.y / 2; 
	heading_pitch_function(int(fabs(angle) > 1.0 ? viewport.x :angle * viewport.x), int(m_loc.y));
	m_loc = save_m_loc;
}

void TCameraData::pitch(float angle) 
{
	C2DFVector save_m_loc = m_loc; 
	m_loc.x = viewport.x / 2; 
	m_loc.y = 0; 
	heading_pitch_function(int(m_loc.x), int(fabs(angle) > 1.0 ? viewport.y :angle * viewport.y));
	m_loc = save_m_loc;
}

void TCameraData::zoom_dist_function(int x, int y)
{
	zooming(1.0 + (m_loc.x - x)/100.0); 
	move(y - m_loc.y);
}
	
void TCameraData::mid_shift_function(int x, int y)
{
	C2DFVector st((m_loc.x - x)/5.0, (y - m_loc.y)/5.0); 
	stride(st);
}
	
TCamera::TCamera():
	data(new TCameraData(500, C4DFVector(-0.09, -0.87, 0.45,0.18),50,C3DFVector(120,116,118)))
{
}

TCamera::TCamera(float _distance, const C4DFVector& _rot,float _zoom,const C3DFVector& _mid_shift):
	data(new TCameraData(_distance,_rot,_zoom,_mid_shift))
{
}

const C3DFVector TCamera::get_farplane_spacecoord(const C2DFVector& screen_coord)const
{
	//return data->get_farplane_spacecoord(screen_coord);
	return data->get_spacecoord(screen_coord, 1.0);
}

const C3DFVector TCamera::get_nearplane_spacecoord(const C2DFVector& screen_coord) const
{
	//return data->get_nearplane_spacecoord(screen_coord);
	return 	data->get_spacecoord(screen_coord, 0.0);
}

	
void TCamera::gl_set_fustrum(const C2DBounds& new_viewport)
{
	data->gl_set_fustrum(new_viewport);
}


void TCameraData::gl_use_camera()
{
	start_time = watch.get_seconds();
	
	glMatrixMode(GL_MODELVIEW);
	
#if 0	
	// calc inverse matrix
	glLoadIdentity();
	glTranslatef(mid_shift.x,mid_shift.y,mid_shift.z);
	glRotatef(-rot.z,0.0f,0.0f,1.0f);	
	glRotatef(-rot.y,0.0f,1.0f,0.0f);
	glRotatef(-rot.x,1.0f,0.0f,0.0f);
	glTranslatef(0.0f,0.0f,distance);
	glGetFloatv(GL_MODELVIEW_MATRIX,mm_inv);
#endif
	// apply normal matrix
	glLoadIdentity();
	glTranslatef(0.0f,0.0f,-distance);
	glMultMatrixf(&rotm[0][0]);
	glTranslatef(-mid_shift.x,-mid_shift.y,-mid_shift.z);
	glGetDoublev(GL_MODELVIEW_MATRIX,mm);

	update_location();
}

void TCameraData::final_measure_time()
{
	time_diff = watch.get_seconds() - start_time;
}

void TCamera::gl_use_camera()
{
	data->gl_use_camera();
}

inline bool TCameraData::get_low_detail()const
{
	return low_detail; 
}

bool TCamera::low_detail()const
{
	return data->get_low_detail();
}

void TCameraData::zooming(float factor)
{
	zoom *= factor; 
	do_gl_set_fustrum();
}

void TCameraData::update_location() 
{
	dist_helper = C3DFVector(mm[2]+mm[3],mm[6]+mm[7],mm[10]+mm[11]);

	location.x = - distance * rotm[0][2] + mid_shift.x ;
	location.y = - distance * rotm[1][2] + mid_shift.y ;
	location.z = - distance * rotm[2][2] + mid_shift.z ;

}

void TCameraData::move(const float delta)
{
	// move along view direction
	distance += delta;
}

void TCameraData::stride(const C2DFVector& delta)
{
	
	mid_shift.x += delta.x * rotm[0][0] + delta.y * rotm[0][1];
	mid_shift.y += delta.x * rotm[1][0] + delta.y * rotm[1][1];
	mid_shift.z += delta.x * rotm[2][0] + delta.y * rotm[2][1];
		
}

TCamera::~TCamera()
{
	delete data; 
}

C3DFVector TCamera::transform(const C3DFVector& v)const
{
	double *mm=data->mm; 
	return C3DFVector( mm[0] * v.x + mm[1] * v.y + mm[ 2] * v.z + mm[ 3],
			   mm[4] * v.x + mm[5] * v.y + mm[ 6] * v.z + mm[ 7],
			   mm[8] * v.x + mm[9] * v.y + mm[10] * v.z + mm[11]);
}

#if 0
C3DFVector TCamera::inv_transform(const C3DFVector& v)const
{
	float *mm=data->mm_inv; 
	return C3DFVector( mm[0] * v.x + mm[4] * v.y + mm[ 8] * v.z + mm[12],
			   mm[1] * v.x + mm[5] * v.y + mm[ 9] * v.z + mm[13],
			   mm[2] * v.x + mm[6] * v.y + mm[10] * v.z + mm[14]);
}
#endif

const C2DBounds& TCamera::get_viewport() const
{
	return data->viewport;
}

const C3DFVector& TCamera::get_location() const
{
	return data->location; 
}

const C3DFVector& TCamera::get_mid_shift() const
{
	return data->mid_shift; 
}


void TCamera::heading(float angle)
{
	data->heading(angle);
}
	
float TCamera::get_z_dist(const C3DFVector& v)const
{
	return dot((v + data->mid_shift),  data->dist_helper);
}

bool TCamera::do_handle_mouse_event(int button,int x, int y)
{
	return data->do_handle_mouse_event(button,x,y);
}

bool TCameraData::do_handle_mouse_event(int button,int x, int y)
{
	if ( button & EV_MOUSE1_DOWN ) {
		low_detail = (time_diff > 0.1); 
		if (button & EV_KEY_MODIFIER_CTRL) 
			mouse_move_func = data_zoom_dist_function;
		else if (button & EV_KEY_MODIFIER_SHIFT) 
			mouse_move_func = data_mid_shift_function;
		else
			mouse_move_func = data_heading_pitch_function;
	} else if (mouse_move_func) {
		if (button & EV_MOUSE_MOVE)
			mouse_move_func(this,x,y);
		else if (button & EV_MOUSE1_UP) {
			low_detail = false; 
			mouse_move_func = 0; 
		}else
			return false; 
	} else
		return false;
	
	m_loc.x = x; 
	m_loc.y = y; 
	return true; 
}

bool TCamera::do_handle_command_event(PEventInfo info)
{
	switch (info->get_command()) {
		
	case EV_CAMERA_TIME_MEASURE:
		data->final_measure_time();
		break;
	
	case EV_CAMERA_SET_PREDEF_VIEW: {
		TLocation& loc = dynamic_cast<TLocation&>(*info);
		data->set_location(loc);
	} break;
		
	case EV_CAMERA_GET_PREDEF_VIEW: {
		TLocation& loc = dynamic_cast<TLocation&>(*info);
		data->get_location(loc);
		loc.set_command(EV_CAMERA_SET_PREDEF_VIEW);
	} break;
	
	default: return TEventHandler::do_handle_command_event(info);
	} // end switch
	
	return true;
}

TCamera::TLocation::TLocation(float _distance, const C4DFVector& _rot,float _zoom,const C3DFVector& _mid_shift):
	CEventInfo(EV_CAMERA_SET_PREDEF_VIEW),
	distance(_distance), 
	zoom(_zoom),
	rot(_rot),
	mid_shift(_mid_shift)
{
}

TCamera::TLocation::TLocation():
	CEventInfo(EV_CAMERA_GET_PREDEF_VIEW)
{
}

void TCamera::TLocation::read(istream& is)
{
	int c;
	is >> c;
	is >> distance;
	is >>  zoom;
	is >>  rot;
	is >> mid_shift; 
	set_command(c);
}

void TCamera::TLocation::write(ostream& os)
{
	os << get_command() << " " << distance << " " << zoom << " " << rot << " " << mid_shift << endl; 
}
