/**
 * =========================================================================
 * File        : ShadowMap.cpp
 * Project     : Pyrogenesis
 * Description : Shadow mapping related texture and matrix management
 *
 * @author Nicolai Hähnle <nicolai@wildfiregames.com>
 * =========================================================================
 */

#include "precompiled.h"

#include "lib/ogl.h"
#include "ps/CLogger.h"

#include "graphics/LightEnv.h"

#include "maths/Bound.h"
#include "maths/MathUtil.h"
#include "maths/Matrix3D.h"

#include "renderer/Renderer.h"
#include "renderer/ShadowMap.h"

#define LOG_CATEGORY "graphics"


///////////////////////////////////////////////////////////////////////////////////////////////////
// ShadowMap implementation

/**
 * Struct ShadowMapInternals: Internal data for the ShadowMap implementation
 */
struct ShadowMapInternals
{
	// whether we're using depth texture or luminance map
	bool UseDepthTexture;
	// bit depth for the depth texture, if used
	int DepthTextureBits;
	// if non-zero, we're using EXT_framebuffer_object for shadow rendering,
	// and this is the framebuffer
	GLuint Framebuffer;
	// handle of shadow map
	GLuint Texture;
	// width, height of shadow map
	u32 Width, Height;
	// used width, height of shadow map
	u32 EffectiveWidth, EffectiveHeight;
	// transform light space into projected light space
	// in projected light space, the shadowbound box occupies the [-1..1] cube
	// calculated on BeginRender, after the final shadow bounds are known
	CMatrix3D LightProjection;
	// Transform world space into light space; calculated on SetupFrame
	CMatrix3D LightTransform;
	// Transform world space into texture space of the shadow map;
	// calculated on BeginRender, after the final shadow bounds are known
	CMatrix3D TextureMatrix;

	// transform light space into world space
	CMatrix3D InvLightTransform;
	// bounding box of shadowed objects in light space
	CBound ShadowBound;

	// Camera transformed into light space
	CCamera LightspaceCamera;

	// Helper functions
	void CalcShadowMatrices();
	void CreateTexture();
};


///////////////////////////////////////////////////////////////////////////////////////////////////
// Construction/Destruction
ShadowMap::ShadowMap()
{
	m = new ShadowMapInternals;
	m->Framebuffer = 0;
	m->Texture = 0;
	m->Width = 0;
	m->Height = 0;
	m->EffectiveWidth = 0;
	m->EffectiveHeight = 0;
	m->UseDepthTexture = false;
	m->DepthTextureBits = 16;
}


ShadowMap::~ShadowMap()
{
	if (m->Texture)
		glDeleteTextures(1, &m->Texture);
	if (m->Framebuffer)
		pglDeleteFramebuffersEXT(1, &m->Framebuffer);

	delete m;
}

///////////////////////////////////////////////////////////////////////////////////////////////////
// Force the texture/buffer/etc to be recreated, particularly when the renderer's
// size has changed
void ShadowMap::RecreateTexture()
{
	if (m->Texture)
		glDeleteTextures(1, &m->Texture);
	if (m->Framebuffer)
		pglDeleteFramebuffersEXT(1, &m->Framebuffer);

	m->Texture = 0;
	m->Framebuffer = 0;

	// (Texture will be constructed in next SetupFrame)
}

