#include "common.h"

#include "main.h"
#include "CutsceneMgr.h"
#include "Lights.h"
#include "Camera.h"
#include "Weather.h"
#include "World.h"
#include "Collision.h"
#include "Sprite.h"
#include "Timer.h"
#include "PointLights.h"

int16 CPointLights::NumLights;
CRegisteredPointLight CPointLights::aLights[NUMPOINTLIGHTS];
CVector CPointLights::aCachedMapReads[32];
float CPointLights::aCachedMapReadResults[32];
int32 CPointLights::NextCachedValue;

void
CPointLights::Init(void)
{
	for(int i = 0; i < ARRAY_SIZE(aCachedMapReads); i++){
		aCachedMapReads[i] = CVector(0.0f, 0.0f, 0.0f);
		aCachedMapReadResults[i] = 0.0f;
	}
	NextCachedValue = 0;
}

void
CPointLights::InitPerFrame(void)
{
	NumLights = 0;
}

#define MAX_DIST 22.0f

void
CPointLights::AddLight(uint8 type, CVector coors, CVector dir, float radius, float red, float green, float blue, uint8 fogType, bool castExtraShadows)
{
	CVector dist;
	float distance;

	// The check is done in some weird way in the game
	// we're doing it a bit better here
	if(NumLights >= NUMPOINTLIGHTS)
		return;

	dist = coors - TheCamera.GetPosition();
	if(Abs(dist.x) < MAX_DIST && Abs(dist.y) < MAX_DIST){
		distance = dist.Magnitude();
		if(distance < MAX_DIST){
			aLights[NumLights].type = type;
			aLights[NumLights].fogType = fogType;
			aLights[NumLights].coors = coors;
			aLights[NumLights].dir = dir;
			aLights[NumLights].radius = radius;
			aLights[NumLights].castExtraShadows = castExtraShadows;
			if(distance < MAX_DIST*0.75f){
				aLights[NumLights].red = red;
				aLights[NumLights].green = green;
				aLights[NumLights].blue = blue;
			}else{
				float fade = 1.0f - (distance/MAX_DIST - 0.75f)*4.0f;
				aLights[NumLights].red = red * fade;
				aLights[NumLights].green = green * fade;
				aLights[NumLights].blue = blue * fade;
			}
			NumLights++;
		}
	}
}

float
CPointLights::GenerateLightsAffectingObject(Const CVector *objCoors)
{
	int i;
	float ret;
	CVector dist;
	float radius, distance;

	ret = 1.0f;
	for(i = 0; i < NumLights; i++){
		if(aLights[i].type == LIGHT_FOGONLY || aLights[i].type == LIGHT_FOGONLY_ALWAYS)
			continue;

		// same weird distance calculation. simplified here
		dist = aLights[i].coors - *objCoors;
		radius = aLights[i].radius;
		if(Abs(dist.x) < radius &&
		   Abs(dist.y) < radius &&
		   Abs(dist.z) < radius){

			distance = dist.Magnitude();
			if(distance < radius){

				float distNorm = distance/radius;
				if(aLights[i].type == LIGHT_DARKEN){
					// darken the object the closer it is
					ret *= distNorm;
				}else{
					float intensity;
					// distance fade
					if(distNorm < 0.5f)
						intensity = 1.0f;
					else
						intensity = 1.0f - (distNorm - 0.5f)/(1.0f - 0.5f);

					if(distance != 0.0f){
						CVector dir = dist / distance;

						if(aLights[i].type == LIGHT_DIRECTIONAL){
							float dot = -DotProduct(dir, aLights[i].dir);
							intensity *= Max((dot-0.5f)*2.0f, 0.0f);
						}

						if(intensity > 0.0f)
							AddAnExtraDirectionalLight(Scene.world,
								dir.x, dir.y, dir.z,
								aLights[i].red*intensity, aLights[i].green*intensity, aLights[i].blue*intensity);
					}
				}
			}
		}
	}

	return ret;
}

extern RwRaster *gpPointlightRaster;

void
CPointLights::RemoveLightsAffectingObject(void)
{
	RemoveExtraDirectionalLights(Scene.world);
}

// for directional fog
#define FOG_AREA_LENGTH 12.0f
#define FOG_AREA_WIDTH 5.0f
// for pointlight fog
#define FOG_AREA_RADIUS 9.0f

