// Ryzom - MMORPG Framework <http://dev.ryzom.com/projects/ryzom/>
// Copyright (C) 2010  Winch Gate Property Limited
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program 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 Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

#include "stdpch.h"
//
#include "nel/3d/u_instance_material.h"
#include "nel/3d/u_particle_system_instance.h"
#include "nel/3d/u_instance.h"
//
#include "nel/misc/path.h"
#include "nel/misc/file.h"
//
#include "sky_object.h"
#include "sky.h"

using namespace NLMISC;
using namespace NL3D;

////////////////////////////
// CSkyObject::CColorInfo //
////////////////////////////

// *************************************************************************************************
void CSkyObject::CColorInfo::init(const CSkyObjectSheet::CColorInfoSheet &ci,
								  std::map<std::string, CBitmap *> &bitmapByName,
								  std::vector<CBitmap *> &builtBitmaps)
{
	Mode = ci.Mode;
	bool alreadyBuilt;
	Map = buildSharedBitmap(ci.MapName, bitmapByName, builtBitmaps, alreadyBuilt);
}

// *************************************************************************************************
CRGBA CSkyObject::CColorInfo::computeColor(float dayPart, float weatherLevel, CRGBA fogColor)
{
	switch(Mode)
	{
		case Unused:		return CRGBA(0, 0, 0, 0);
		case FogColor:		return fogColor;
		case BitmapColor:
			clamp(dayPart, 0.f, 1.f);
			clamp(weatherLevel, 0.f, 1.f);
			if (!Map) return CRGBA(0, 0, 0, 0);
			return Map->getColor(dayPart, weatherLevel, true, false);
		case BitmapColorModulatedByFogColor:
		{
			clamp(dayPart, 0.f, 1.f);
			clamp(weatherLevel, 0.f, 1.f);
			if (!Map) return CRGBA(0, 0, 0, 0);
			CRGBA result = Map->getColor(dayPart, weatherLevel, true, false);
			result.modulateFromColor(result, fogColor);
			return result;
		}
		break;
		default:
			nlassert(0); // unknwon type
		break;
	}
	return CRGBA(0, 0, 0, 0);
}


// *************************************************************************************************
////////////////////////////////////
// CSkyObject::CColorGradientInfo //
////////////////////////////////////

void CSkyObject::CColorGradientInfo::init(const CSkyObjectSheet::CColorGradientInfoSheet &cgis,
							  std::map<std::string,CBitmap *> &bitmapByName,
							  std::vector<CBitmap *> &builtBitmaps)
{
	TargetTextureStage = cgis.TargetTextureStage;
	WeatherToGradient.resize(cgis.WeatherToGradient.size());
	CBitmap *lastSeenBitmap = NULL;
	for(uint k = 0; k < cgis.WeatherToGradient.size(); ++k)
	{
		bool alreadyBuilt;
		WeatherToGradient[k] = buildSharedBitmap(cgis.WeatherToGradient[k], bitmapByName, builtBitmaps, alreadyBuilt);
		if (WeatherToGradient[k])
		{
			if (!WeatherToGradient[k]->convertToType(CBitmap::RGBA))
			{
				// can't use bitmap..
				WeatherToGradient[k] = NULL; // don't do a delete here because it'is 'builtBitmaps' that has ownership
			}
			else
			{
				if (!alreadyBuilt)
				{
					// rotate the bitmap because it is faster to blit a row than a column
					WeatherToGradient[k]->rot90CCW();
					WeatherToGradient[k]->flipV();
				}
				if (lastSeenBitmap)
				{
					if (WeatherToGradient[k]->getWidth() != lastSeenBitmap->getWidth() ||
						WeatherToGradient[k]->getHeight() != lastSeenBitmap->getHeight()
					   )
					{
						nlwarning("All bitmaps must have the same size in the gradient");
					}
				}
				lastSeenBitmap = WeatherToGradient[k];
			}
		}
	}
}

/*
static void dumpGrad(CBitmap &bm)
{
	CRGBA *pixs =  (CRGBA *) &bm.getPixels(0)[0];
	for(uint k = 0; k < bm.getWidth(); ++k)
	{
		nlinfo("(r, g, b, a) = (%d, %d, %d, %d)", pixs[k].R, pixs[k].G, pixs[k].B, pixs[k].A);
	}
}
*/