//////////////////////////////////////////////////////////////////////////////
// SetupFrame: camera and light direction for this frame
void ShadowMap::SetupFrame(const CCamera& camera, const CVector3D& lightdir)
{
	if (!m->Texture)
		m->CreateTexture();

	CVector3D z = lightdir;
	CVector3D y;
	CVector3D x = camera.m_Orientation.GetIn();
	CVector3D eyepos = camera.m_Orientation.GetTranslation();

	z.Normalize();
	x -= z * z.Dot(x);
	if (x.GetLength() < 0.001)
	{
		// this is invoked if the camera and light directions almost coincide
		// assumption: light direction has a significant Z component
		x = CVector3D(1.0, 0.0, 0.0);
		x -= z * z.Dot(x);
	}
	x.Normalize();
	y = z.Cross(x);

	// X axis perpendicular to light direction, flowing along with view direction
	m->LightTransform._11 = x.X;
	m->LightTransform._12 = x.Y;
	m->LightTransform._13 = x.Z;

	// Y axis perpendicular to light and view direction
	m->LightTransform._21 = y.X;
	m->LightTransform._22 = y.Y;
	m->LightTransform._23 = y.Z;

	// Z axis is in direction of light
	m->LightTransform._31 = z.X;
	m->LightTransform._32 = z.Y;
	m->LightTransform._33 = z.Z;

	// eye is at the origin of the coordinate system
	m->LightTransform._14 = -x.Dot(eyepos);
	m->LightTransform._24 = -y.Dot(eyepos);
	m->LightTransform._34 = -z.Dot(eyepos);

	m->LightTransform._41 = 0.0;
	m->LightTransform._42 = 0.0;
	m->LightTransform._43 = 0.0;
	m->LightTransform._44 = 1.0;

	m->LightTransform.GetInverse(m->InvLightTransform);
	m->ShadowBound.SetEmpty();

	//
	m->LightspaceCamera = camera;
	m->LightspaceCamera.m_Orientation = m->LightTransform * camera.m_Orientation;
	m->LightspaceCamera.UpdateFrustum();
}


//////////////////////////////////////////////////////////////////////////////
// AddShadowedBound: add a world-space bounding box to the bounds of shadowed
// objects
void ShadowMap::AddShadowedBound(const CBound& bounds)
{
	CBound lightspacebounds;

	bounds.Transform(m->LightTransform, lightspacebounds);
	m->ShadowBound += lightspacebounds;
}


///////////////////////////////////////////////////////////////////////////////////////////////////
// CalcShadowMatrices: calculate required matrices for shadow map generation - the light's
// projection and transformation matrices
void ShadowMapInternals::CalcShadowMatrices()
{
	CRenderer& renderer = g_Renderer;

	float minZ = ShadowBound[0].Z;

	ShadowBound.IntersectFrustumConservative(LightspaceCamera.GetFrustum());

	// minimum Z bound must not be clipped too much, because objects that lie outside
	// the shadow bounds cannot cast shadows either
	// the 2.0 is rather arbitrary: it should be big enough so that we won't accidently miss
	// a shadow generator, and small enough not to affect Z precision
	ShadowBound[0].Z = minZ - 2.0;

	// Setup orthogonal projection (lightspace -> clip space) for shadowmap rendering
	CVector3D scale = ShadowBound[1] - ShadowBound[0];
	CVector3D shift = (ShadowBound[1] + ShadowBound[0]) * -0.5;

	if (scale.X < 1.0)
		scale.X = 1.0;
	if (scale.Y < 1.0)
		scale.Y = 1.0;
	if (scale.Z < 1.0)
		scale.Z = 1.0;

	scale.X = 2.0 / scale.X;
	scale.Y = 2.0 / scale.Y;
	scale.Z = 2.0 / scale.Z;

	LightProjection.SetZero();
	LightProjection._11 = scale.X;
	LightProjection._14 = shift.X * scale.X;
	LightProjection._22 = scale.Y;
	LightProjection._24 = shift.Y * scale.Y;
	LightProjection._33 = scale.Z;
	LightProjection._34 = shift.Z * scale.Z + renderer.m_ShadowZBias;
	LightProjection._44 = 1.0;


	// Calculate texture matrix by creating the clip space to texture coordinate matrix
	// and then concatenating all matrices that have been calculated so far
	CMatrix3D lightToTex;
	float texscalex = (float)EffectiveWidth / (float)Width;
	float texscaley = (float)EffectiveHeight / (float)Height;
	float texscalez = 1.0;

	texscalex = texscalex / (ShadowBound[1].X - ShadowBound[0].X);
	texscaley = texscaley / (ShadowBound[1].Y - ShadowBound[0].Y);
	texscalez = texscalez / (ShadowBound[1].Z - ShadowBound[0].Z);

	lightToTex.SetZero();
	lightToTex._11 = texscalex;
	lightToTex._14 = -ShadowBound[0].X * texscalex;
	lightToTex._22 = texscaley;
	lightToTex._24 = -ShadowBound[0].Y * texscaley;
	lightToTex._33 = texscalez;
	lightToTex._34 = -ShadowBound[0].Z * texscalez;
	lightToTex._44 = 1.0;

	TextureMatrix = lightToTex * LightTransform;
}



