//	Darkstar Engine  (c) UnderWorld Interactive Studio (all coding by Svarog    2008)
//
//      Данный файл является частью Darkstar Engine. Любое использование кода файла
//      без согласия компании, является нарушением закона об авторском праве (в слу-
//      чае если не оговорено обратное).
//
//      Имя файла: preetham_sky.cpp
//      Краткое описание: darkstar preetham sky representation
//

#include <Darkstar/Nature/Sky/preetham_sky.hpp>
#include <Darkstar/Graphic System/Effect/effect_manager.hpp>
#include <Darkstar/Resource System/Textures/textureManager.hpp>
#include <Darkstar/Resource System/Model/input_layout.hpp>
#include <Darkstar/Graphic System/Direct3D/graphicDriver.hpp>
#include <Darkstar/Kernel/logh.hpp>

using namespace nature;
using namespace math;
using ker::Log;

using gs::GraphicDriver;

PreethamSky::PreethamSky(const shared_ptr<rs::StaticModel>& geosphere, const std::string& star_database_name, const std::string& stars_texture_name, const std::string& stars_effect_name)
{
	sky_dome = geosphere;

	turbidity = 2.5;
	_computePerezCoeff();
	// we use coordinates of Saint-Petersburg (Russia) by default
	site_latitude = 60.0f;
	site_longitude = 30.3f;
	sun.setSiteCoordinates(site_longitude, site_latitude, true);
	site_latitude *= deg_to_rad;

	if(star_database_name != "" && stars_effect_name != "" && stars_texture_name != "")
	{
		stars.reset(new StarDatabase(star_database_name, stars_texture_name, stars_effect_name, site_latitude));
		is_night_sky_stars_enable = stars->getIsDatabaseCorrect();
	}
	else
		is_night_sky_stars_enable = false;

	setTimeAndDate(ker::Time(12, 0, 0), ker::Date(6, 1));

	fullscreen_quad.reset(new rs::Quad(geosphere->getMesh(0)->getEffect()));

	is_cubemap_enable = sky_dome->getMesh(0)->getMaterial()->getTechnique() == "with_cubemap" ? true : false;
	sky_cubemap.reset(new gs::RenderTarget());
	if(is_cubemap_enable)
		setCubemapSize(1024);
	else
		setCubemapSize(1);
	drawToCubemap(is_cubemap_enable);
};

PreethamSky::~PreethamSky()
{

};

void PreethamSky::setCubemapSize(const unsigned int new_size)
{
	cubemap_size = new_size;
	shared_ptr<gs::RenderTarget> tmp_cubemap(new gs::RenderTarget());
	if(!tmp_cubemap->createCube(cubemap_size, cubemap_size, gs::DRTFMT_R16G16B16A16_FLOAT))
	{
		lwar << "Cannot create cubemap for sky with size " << cubemap_size;
		return;
	};
	sky_cubemap = tmp_cubemap;
};

void PreethamSky::drawToCubemap(const bool is_enable)
{
	sky_dome->getMesh(0)->getMaterial()->setTechnique("without_cubemap");
	is_cubemap_enable = false;
	if(!is_enable)
		return;
	if(!sky_cubemap)
	{
		lwar << "Cannot draw sky to cubemap";
		return;
	};
	is_cubemap_enable = true;
	sky_dome->getMesh(0)->getMaterial()->setTechnique("with_cubemap");
};

// функция Переса
float PreethamSky::_PerezFunction(const float theta, const float gamma, const float A, const float B, const float C, const float D, const float E)
{
	float cos_gamma = cosf(gamma);
	float cos_theta = cosf(theta);
	return (1 + A*expf(B/cos_theta))*(1 + C*expf(D*gamma) + E*cos_gamma*cos_gamma);
};