// *************************************************************************************************
void CSkyObject::CColorGradientInfo::setup(NL3D::UInstance instance, float dayPart, float weatherLevel, CBitmap &gradientCache, CBitmap &gradientCacheBlurred)
{
	if (instance.empty()) return;
	if (WeatherToGradient.empty()) return;
	clamp(dayPart, 0.f, 1.f);
	clamp(weatherLevel, 0.f, 1.f);
	// takes 2 closest bitmaps to do the blend
	uint bm0Index = std::min(uint(weatherLevel * WeatherToGradient.size()), uint(WeatherToGradient.size() - 1));
	uint bm1Index = std::min(uint(weatherLevel * WeatherToGradient.size() + 1), uint(WeatherToGradient.size() - 1));
	CBitmap *bm0 = WeatherToGradient[bm0Index];
	CBitmap *bm1 = WeatherToGradient[bm1Index];
	if (!bm1 && !bm0) return;
	if (!bm1) bm1 = bm0;
	if (!bm0) bm0 = bm1;
	// make sure that both bitmap have the same size
	if (bm0->getWidth() != bm1->getWidth() || bm0->getHeight() != bm1->getHeight()) return;
	// extract the 2 slices before to blend
	uint slice0 = (uint) (dayPart * bm0->getHeight());
	uint slice1 = (uint) (dayPart * bm0->getHeight() + 1) % bm0->getHeight();
	//nlinfo("slice0 = %d", slice0);
	Slice0[0].resize(bm0->getWidth(), 1);
	Slice0[1].resize(bm0->getWidth(), 1);
	Slice0[0].blit(*bm0, 0, slice0, bm0->getWidth(), 1, 0, 0);
	Slice0[1].blit(*bm0, 0, slice1, bm0->getWidth(), 1, 0, 0);
	Slice0[0].blend(Slice0[0], Slice0[1], (uint) (256 * fmodf(dayPart * bm0->getHeight(), 1.f)), true);
	Slice1[0].resize(bm0->getWidth(), 1);
	Slice1[1].resize(bm0->getWidth(), 1);
	Slice1[0].blit(*bm1, 0, slice0, bm1->getWidth(), 1, 0, 0);
	Slice1[1].blit(*bm1, 0, slice1, bm1->getWidth(), 1, 0, 0);
	Slice1[0].blend(Slice1[0], Slice1[1], (uint) (256 * fmodf(dayPart * bm0->getHeight(), 1.f)), true);
	Slice0[0].blend(Slice0[0], Slice1[0], (uint) (256 * fmodf(weatherLevel * WeatherToGradient.size(), 1.f)), true);
	// see if blended result differs from cache, if so, update texture of the instance
	const uint32 *newGrad = (uint32 *) &Slice0[0].getPixels()[0];
	if (gradientCache.getPixels(0).empty())
	{
		gradientCache.resize(bm1->getWidth() , 1, CBitmap::RGBA);
	}
	const uint32 *oldGrad = (uint32 *) &gradientCache.getPixels()[0];
	nlassert(gradientCache.getWidth() == Slice0[0].getWidth());
	if (!std::equal(newGrad, newGrad + bm0->getWidth(), oldGrad))
	{
		// update the cache
		gradientCache.swap(Slice0[0]);
		// build a blurred version of the gradient cache (improve quality of gradient)
		if (gradientCacheBlurred.getWidth() != gradientCache.getWidth() ||
			gradientCacheBlurred.getHeight() != gradientCache.getHeight())
		{
			gradientCacheBlurred.resize(gradientCache.getWidth(), 1);
		}
		nlassert(gradientCacheBlurred.PixelFormat == gradientCache.PixelFormat);
		CRGBA *destPix = (CRGBA *) &gradientCacheBlurred.getPixels()[0];
		const CRGBA *srcPix = (const CRGBA *)  &gradientCache.getPixels(0)[0];
		*destPix++ = *srcPix ++;
		const CRGBA *lastSrcPix = srcPix + Slice0[0].getWidth() - 2;
		while (srcPix != lastSrcPix)
		{
			destPix->R = (uint8) (((uint16) srcPix[- 1].R +	(uint16) srcPix->R + (uint16) srcPix[1].R) * (256 / 3) >> 8);
			destPix->G = (uint8) (((uint16) srcPix[- 1].G +	(uint16) srcPix->G + (uint16) srcPix[1].G) * (256 / 3) >> 8);
			destPix->B = (uint8) (((uint16) srcPix[- 1].B +	(uint16) srcPix->B + (uint16) srcPix[1].B) * (256 / 3) >> 8);
			destPix->A = (uint8) (((uint16) srcPix[- 1].A +	(uint16) srcPix->A + (uint16) srcPix[1].A) * (256 / 3) >> 8);
			++ destPix;
			++ srcPix;
		}
		*destPix++ = *srcPix ++;
		// set the new texture
		uint numMaterials = instance.getNumMaterials();
		for(uint k = 0; k < numMaterials; ++k)
		{
			// do a free ccw rotate by swapping height & width (because height is 1)
			instance.getMaterial(k).setTextureMem(TargetTextureStage,
												   &gradientCacheBlurred.getPixels()[0],
												   gradientCacheBlurred.getWidth() * gradientCacheBlurred.getHeight() * sizeof(uint32),
												   false,
												   false,
												   gradientCacheBlurred.getHeight(),
												   gradientCacheBlurred.getWidth());
			// clamp on v coordinate
			instance.getMaterial(k).setWrapT(TargetTextureStage, NL3D::UInstanceMaterial::Clamp);
		}
	}
}

