#pragma once

#include <C2/lm/vector3.h>
#include <C2/lm/quat4.h>

#include "ManipulatorBase.h"

#include "GlInclude.h"


namespace lib_gl
{


class CameraManipulator : public Manipulator
{
public:
	virtual void SetGLModelview(void);

	void MultGLLookAt(void);

	void LookAt( const lm::vec3f& i_eye , const lm::vec3f& i_view , const lm::vec3f& i_up );

	//! 上方向ベクトルの設定. 大体の方向を指定すると, 視線方向と直交するように補正する.
	void SetUpAngle( const lm::vec3f& i_up );

	//! 視点から注視点への距離を取得
	float GetDistanceToLookPos(void) const;

	//! 注視点→視点となるベクトルを取得する
	lm::vec3f GetViewToEye(void) const;

	//! 視点→注視点となるベクトルを取得する
	lm::vec3f GetEyeToView(void) const;

	lm::vec3f GetFront(void) const;
	lm::vec3f GetUp(void) const;
	lm::vec3f GetBinormal(void) const;

	//! タンブル. 注視点を中心に視点を回転させる.
	//! @param alpha - 方位角の回転角度(右手系なら正で上方から見て反時計回り)
	//! @param beta  - 天頂角の回転角度(正でカメラ右側から見て反時計回り）
	void Tumble( float alpha , float beta );

	//! 視点を中心に視点を回転させる.
	//! @param alpha - 方位角の回転角度(右手系なら正で上方から見て反時計回り)
	//! @param beta  - 天頂角の回転角度(正でカメラ右側から見て反時計回り）
	void Rotate( float alpha , float beta );

	//! トラック. 視空間に対して平行に移動.
	void Track( float right , float up , float front );
	lm::vec3f GetTrackMove( float right , float up , float front ) const;

	//! ドリー. 注視点に近づく.
	//! @param dz           - 注視点に近づく量.
	//! @param min_distance - 注視点に最も近づいたときの距離.
	void Dolly( float dz , float min_distance = 0.0f );

	//! 現在の視点, 注視点間距離の比率でドリー
	//! @param ratio - 始点, 注視点間の距離が移動前後でratio倍になる位置にセットする.
	void DollyByRatio( float ratio );

	//! 視点, 注視点を平行移動
	void Translate( const lm::vec3f& v );
	void Translate( float x , float y , float z );

	//! 注視点が指定位置になるように平行移動
	void TranslateViewPosTo( const lm::vec3f& pos );

	//! 2つの視点を球面線形補間した視点を求める
	//! @param ma    - 視点1
	//! @param mb    - 視点2
	//! @param ratio - 補間係数. 0でmaと一致, 1でmbと一致.
	void SetSlerp(const CameraManipulator& ma, const CameraManipulator& mb, float ratio);