// считаем коэффициенты функции Переса для вычисления цвета в каждой точке неба
void PreethamSky::_computePerezCoeff()
{
	A_coeff.x = x_dist_coeff[0][0]*turbidity + x_dist_coeff[0][1];
	B_coeff.x = x_dist_coeff[1][0]*turbidity + x_dist_coeff[1][1];
	C_coeff.x = x_dist_coeff[2][0]*turbidity + x_dist_coeff[2][1];
	D_coeff.x = x_dist_coeff[3][0]*turbidity + x_dist_coeff[3][1];
	E_coeff.x = x_dist_coeff[4][0]*turbidity + x_dist_coeff[4][1];

	A_coeff.y = y_dist_coeff[0][0]*turbidity + y_dist_coeff[0][1];
	B_coeff.y = y_dist_coeff[1][0]*turbidity + y_dist_coeff[1][1];
	C_coeff.y = y_dist_coeff[2][0]*turbidity + y_dist_coeff[2][1];
	D_coeff.y = y_dist_coeff[3][0]*turbidity + y_dist_coeff[3][1];
	E_coeff.y = y_dist_coeff[4][0]*turbidity + y_dist_coeff[4][1];

	A_coeff.Y = Y_dist_coeff[0][0]*turbidity + Y_dist_coeff[0][1];
	B_coeff.Y = Y_dist_coeff[1][0]*turbidity + Y_dist_coeff[1][1];
	C_coeff.Y = Y_dist_coeff[2][0]*turbidity + Y_dist_coeff[2][1];
	D_coeff.Y = Y_dist_coeff[3][0]*turbidity + Y_dist_coeff[3][1];
	E_coeff.Y = Y_dist_coeff[4][0]*turbidity + Y_dist_coeff[4][1];
};

// рассчитываем вклад точки зенита для остальных точек неба
void PreethamSky::_computeZenitDistribution()
{
	// в случае ночного неба эти значения дают наилучший результат
	float turbidity_square = turbidity*turbidity;
	float theta2 = sun.getThetaSun()*sun.getThetaSun();
	float theta3 = sun.getThetaSun()*theta2;

	zenith_distribution.x = (turbidity_square*x_zenith_coeff[0][0] + turbidity*x_zenith_coeff[1][0] 
					+ x_zenith_coeff[2][0])*theta3 + 
					(turbidity_square*x_zenith_coeff[0][1] + turbidity*x_zenith_coeff[1][1] 
					+ x_zenith_coeff[2][1])*theta2 +
					(turbidity_square*x_zenith_coeff[0][2] + turbidity*x_zenith_coeff[1][2]
					+ x_zenith_coeff[2][2])*sun.getThetaSun() +
					(turbidity_square*x_zenith_coeff[0][3] + turbidity*x_zenith_coeff[1][3]
					+ x_zenith_coeff[2][3]);
	
	zenith_distribution.x /= _PerezFunction(0, sun.getThetaSun(), A_coeff.x, B_coeff.x, C_coeff.x, D_coeff.x, E_coeff.x);
    
	zenith_distribution.y = (turbidity_square*y_zenith_coeff[0][0] + turbidity*y_zenith_coeff[1][0] 
					+ y_zenith_coeff[2][0])*theta3 + 
						(turbidity_square*y_zenith_coeff[0][1] + turbidity*y_zenith_coeff[1][1] 
					+ y_zenith_coeff[2][1])*theta2 +
						(turbidity_square*y_zenith_coeff[0][2] + turbidity*y_zenith_coeff[1][2]
					+ y_zenith_coeff[2][2])*sun.getThetaSun() +
						(turbidity_square*y_zenith_coeff[0][3] + turbidity*y_zenith_coeff[1][3]
					+ y_zenith_coeff[2][3]);

	zenith_distribution.y /= _PerezFunction(0, sun.getThetaSun(), A_coeff.y, B_coeff.y, C_coeff.y, D_coeff.y, E_coeff.y);

	float chi = (4.0f/9.0f - turbidity/120.f)*(math::pi - 2 *sun.getThetaSun());
	zenith_distribution.Y = (4.0453f*turbidity - 4.9710f)*tanf(chi) - 0.2155f*turbidity + 2.4192f;
	zenith_distribution.Y /= _PerezFunction(0, sun.getThetaSun(), A_coeff.Y, B_coeff.Y, C_coeff.Y, D_coeff.Y, E_coeff.Y);
	
	if(sun.getThetaSun() <= pi2)
		sun_intensity = zenith_distribution.Y * _PerezFunction(sun.getThetaSun(), 0, A_coeff.Y, B_coeff.Y, C_coeff.Y, D_coeff.Y, E_coeff.Y);	
	else
	{
		// some "magic" values. They obtained heuristic methods
		zenith_distribution.x = 0.36602601f;
		zenith_distribution.y = 0.39682165f;
		zenith_distribution.Y = 3.542418f;
		sun_intensity = 0;
	};
};