// *************************************************************************************************
////////////////
// CSkyObject //
////////////////

void CSkyObject::init(const CSkyObjectSheet::CVersionSheet &sheet,
					  NL3D::UInstance instance,
					  std::map<std::string,CBitmap *> &bitmapByName,
					  std::vector<CBitmap *> &builtBitmaps,
					  bool	visibleInMainScene,
					  bool	visibleInEnvMap
					 )
{
	if (instance.empty()) return;
	Instance = instance;
	// set display priority
	instance.setTransparencyPriority(sheet.TransparencyPriority);
	PS.cast(Instance);
	//
	DiffuseColor.init(sheet.DiffuseColor, bitmapByName, builtBitmaps);
	ParticleEmitters.init(sheet.ParticleEmitters, bitmapByName, builtBitmaps);
	for(uint k = 0; k < SKY_MAX_NUM_STAGE; ++k)
	{
		ConstantColor[k].init(sheet.ConstantColor[k], bitmapByName, builtBitmaps);
		bool alreadyBuilt;
		if (!sheet.OffsetUBitmap[k].empty())
		{
			OffsetUBitmap[k] = buildSharedBitmap(sheet.OffsetUBitmap[k], bitmapByName, builtBitmaps, alreadyBuilt);
		}
		if (!sheet.OffsetVBitmap[k].empty())
		{
			OffsetVBitmap[k] = buildSharedBitmap(sheet.OffsetVBitmap[k], bitmapByName, builtBitmaps, alreadyBuilt);
		}
	}
	ColorGradient.init(sheet.ColorGradient, bitmapByName, builtBitmaps);
	RefColor = sheet.RefColor;
	std::copy(sheet.TexPanner, sheet.TexPanner + SKY_MAX_NUM_STAGE, TexPanner);
	std::copy(sheet.OffsetFactor, sheet.OffsetFactor + SKY_MAX_NUM_STAGE, OffsetFactor);
	for(uint k = 0; k < SKY_MAX_NUM_FX_USER_PARAMS; ++k)
	{
		if (!sheet.FXUserParamBitmap[k].empty())
		{
			bool alreadyBuilt;
			FXUserParams[k] = buildSharedBitmap(sheet.FXUserParamBitmap[k], bitmapByName, builtBitmaps, alreadyBuilt);
		}
	}
	Name = sheet.ShapeName;
	VisibleInMainScene = visibleInMainScene;
	VisibleInEnvMap = visibleInEnvMap;
}