//////////////////////////////////////////////////////////////////////////
// Create the shadow map
void ShadowMapInternals::CreateTexture()
{
	// Cleanup
	if (Texture)
	{
		glDeleteTextures(1, &Texture);
		Texture = 0;
	}
	if (Framebuffer)
	{
		pglDeleteFramebuffersEXT(1, &Framebuffer);
		Framebuffer = 0;
	}

	// Prepare FBO if available
	// Note: luminance is not an RGB format, so a luminance texture cannot be used
	// as a color buffer
	if (UseDepthTexture && g_Renderer.GetCapabilities().m_FramebufferObject)
	{
		pglGenFramebuffersEXT(1, &Framebuffer);
	}

	// get shadow map size as next power of two up from view width and height
	Width = g_Renderer.GetWidth();
	Width = std::min(RoundUpToPowerOf2(Width), ogl_max_tex_size);
	Height = g_Renderer.GetHeight();
	Height = std::min(RoundUpToPowerOf2(Height), ogl_max_tex_size);

	if (Framebuffer)
	{
		EffectiveWidth = Width;
		EffectiveHeight = Height;
	}
	else
	{
		EffectiveWidth = std::min(Width, (u32)g_Renderer.GetWidth());
		EffectiveHeight = std::min(Height, (u32)g_Renderer.GetHeight());
	}

	const char* formatname = "LUMINANCE";

	if (UseDepthTexture)
	{
		switch(DepthTextureBits) {
		case 16: formatname = "DEPTH_COMPONENT16"; break;
		case 24: formatname = "DEPTH_COMPONENT24"; break;
		case 32: formatname = "DEPTH_COMPONENT32"; break;
		default: formatname = "DEPTH_COMPONENT"; break;
		}
	}

	LOG(NORMAL, LOG_CATEGORY, "Creating shadow texture (size %ix%i) (format = %s)%s",
		Width, Height, formatname, Framebuffer ? " (using EXT_framebuffer_object)" : "");

	// create texture object
	glGenTextures(1, &Texture);
	g_Renderer.BindTexture(0, Texture);

	u32 size = Width*Height;

	if (UseDepthTexture)
	{
		GLenum format;

		switch(DepthTextureBits) {
		case 16: format = GL_DEPTH_COMPONENT16; break;
		case 24: format = GL_DEPTH_COMPONENT24; break;
		case 32: format = GL_DEPTH_COMPONENT32; break;
		default: format = GL_DEPTH_COMPONENT; break;
		}

		float* buf = new float[size];
		for(uint i = 0; i < size; i++) buf[i] = 1.0;
		glTexImage2D(GL_TEXTURE_2D, 0, format, Width, Height, 0,
			     GL_DEPTH_COMPONENT, GL_FLOAT, buf);
		delete[] buf;

		glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_TEXTURE_MODE, GL_LUMINANCE);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_R_TO_TEXTURE);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL);
	}
	else
	{
		u32* buf=new u32[size];
		for (uint i=0;i<size;i++) buf[i]=0x00ffffff;
		glTexImage2D(GL_TEXTURE_2D,0,GL_LUMINANCE8,Width,Height,0,GL_RGBA,GL_UNSIGNED_BYTE,buf);
		delete[] buf;
	}

	// set texture parameters
	glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER, GL_LINEAR);

	// bind to framebuffer object
	if (Framebuffer)
	{
		debug_assert(UseDepthTexture);

		glBindTexture(GL_TEXTURE_2D, 0);
		pglBindFramebufferEXT(GL_FRAMEBUFFER_EXT, Framebuffer);

		pglFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT,
				GL_TEXTURE_2D, Texture, 0);

		glDrawBuffer(GL_NONE);
		glReadBuffer(GL_NONE);

		GLenum status = pglCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT);

		pglBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);

		if (status != GL_FRAMEBUFFER_COMPLETE_EXT)
		{
			LOG(WARNING, LOG_CATEGORY, "Framebuffer object incomplete: %04f", status);

			pglDeleteFramebuffersEXT(1, &Framebuffer);
			Framebuffer = 0;
			EffectiveWidth = std::min(Width, (u32)g_Renderer.GetWidth());
			EffectiveHeight = std::min(Height, (u32)g_Renderer.GetHeight());
		}
	}
}