	//! 視線方向の極座標値を取得する
	//! @param alpha - 方位角の回転角度(右手系なら正で上方から見て反時計回り)
	//! @param beta  - 天頂角の回転角度(正でカメラ右側から見て反時計回り）
	void GetViewAngles(float& alpha, float& beta) const;


public:
	lm::vec3f  m_EyePos;   //!< 視点(観測者の位置)
	lm::vec3f  m_ViewPos;  //!< 注視点(観測者が見つめている点の位置)
	lm::vec3f  m_Up;       //!< 天頂方向ベクトル
};



inline
void CameraManipulator::SetGLModelview(void)
{
	glPushAttrib(GL_TRANSFORM_BIT);
	glMatrixMode(GL_MODELVIEW);
	glLoadIdentity();

	MultGLLookAt();

	glPopAttrib();
}

//! カレント行列にLookAt行列をかける
inline
void CameraManipulator::MultGLLookAt(void)
{
	// 視点
	double px = static_cast<double>( m_EyePos.x );
	double py = static_cast<double>( m_EyePos.y );
	double pz = static_cast<double>( m_EyePos.z );
	// 注視点
	double vx = static_cast<double>( m_ViewPos.x );
	double vy = static_cast<double>( m_ViewPos.y );
	double vz = static_cast<double>( m_ViewPos.z );
	// 上方向
	double ox = static_cast<double>( m_Up.x );
	double oy = static_cast<double>( m_Up.y );
	double oz = static_cast<double>( m_Up.z );

	gluLookAt( px , py , pz , vx , vy , vz , ox , oy , oz );
}


inline
void CameraManipulator::SetUpAngle( const lm::vec3f& i_up )
{
	lm::vec3f local_eye = GetViewToEye();
	lm::vec3f binormal = cross( local_eye , i_up );
	m_Up = cross( binormal , local_eye ).get_normalize();
}

inline
void CameraManipulator::LookAt( const lm::vec3f& i_eye , const lm::vec3f& i_view , const lm::vec3f& i_up )
{
	m_EyePos = i_eye;
	m_ViewPos = i_view;
	SetUpAngle( i_up );
}

//! 視点から注視点への距離を取得
inline
float CameraManipulator::GetDistanceToLookPos(void) const
{
	return ( m_EyePos - m_ViewPos ).length();
}

//! 注視点→視点となるベクトルを取得する
inline
lm::vec3f CameraManipulator::GetViewToEye(void) const
{
	return ( m_EyePos - m_ViewPos );
}

//! 視点→注視点となるベクトルを取得する
inline
lm::vec3f CameraManipulator::GetEyeToView(void) const
{
	return ( m_ViewPos - m_EyePos );
}

inline
lm::vec3f CameraManipulator::GetFront(void) const
{
	return GetEyeToView().get_normalize();
}

inline
lm::vec3f CameraManipulator::GetUp(void) const
{
	return m_Up.get_normalize();
}

inline
lm::vec3f CameraManipulator::GetBinormal(void) const
{
	return cross( GetEyeToView() , m_Up ).get_normalize();
}


//! タンブル. 注視点を中心に視点を回転させる.
//! @param alpha - 方位角の回転角度(右手系なら正で上方から見て反時計回り)
//! @param beta  - 天頂角の回転角度(正でカメラ右側から見て反時計回り）
inline
void CameraManipulator::Tumble( float alpha , float beta )
{
	lm::vec3f local_eye = GetViewToEye();
	local_eye.rotate_y( alpha );
	m_Up.rotate_y( alpha );

	lm::vec3f binormal = cross( m_Up , local_eye ).get_normalize();
	local_eye.rotate( beta , binormal );
	m_Up.rotate( beta , binormal );

	m_EyePos = m_ViewPos + local_eye;
}

//! 視点を中心に視線方向を回転させる.
//! @param alpha - 方位角の回転角度(右手系なら正で上方から見て反時計回り)
//! @param beta  - 天頂角の回転角度(正でカメラ右側から見て反時計回り）
inline
void CameraManipulator::Rotate( float alpha , float beta )
{
	lm::vec3f local_eye = GetViewToEye();
	local_eye.rotate_y( alpha );
	m_Up.rotate_y( alpha );

	lm::vec3f binormal = cross( m_Up , local_eye ).get_normalize();
	local_eye.rotate( beta , binormal );
	m_Up.rotate( beta , binormal );

	m_ViewPos = m_EyePos - local_eye;
}

//! トラック. 視空間に対して平行に移動.
inline
void CameraManipulator::Track( float right , float up , float front )
{
	Translate(GetTrackMove(right, up, front));
}

inline
lm::vec3f CameraManipulator::GetTrackMove( float right , float up , float front ) const
{
	lm::vec3f n = GetUp();
	lm::vec3f b = GetBinormal();
	lm::vec3f f = GetFront();

	return b * right + n * up + f * front;
}

//! ドリー. 注視点に近づく.
//! @param dz           - 注視点に近づく量.
//! @param min_distance - 注視点に最も近づいたときの距離.
inline
void CameraManipulator::Dolly( float dz , float min_distance )
{
	lm::vec3f local_eye = GetViewToEye();
	float distance = local_eye.length();
	distance -= dz;
	distance = (std::max)( min_distance , distance );
	local_eye.normalize();
	local_eye *= distance;

	m_EyePos = m_ViewPos + local_eye;
}

//! 現在の視点, 注視点間距離の比率でドリー
//! @param ratio - 始点, 注視点間の距離が移動前後でratio倍になる位置にセットする.
inline
void CameraManipulator::DollyByRatio( float ratio )
{
	m_EyePos = m_ViewPos + GetViewToEye() * ratio;
}

//! 視点, 注視点を平行移動
inline
void CameraManipulator::Translate( const lm::vec3f& v )
{
	Translate(v.x, v.y, v.z);
}

inline
void CameraManipulator::Translate( float x , float y , float z )
{
	lm::vec3f vmove(x, y, z);
	m_EyePos  += vmove;
	m_ViewPos += vmove;
}

inline
void CameraManipulator::TranslateViewPosTo( const lm::vec3f& pos )
{
	Translate(pos - m_ViewPos);
}


inline
void CameraManipulator::SetSlerp(const CameraManipulator& ma, const CameraManipulator& mb, float ratio)
{
	lm::vec3f ax = ma.GetFront();
	lm::vec3f ay = ma.GetUp();
	lm::vec3f az = ma.GetBinormal();
	lm::vec3f bx = mb.GetFront();
	lm::vec3f by = mb.GetUp();
	lm::vec3f bz = mb.GetBinormal();

	lm::quat4f qs;
	qs.set_rotate_from_coord(ax, ay, az);

	lm::quat4f qe;
	qe.set_rotate_from_coord(bx, by, bz);

	lm::quat4f::minimize_rotation(qs, qe);

	lm::quat4f qi = lm::quat4f::interpolate(qs, qe, ratio);

	float r0 = (1.0f - ratio);
	float r1 = ratio;
	m_EyePos = ma.m_EyePos * r0 + mb.m_EyePos * r1;

	float len = ma.GetDistanceToLookPos() * r0 + mb.GetDistanceToLookPos() * r1;
	m_ViewPos = m_EyePos + qi.get_ex() * len;

	SetUpAngle( qi.get_ey() );
}

inline
void CameraManipulator::GetViewAngles(float& alpha, float& beta) const
{
	alpha = 0.0f;
	beta = 0.0f;

	lm::vec3f d = GetViewToEye();
	float len = d.length();
	if (len == 0.0f)
		return;

	d /= len;

	beta = acos(d.y);
	alpha = atan2(d.z, d.x);

	//bool is_reverse = (m_Up.y < 0.0f);
	//if (is_reverse)
	//{
	//	if (alpha > 0.0f)
	//		alpha -= M_PI;
	//	else
	//		alpha += M_PI;

	//	beta = -beta;
	//}
}


}