// *************************************************************************************************
bool CSkyObject::setup(const CClientDate &date, const CClientDate &animationDate, float numHoursInDay, float weatherLevel, CRGBA fogColor, bool envMapScene)
{
	if (Instance.empty()) return false;
	Active = true;
	nlassert(numHoursInDay > 0.f);
	float dayPart = date.Hour / numHoursInDay;
	clamp(dayPart, 0.f, 1.f);
	clamp(weatherLevel, 0.f, 1.f);
	if (DiffuseColor.Mode != Unused)
	{
		CRGBA newDiffuseColor = DiffuseColor.computeColor(dayPart, weatherLevel, fogColor);
		if (newDiffuseColor != LastDiffuseColor)
		{
			// is it a particle system
			if (!PS.empty())
			{
				PS.setUserColor(newDiffuseColor);
				// PS are hiden / shown, so must unfreeze hrc. (adding an isntance group causes hrc to be frozen)
				PS.unfreezeHRC();
			}
			else
			{
				// set diffuse color for each material with normal shader
				uint numMaterials = Instance.getNumMaterials();
				for(uint k = 0; k < numMaterials; ++k)
				{
					UInstanceMaterial im = Instance.getMaterial(k);
					if (im.isLighted())
					{
						Instance.getMaterial(k).setDiffuse(newDiffuseColor);
					}
					else
					{
						Instance.getMaterial(k).setColor(newDiffuseColor);
					}
				}
				// set mean color for other objects (lens flares...)
				Instance.setMeanColor(newDiffuseColor);
			}
			LastDiffuseColor = newDiffuseColor;
		}
		if (RefColor == DiffuseColorRef) // if this is the ref color, then the object is not visible if alpha is 0
		{
			if (newDiffuseColor.A == 0)
			{
				Active = false;
			}
		}
	}
	// is it a particle system
	if (ParticleEmitters.Mode != Unused)
	{
		CRGBA newParticleEmittersColor = ParticleEmitters.computeColor(dayPart, weatherLevel, fogColor);
		if (newParticleEmittersColor != LastParticleEmittersColor)
		{
			if (!PS.empty())
			{
				// emitters are on is any of the components is not 0
				PS.activateEmitters(newParticleEmittersColor != CRGBA::Black);
			}
			LastParticleEmittersColor = newParticleEmittersColor;
		}
		if (RefColor == ParticleEmittersColorRef) // if this is the ref color, then the object is not visible if alpha is 0
		{
			if (LastParticleEmittersColor == CRGBA::Black)
			{
				if (!PS.hasParticles()) // can deactivate PS only when all particles are off
				{
					Active = false;
				}
			}
		}
	}
	uint numMaterials = Instance.getNumMaterials();
	for(uint k = 0; k < SKY_MAX_NUM_STAGE; ++k)
	{
		if (ConstantColor[k].Mode != Unused)
		{
			CRGBA newConstantColor = ConstantColor[k].computeColor(dayPart, weatherLevel, fogColor);
			if (newConstantColor != LastConstantColor[k])
			{
				for(uint l = 0; l < numMaterials; ++l)
				{
					Instance.getMaterial(l).setConstantColor(k, newConstantColor);
				}
				LastConstantColor[k] = newConstantColor;
			}
			if (RefColor == (TSkyRefColor) (ConstantColor0Ref + k))
			{
				if (newConstantColor.A == 0)
				{
					Active = false;
				}
			}
		}
	}
	bool draw = Active;
	if (envMapScene && !VisibleInEnvMap)
	{
		draw = false;
	}
	else if (!envMapScene && !VisibleInMainScene)
	{
		draw = false;
	}
	if (draw)
	{
		Instance.show();
		Instance.unfreezeHRC();
	}
	else
	{
		Instance.hide();
		Instance.freezeHRC();
	}
	double animTime = animationDate.Hour  + (double) animationDate.Day * (double) numHoursInDay;
	if (PS.empty())
	{
		////////////////////
		// gradient setup //
		////////////////////
		ColorGradient.setup(Instance, dayPart, weatherLevel, GradientCache, GradientCacheBlurred);

		///////////////////////
		// tex panning setup //
		///////////////////////
		for(uint k = 0; k < SKY_MAX_NUM_STAGE; ++k)
		{
			if (TexPanner[k].U != 0.f || TexPanner[k].V != 0.f ||
				OffsetUBitmap[k] != NULL || OffsetVBitmap[k] != NULL )
			{
				//nlinfo("global date = %f", animTime);
				// there's tex panning for that stage
				double u = TexPanner[k].U * animTime;
				u = fmod(u, 1);
				double v = TexPanner[k].V * animTime;
				v = fmod(v, 1);
				CVector offset((float) u, (float) v, 0.f);
				// apply scaling if needed
				if (OffsetUBitmap[k])
				{
					offset.x += OffsetFactor[k].U * OffsetUBitmap[k]->getColor(dayPart, weatherLevel, true, false).R;
				}
				if (OffsetVBitmap[k])
				{
					offset.y += OffsetFactor[k].V * OffsetVBitmap[k]->getColor(dayPart, weatherLevel, true, false).R;
				}
				CMatrix mat;
				mat.setPos(offset);
				for(uint l = 0; l < numMaterials; ++l)
				{
					Instance.getMaterial(l).enableUserTexMat(k);
					Instance.getMaterial(l).setUserTexMat(k, mat);
				}
			}
		}
	}
	else
	{
		// user params setup
		for(uint k = 0; k < SKY_MAX_NUM_FX_USER_PARAMS; ++k)
		{
			if (FXUserParams[k])
			{
				CRGBA color = FXUserParams[k]->getColor(dayPart, weatherLevel, true, false);
				PS.setUserParam(k, color.R / 255.f);
			}
		}
	}
	return Active;
}

// *************************************************************************************************
CSkyObject::~CSkyObject()
{
}