#pragma once

#include <utility>
#include <algorithm>

#define _USE_MATH_DEFINES
#include <math.h>

#include "Viewport.h"


namespace lib_gl
{


// ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
//! 投影設定クラスの規定クラス
class Projection
{
public:
	Projection(void)
	{
		m_Near = 0.01f;
		m_Far  = 1.0f;
	}

	//! OpenGLのカレント行列をセットする
	virtual void SetGLProjection(void) = 0;

	//! NearとFarを同時にセットする
	void SetNearFar( float i_near , float i_far )
	{
		m_Near = i_near;
		m_Far  = i_far;
	}

	void SetViewport(int width, int height)
	{
		m_Viewport.SetViewport( 0 , 0 , width , height );
	}

	void SetViewport(int left, int right, int width, int height)
	{
		m_Viewport.SetViewport( left , right , width , height );
	}

	float GetClipNear(void) const
	{
		return m_Near;
	}

	float GetClipFar(void) const
	{
		return m_Far;
	}

	float GetClipMid(void) const
	{
		return (m_Near + m_Far) * 0.5f;
	}


public:
	Viewport  m_Viewport;  //!< レンダリング先ビューポート
	float     m_Near;      //!< クリップ範囲最小値.
	float     m_Far;       //!< クリップ範囲最大値.
};



// ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
//! 平行投影
class OrthoProjection : public Projection
{
public:
	//! m_BaseLengthの値の種類.
	enum BaseLengthType
	{
		LENGTH_WIDTH   , //! 横幅基準 ( m_BaseLength == ビューポートの左端から右端の距離 )
		LENGTH_HEIGHT  , //! 縦幅基準 ( m_BaseLength == ビューポートの下端から上端の距離 )
		LENGTH_WIDE    , //! 縦と横で長い側を基準とする
		LENGTH_NARROW  , //! 縦と横で短い側を基準とする
	};


public:
	OrthoProjection(void) : Projection()
	{
		m_BaseLengthType = LENGTH_NARROW;
		m_BaseLength = 1.0f;
	}

	virtual void SetGLProjection(void);

	//! 投影範囲の幅を取得する
	float GetViewWidth(void)  const;
	//! 投影範囲の高さを取得する
	float GetViewHeight(void) const;
	//! 投影範囲の縦幅,横幅を取得する
	void GetViewSize( float& w , float& h ) const;

protected:
	void GetViewSize_width  ( float& w , float& h ) const;
	void GetViewSize_height ( float& w , float& h ) const;
	void GetViewSize_wide   ( float& w , float& h ) const;
	void GetViewSize_narrow ( float& w , float& h ) const;


public:
	//! 投影幅の基準となる長さ
	float m_BaseLength;

	BaseLengthType m_BaseLengthType;
};

inline
void OrthoProjection::SetGLProjection(void)
{
	glPushAttrib(GL_TRANSFORM_BIT);
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();

	float w , h;
	GetViewSize( w , h );
	float half_w = w * 0.5f;
	float half_h = h * 0.5f;

	glOrtho( -half_w , half_w , -half_h , half_h , m_Near , m_Far );

	glPopAttrib();
}

inline
void OrthoProjection::GetViewSize( float& w , float& h ) const
{
	switch(m_BaseLengthType)
	{
	case LENGTH_WIDTH  : GetViewSize_width  ( w , h );  break;
	case LENGTH_HEIGHT : GetViewSize_height ( w , h );  break;
	case LENGTH_WIDE   : GetViewSize_wide   ( w , h );  break;
	case LENGTH_NARROW : GetViewSize_narrow ( w , h );  break;
	}
}

inline
float OrthoProjection::GetViewWidth(void) const
{
	float w , h;
	GetViewSize( w , h );
	return w;
}

inline
float OrthoProjection::GetViewHeight(void) const
{
	float w , h;
	GetViewSize( w , h );
	return h;
}

inline
void OrthoProjection::GetViewSize_width( float& w , float& h ) const
{
	float vw = (float)m_Viewport.Width;
	float vh = (float)m_Viewport.Height;
	w = m_BaseLength;
	h = m_BaseLength * vh / vw;
}

inline
void OrthoProjection::GetViewSize_height( float& w , float& h ) const
{
	float vw = (float)m_Viewport.Width;
	float vh = (float)m_Viewport.Height;
	w = m_BaseLength * vw / vh;
	h = m_BaseLength;
}

inline
void OrthoProjection::GetViewSize_wide( float& w , float& h ) const
{
	float vw = (float)m_Viewport.Width;
	float vh = (float)m_Viewport.Height;
	float max_size = (std::max)( vw , vh );
	w = m_BaseLength * vw / max_size;
	h = m_BaseLength * vh / max_size;
}

inline
void OrthoProjection::GetViewSize_narrow( float& w , float& h ) const
{
	float vw = (float)m_Viewport.Width;
	float vh = (float)m_Viewport.Height;
	float min_size = (std::min)( vw , vh );
	w = m_BaseLength * vw / min_size;
	h = m_BaseLength * vh / min_size;
}



// ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
//! 透視投影
class PersProjection : public Projection
{
public:
	PersProjection(void) : Projection()
	{
		m_Fovy = 30.0f;
	}

	virtual void SetGLProjection(void);

	// 指定した距離位置の視界の面積と同等の平行投影を設定する
	void SetGLOrtho(float distance);

	// 視点から指定距離分はなれた位置の画面幅を取得する
	void GetViewRange(float distance, float& w, float& h) const;

	// 平行投影描画時の画面幅を取得する
	void GetViewRangeOrtho(float distance, float& w, float& h) const;


public:
	float m_Fovy;  //<! 視野角.
};


inline
void PersProjection::SetGLProjection(void)
{
	glPushAttrib(GL_TRANSFORM_BIT);
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();

	gluPerspective( (double)m_Fovy , (double)m_Viewport.GetAspect() , (double)m_Near , (double)m_Far );

	glPopAttrib();
}

inline
void PersProjection::SetGLOrtho(float distance)
{
	glPushAttrib(GL_TRANSFORM_BIT);
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();

	double fovy_rad = (double)m_Fovy * M_PI / 180.0;
	double td = tan(fovy_rad) * (double)distance / 2.0f;
	double aspect = (double)m_Viewport.GetAspect();

	double r = td * aspect;
	double t = td;
	double l = -r;
	double b = -t;
	glOrtho(l, r, b, t, (double)m_Near, (double)m_Far);

	glPopAttrib();
}


inline
void PersProjection::GetViewRange(float distance, float& w, float& h) const
{
	float vw = (float)m_Viewport.Width;
	float vh = (float)m_Viewport.Height;

	float ang = m_Fovy;
	if(ang <= 0.0f)
		return;

	float rect_size = distance * tan(ang * (float)M_PI * 0.5f / 180.0f);
	w = 2.0f * rect_size * vw / vh;
	h = 2.0f * rect_size;
}


inline
void PersProjection::GetViewRangeOrtho(float distance, float& w, float& h) const
{
	double fovy_rad = (double)m_Fovy * M_PI / 180.0;
	double td = tan(fovy_rad) * (double)distance / 2.0;
	double aspect = (double)m_Viewport.GetAspect();
	w = 2.0f * (float)td * (float)aspect;
	h = 2.0f * (float)td;
}


}