void PreethamSky::setTurbidity(const float new_turbidity)
{
	turbidity = new_turbidity;
	_computePerezCoeff();
	_computeZenitDistribution();
};

void PreethamSky::setSun(const float new_theta_sun, const float new_phi_sun)
{
	sun.setAngles(new_theta_sun, new_phi_sun);
	_computeZenitDistribution();
};

void PreethamSky::setSiteCoordinates(const float site_longitude, const float site_latitude)
{
	this->site_longitude = site_longitude;
	this->site_latitude = site_latitude*deg_to_rad;

	sun.setSiteCoordinates(site_longitude, site_latitude);
	_computeZenitDistribution();
	stars->setSiteLatitude(site_latitude);
};

void PreethamSky::setTimeAndDate(const ker::Time& time, const ker::Date& date)
{
	this->current_time = time;
	this->current_date = date;

	sun.setTimeAndDate(time, date);
	_computeZenitDistribution();
};

void PreethamSky::setTime(const ker::Time& time)
{
	this->current_time = time;

	sun.setTime(time);
	_computeZenitDistribution();
};

void PreethamSky::setDate(const ker::Date& date)
{
	this->current_date = date;

	sun.setDate(date);
	_computeZenitDistribution();
};

const Vect3& PreethamSky::getSunDirection() const
{
	return sun.getSunDirection();
};

shared_ptr<rs::BaseTexture> PreethamSky::getSkybox() const
{
	return sky_cubemap->getTexture();
};

float PreethamSky::getSunIntensity() const
{
	if(current_time.getCurrentDayMinute() >= sun.getSunriseTime() && current_time.getCurrentDayMinute() <= sun.getSunNoonTime())
	{
		float inter_coeff = (current_time.getCurrentDayMinute() - sun.getSunriseTime())/(sun.getSunNoonTime() - sun.getSunriseTime());
		return inter_coeff*0.9f + (1-inter_coeff)*0.3f;
	}
	if(current_time.getCurrentDayMinute() > sun.getSunNoonTime() && current_time.getCurrentDayMinute() <= sun.getSunsetTime())
	{
		float inter_coeff = (current_time.getCurrentDayMinute() - sun.getSunNoonTime()) / (sun.getSunsetTime() - sun.getSunNoonTime());
		return inter_coeff*0.3f + (1-inter_coeff)*0.9f;
	};
	return 0;
};