float FogSizes[8] = { 1.3f, 2.0f, 1.7f, 2.0f, 1.4f, 2.1f, 1.5f, 2.3f };

void
CPointLights::RenderFogEffect(void)
{
	int i;
	float fogginess;
	CColPoint point;
	CEntity *entity;
	float xmin, ymin;
	float xmax, ymax;
	int16 xi, yi;
	CVector spriteCoors;
	float spritew, spriteh;

	if(CCutsceneMgr::IsRunning())
		return;

	PUSH_RENDERGROUP("CPointLights::RenderFogEffect");

	RwRenderStateSet(rwRENDERSTATEZWRITEENABLE, (void*)FALSE);
	RwRenderStateSet(rwRENDERSTATEVERTEXALPHAENABLE, (void*)TRUE);
	RwRenderStateSet(rwRENDERSTATESRCBLEND, (void*)rwBLENDONE);
	RwRenderStateSet(rwRENDERSTATEDESTBLEND, (void*)rwBLENDONE);
	RwRenderStateSet(rwRENDERSTATETEXTURERASTER, gpPointlightRaster);

	CSprite::InitSpriteBuffer();

	for(i = 0; i < NumLights; i++){
		if(aLights[i].fogType != FOG_NORMAL && aLights[i].fogType != FOG_ALWAYS)
			continue;

		fogginess = aLights[i].fogType == FOG_NORMAL ? CWeather::Foggyness : 1.0f;
		if(fogginess == 0.0f)
			continue;

		if(aLights[i].type == LIGHT_DIRECTIONAL){

			// TODO: test this. haven't found directional fog so far

			float coors2X = aLights[i].coors.x + FOG_AREA_LENGTH*aLights[i].dir.x;
			float coors2Y = aLights[i].coors.y + FOG_AREA_LENGTH*aLights[i].dir.y;

			if(coors2X < aLights[i].coors.x){
				xmin = coors2X;
				xmax = aLights[i].coors.x;
			}else{
				xmax = coors2X;
				xmin = aLights[i].coors.x;
			}
			if(coors2Y < aLights[i].coors.y){
				ymin = coors2Y;
				ymax = aLights[i].coors.y;
			}else{
				ymax = coors2Y;
				ymin = aLights[i].coors.y;
			}

			xmin -= 5.0f;
			ymin -= 5.0f;
			xmax += 5.0f;
			ymax += 5.0f;

			for(xi = (int16)xmin - (int16)xmin % 4; xi <= (int16)xmax + 4; xi += 4){
				for(yi = (int16)ymin - (int16)ymin % 4; yi <= (int16)ymax + 4; yi += 4){
					// Some kind of pseudo random number?
					int r = (xi ^ yi)>>2 & 0xF;
					if((r & 1) == 0)
						continue;

					// Check if fog effect is close enough to directional line in x and y
					float dx = xi - aLights[i].coors.x;
					float dy = yi - aLights[i].coors.y;
					float dot = dx*aLights[i].dir.x + dy*aLights[i].dir.y;
					float distsq = sq(dx) + sq(dy);
					float linedistsq = distsq - sq(dot);
					if(dot > 0.0f && dot < FOG_AREA_LENGTH && linedistsq < sq(FOG_AREA_WIDTH)){
						CVector fogcoors(xi, yi, aLights[i].coors.z + 10.0f);
						if(CWorld::ProcessVerticalLine(fogcoors, fogcoors.z - 20.0f,
								point, entity, true, false, false, false, true, false, nil)){
							// Now same check again in xyz
							fogcoors.z = point.point.z + 1.3f;
							// actually we don't have to recalculate x and y, but the game does it that way
							dx = xi - aLights[i].coors.x;
							dy = yi - aLights[i].coors.y;
							float dz = fogcoors.z - aLights[i].coors.z;
							dot = dx*aLights[i].dir.x + dy*aLights[i].dir.y + dz*aLights[i].dir.z;
							distsq = sq(dx) + sq(dy) + sq(dz);
							linedistsq = distsq - sq(dot);
							if(dot > 0.0f && dot < FOG_AREA_LENGTH && linedistsq < sq(FOG_AREA_WIDTH)){
								float intensity = 158.0f * fogginess;
								// more intensity the smaller the angle
								intensity *= dot/Sqrt(distsq);
								// more intensity the closer to light source
								intensity *= 1.0f - sq(dot/FOG_AREA_LENGTH);
								// more intensity the closer to line
								intensity *= 1.0f - sq(Sqrt(linedistsq) / FOG_AREA_WIDTH);

								if(CSprite::CalcScreenCoors(fogcoors, &spriteCoors, &spritew, &spriteh, true)) {
									float rotation = (CTimer::GetTimeInMilliseconds()&0x1FFF) * 2*3.14f / 0x2000;
									float size = FogSizes[r>>1];
									CSprite::RenderBufferedOneXLUSprite_Rotate_Aspect(spriteCoors.x, spriteCoors.y, spriteCoors.z,
										spritew * size, spriteh * size,
										aLights[i].red * intensity, aLights[i].green * intensity, aLights[i].blue * intensity,
										intensity, 1/spriteCoors.z, rotation, 255);
								}
							}
						}
					}
				}
			}

		}else if(aLights[i].type == LIGHT_POINT || aLights[i].type == LIGHT_FOGONLY || aLights[i].type == LIGHT_FOGONLY_ALWAYS){
			float groundZ;
			if(ProcessVerticalLineUsingCache(aLights[i].coors, &groundZ)){
				xmin = aLights[i].coors.x - FOG_AREA_RADIUS;
				ymin = aLights[i].coors.y - FOG_AREA_RADIUS;
				xmax = aLights[i].coors.x + FOG_AREA_RADIUS;
				ymax = aLights[i].coors.y + FOG_AREA_RADIUS;

				for(xi = (int16)xmin - (int16)xmin % 2; xi <= (int16)xmax + 2; xi += 2){
					for(yi = (int16)ymin - (int16)ymin % 2; yi <= (int16)ymax + 2; yi += 2){
						// Some kind of pseudo random number?
						int r = (xi ^ yi)>>1 & 0xF;
						if((r & 1) == 0)
							continue;

						float dx = xi - aLights[i].coors.x;
						float dy = yi - aLights[i].coors.y;
						float lightdist = Sqrt(sq(dx) + sq(dy));
						if(lightdist < FOG_AREA_RADIUS){
							dx = xi - TheCamera.GetPosition().x;
							dy = yi - TheCamera.GetPosition().y;
							float camdist = Sqrt(sq(dx) + sq(dy));
							if(camdist < MAX_DIST){
								float intensity;
								// distance fade
								if(camdist < MAX_DIST/2)
									intensity = 1.0f;
								else
									intensity = 1.0f - (camdist - MAX_DIST/2) / (MAX_DIST/2);
								intensity *= 132.0f * fogginess;
								// more intensity the closer to light source
								intensity *= 1.0f - sq(lightdist / FOG_AREA_RADIUS);

								CVector fogcoors(xi, yi, groundZ + 1.6f);
								if(CSprite::CalcScreenCoors(fogcoors, &spriteCoors, &spritew, &spriteh, true)) {
									float rotation = (CTimer::GetTimeInMilliseconds()&0x3FFF) * 2*3.14f / 0x4000;
									float size = FogSizes[r>>1];
									CSprite::RenderBufferedOneXLUSprite_Rotate_Aspect(spriteCoors.x, spriteCoors.y, spriteCoors.z,
										spritew * size, spriteh * size,
										aLights[i].red * intensity, aLights[i].green * intensity, aLights[i].blue * intensity,
										intensity, 1/spriteCoors.z, rotation, 255);
								}
							}
						}
					}
				}
			}
		}
	}

	CSprite::FlushSpriteBuffer();

	POP_RENDERGROUP();
}

bool
CPointLights::ProcessVerticalLineUsingCache(CVector coors, float *groundZ)
{
	for(int i = 0; i < ARRAY_SIZE(aCachedMapReads); i++)
		if(aCachedMapReads[i] == coors){
			*groundZ = aCachedMapReadResults[i];
			return true;
		}

	CColPoint point;
	CEntity *entity;
	if(CWorld::ProcessVerticalLine(coors, coors.z - 20.0f, point, entity, true, false, false, false, true, false, nil)){
		aCachedMapReads[NextCachedValue] = coors;
		aCachedMapReadResults[NextCachedValue] = point.point.z;
		NextCachedValue = (NextCachedValue+1) % ARRAY_SIZE(aCachedMapReads);
		*groundZ = point.point.z;
		return true;
	}
	return false;
}