///////////////////////////////////////////////////////////////////////////////////////////////////
// Set up to render into shadow map texture
void ShadowMap::BeginRender()
{
	// HACK HACK: this depends in non-obvious ways on the behaviour of the caller

	// Calc remaining shadow matrices
	m->CalcShadowMatrices();

	if (m->Framebuffer)
	{
		glBindTexture(GL_TEXTURE_2D, 0);
		pglBindFramebufferEXT(GL_FRAMEBUFFER_EXT, m->Framebuffer);
	}

	// clear buffers
	if (m->UseDepthTexture)
	{
		glClear(GL_DEPTH_BUFFER_BIT);
		glColorMask(0,0,0,0);
	}
	else
	{
		glClearColor(1,1,1,0);
		glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	}

	// setup viewport
	glViewport(0, 0, m->EffectiveWidth, m->EffectiveHeight);

	glMatrixMode(GL_PROJECTION);
	glPushMatrix();
	glLoadMatrixf(&m->LightProjection._11);

	glMatrixMode(GL_MODELVIEW);
	glPushMatrix();
	glLoadMatrixf(&m->LightTransform._11);

	glEnable(GL_SCISSOR_TEST);
	glScissor(1,1, m->EffectiveWidth-2, m->EffectiveHeight-2);
}


///////////////////////////////////////////////////////////////////////////////////////////////////
// Finish rendering into shadow map texture
void ShadowMap::EndRender()
{
	glDisable(GL_SCISSOR_TEST);

	// copy result into shadow map texture
	if (m->Framebuffer)
	{
		pglBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);
	}
	else
	{
		if (!g_Renderer.GetDisableCopyShadow())
		{
			g_Renderer.BindTexture(0, m->Texture);
			glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, g_Renderer.GetWidth(), g_Renderer.GetHeight());
		}
	}

	glViewport(0, 0, g_Renderer.GetWidth(), g_Renderer.GetHeight());

	if (m->UseDepthTexture)
	{
		glColorMask(1,1,1,1);
	}

	// restore matrix stack
	glPopMatrix();
	glMatrixMode(GL_PROJECTION);
	glPopMatrix();
	glMatrixMode(GL_MODELVIEW);
}


///////////////////////////////////////////////////////////////////////////////////////////////////
// Retrieve the texture handle and texture matrix for shadowing
GLuint ShadowMap::GetTexture() const
{
	return m->Texture;
}

const CMatrix3D& ShadowMap::GetTextureMatrix() const
{
	return m->TextureMatrix;
}


///////////////////////////////////////////////////////////////////////////////////////////////////
// Using depth textures vs. a simple luminance map
bool ShadowMap::GetUseDepthTexture() const
{
	return m->UseDepthTexture;
}

void ShadowMap::SetUseDepthTexture(bool depthTexture)
{
	if (depthTexture)
	{
		if (!g_Renderer.GetCapabilities().m_DepthTextureShadows)
		{
			LOG(WARNING, LOG_CATEGORY, "Depth textures are not supported by your graphics card/driver. Fallback to luminance map (no self-shadowing)!");
			depthTexture = false;
		}
	}

	if (depthTexture != m->UseDepthTexture)
	{
		if (m->Texture)
		{
			glDeleteTextures(1, &m->Texture);
			m->Texture = 0;
		}
		m->Width = m->Height = 0;

		m->UseDepthTexture = depthTexture;
	}
}