void PreethamSky::draw(const shared_ptr<gs::Camera>& camera)
{
	math::Vect3 camera_position = camera->getPosition();
	sky_dome->setTranslation(camera_position);
	float zfar = camera->getZfar();
	camera->setZfar(4000.0f);

	if(is_cubemap_enable)
	{
		std::vector<std::string> tech_names(6);
		tech_names[0] = "cube_posx";
		tech_names[1] = "cube_negx";
		tech_names[2] = "cube_posy";
		tech_names[3] = "cube_negy";
		tech_names[4] = "cube_posz";
		tech_names[5] = "cube_negz";
		
		shared_ptr<gs::RenderTarget> cur_color = GraphicDriver::ptr()->getCurrentColorRenderTarget();
		shared_ptr<gs::RenderTarget> cur_depth = GraphicDriver::ptr()->getCurrentDepthStencilRenderTarget();
		unsigned int cur_color_rt_layer = GraphicDriver::ptr()->getCurrentColorRTLayer();
		rs::DEViewport cur_viewport = GraphicDriver::ptr()->getCurViewport();
		GraphicDriver::ptr()->end();

		for(unsigned int k = 0; k < 6; ++k)
		{
			GraphicDriver::ptr()->beginWithColor(sky_cubemap, k);
			GraphicDriver::ptr()->clear();
			if(k == 3) // NEGATIVУ_Y face is the earth. So there is not sky in that face
			{
				GraphicDriver::ptr()->end();
				continue;
			};
			GraphicDriver::ptr()->setViewport(cubemap_size, cubemap_size);
			fullscreen_quad->setCurTechniqueName(tech_names[k]);
			for(unsigned int i = 0; i < fullscreen_quad->getModel()->getMeshesNumber(); ++i)
			{
				shared_ptr<gs::Effect> sky_effect = fullscreen_quad->getModel()->getMesh(i)->getEffect();
				if(current_time.getCurrentDayMinute() >= sun.getSunsetTime() || current_time.getCurrentDayMinute() <= sun.getSunriseTime())
					sky_effect->setMacro("NIGHT_SKY", 1);
				else
					sky_effect->setMacro("NIGHT_SKY", 0);
				sky_effect->setVect3("zenit_distribution", &zenith_distribution.vec);
				sky_effect->setVect3("A_coeff", &A_coeff.vec);
				sky_effect->setVect3("B_coeff", &B_coeff.vec);
				sky_effect->setVect3("C_coeff", &C_coeff.vec);
				sky_effect->setVect3("D_coeff", &D_coeff.vec);
				sky_effect->setVect3("E_coeff", &E_coeff.vec);
				sky_effect->setVect3("zenit_direction", &Vect3(0, 1, 0));
				sky_effect->setVect3("sun_direction", &sun.getSunDirection());

				fullscreen_quad->getModel()->drawMesh(i, camera);
			};
			GraphicDriver::ptr()->end();
		};

		GraphicDriver::ptr()->begin(cur_color, cur_depth, cur_color_rt_layer);
		GraphicDriver::ptr()->setViewport(cur_viewport);

		for(unsigned int i = 0; i < sky_dome->getMeshesNumber(); ++i)
		{
			shared_ptr<rs::Material> mat = sky_dome->getMesh(i)->getMaterial();
			mat->addTexture("sky_cube", sky_cubemap->getTexture());
			sky_dome->drawMesh(i, camera);
		};
	}
	else
	{
		for(unsigned int i = 0; i < sky_dome->getMeshesNumber(); ++i)
		{
			shared_ptr<gs::Effect> sky_effect = sky_dome->getMesh(i)->getEffect();
			if(current_time.getCurrentDayMinute() >= sun.getSunsetTime() || current_time.getCurrentDayMinute() <= sun.getSunriseTime())
				sky_effect->setMacro("NIGHT_SKY", 1);
			else
				sky_effect->setMacro("NIGHT_SKY", 0);
			sky_effect->setVect3("zenit_distribution", &zenith_distribution.vec);
			sky_effect->setVect3("A_coeff", &A_coeff.vec);
			sky_effect->setVect3("B_coeff", &B_coeff.vec);
			sky_effect->setVect3("C_coeff", &C_coeff.vec);
			sky_effect->setVect3("D_coeff", &D_coeff.vec);
			sky_effect->setVect3("E_coeff", &E_coeff.vec);
			sky_effect->setVect3("zenit_direction", &Vect3(0, 1, 0));
			sky_effect->setVect3("sun_direction", &sun.getSunDirection());
			sky_dome->drawMesh(i, camera);
		};
	};

	if(is_night_sky_stars_enable)
	{
		if(current_time.getCurrentDayMinute() >= sun.getSunsetTime() || current_time.getCurrentDayMinute() <= sun.getSunriseTime())
		{
			shared_ptr<rs::StaticModel> stars_buffer = stars->getVisibleStarsBuffer();
			stars_buffer->setTranslation(camera_position);
			for(unsigned int j = 0; j < stars_buffer->getMeshesNumber(); ++j)
			{
				stars_buffer->drawMesh(j, camera);
			};
		};
	};
	camera->setZfar(zfar);
};