///////////////////////////////////////////////////////////////////////////////////////////////////
// Depth texture bits
int ShadowMap::GetDepthTextureBits() const
{
	return m->DepthTextureBits;
}

void ShadowMap::SetDepthTextureBits(int bits)
{
	if (bits != m->DepthTextureBits)
	{
		if (m->Texture)
		{
			glDeleteTextures(1, &m->Texture);
			m->Texture = 0;
		}
		m->Width = m->Height = 0;

		m->DepthTextureBits = bits;
	}
}


//////////////////////////////////////////////////////////////////////////////
// RenderDebugDisplay: debug visualizations
//  - blue: objects in shadow
void ShadowMap::RenderDebugDisplay()
{
	glDepthMask(0);
	glDisable(GL_CULL_FACE);

	// Render shadow bound
	glMatrixMode(GL_MODELVIEW);
	glPushMatrix();
	glMultMatrixf(&m->InvLightTransform._11);

	glEnable(GL_BLEND);
	glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
	glColor4ub(0,0,255,64);
	m->ShadowBound.Render();
	glDisable(GL_BLEND);

	glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
	glColor3ub(0,0,255);
	m->ShadowBound.Render();

	glBegin(GL_LINES);
		glVertex3f(0.0, 0.0, 0.0);
		glVertex3f(0.0, 0.0, 50.0);
	glEnd();
	glBegin(GL_POLYGON);
		glVertex3f(0.0, 0.0, 50.0);
		glVertex3f(50.0, 0.0, 50.0);
		glVertex3f(0.0, 50.0, 50.0);
	glEnd();
	glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
	glPopMatrix();

#if 0
	CMatrix3D InvTexTransform;

	m->TextureMatrix.GetInverse(InvTexTransform);

	// Render representative texture rectangle
	glPushMatrix();
	glMultMatrixf(&InvTexTransform._11);

	glEnable(GL_BLEND);
	glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
	glColor4ub(255,0,0,64);
	glBegin(GL_QUADS);
		glVertex3f(0.0, 0.0, 0.0);
		glVertex3f(1.0, 0.0, 0.0);
		glVertex3f(1.0, 1.0, 0.0);
		glVertex3f(0.0, 1.0, 0.0);
	glEnd();
	glDisable(GL_BLEND);

	glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
	glColor3ub(255,0,0);
	glBegin(GL_QUADS);
		glVertex3f(0.0, 0.0, 0.0);
		glVertex3f(1.0, 0.0, 0.0);
		glVertex3f(1.0, 1.0, 0.0);
		glVertex3f(0.0, 1.0, 0.0);
	glEnd();
	glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
	glPopMatrix();
#endif

	// Render the shadow map
	glMatrixMode(GL_PROJECTION);
	glPushMatrix();
	glLoadIdentity();
	glOrtho(0.0, 1.0, 1.0, 0.0, -1.0, 1.0);
	glMatrixMode(GL_MODELVIEW);
	glPushMatrix();
	glLoadIdentity();

	glDisable(GL_DEPTH_TEST);
	g_Renderer.BindTexture(0, m->Texture);
	if (m->UseDepthTexture)
	{
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_NONE);
	}
	glColor3f(1.0f, 1.0f, 1.0f);
	glBegin(GL_QUADS);
		glTexCoord2f(0.0f, 0.0f); glVertex2f(0.0f, 0.0f);
		glTexCoord2f(1.0f, 0.0f); glVertex2f(0.2f, 0.0f);
		glTexCoord2f(1.0f, 1.0f); glVertex2f(0.2f, 0.2f);
		glTexCoord2f(0.0f, 1.0f); glVertex2f(0.0f, 0.2f);
	glEnd();
	if (m->UseDepthTexture)
	{
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_R_TO_TEXTURE);
	}

	glEnable(GL_CULL_FACE);

	glMatrixMode(GL_PROJECTION);
	glPopMatrix();
	glMatrixMode(GL_MODELVIEW);
	glPopMatrix();

	glEnable(GL_DEPTH_TEST);
	glDepthMask(1);
}
