//
// Asura_GX_Glow.cpp
//
// Created by KOR - 15 July 2008
//
// Copyright (C) 2008 Rebellion, All Rights Reserved.
//

#include "Core/Asura.h"

// This file's header.
#include "Asura_GX_Glow.h"

// Asura
#include "Rendering/Asura_Render.h"

// Wii
#include "Rendering/Asura_GX.h"

// Library headers
#include <revolution/gx.h>

u_char*		Asura_GX_Glow::s_pucTopLeftBackupBuffer = NULL;
u_char*		Asura_GX_Glow::s_pucWorkBuffer = NULL;
GXTexObj	Asura_GX_Glow::s_xTopLeftBackupTex;
GXTexObj	Asura_GX_Glow::s_xWorkTex;

const u_int auGLOW_SAMPLES[] = { 1, 3, 5, 7, 5, 3, 1 };
const float afGLOW_OFFSETS[] = { -5.5f, -3.5f, -1.5f, 0.f, 1.5f, 3.5f, 5.5f };

DEBUGVAR float dbg_fGlowBrightness = 1.75f;

void Asura_GX_Glow::Initialise()
{
	if ( s_pucWorkBuffer == NULL )
	{
		// Allocate buffers
		const int iWidth = Asura_GX::GetScreenWidth() / 2;
		const int iHeight = Asura_GX::GetScreenHeight() / 2; 
		// could use OSRoundUp32B instead?
		const int iImageHeight = iHeight + ((iHeight & 31) ? 32 - (iHeight & 31) : 0);
		// could put these in MEM2?
		s_pucTopLeftBackupBuffer = static_cast<u_char*>(Asura_AllocateAlign(iWidth * iImageHeight * 4,ASURA_ALIGN_32_BYTE));
		s_pucWorkBuffer = static_cast<u_char*>(Asura_AllocateAlign(iWidth * iImageHeight * sizeof(u_char),ASURA_ALIGN_32_BYTE));
		Asura_Assert( s_pucTopLeftBackupBuffer,		"Failed to allocate backup buffer");
		Asura_Assert( s_pucWorkBuffer,"Failed to allocate work buffer");
		GXInitTexObj( &s_xTopLeftBackupTex,	s_pucTopLeftBackupBuffer, iWidth, iHeight, GX_TF_RGBA8, GX_CLAMP, GX_CLAMP, GX_DISABLE );
		GXInitTexObj( &s_xWorkTex,s_pucWorkBuffer, iWidth, iHeight, GX_TF_I8, GX_CLAMP, GX_CLAMP, GX_DISABLE );
		GXInitTexObjLOD( &s_xTopLeftBackupTex, GX_NEAR, GX_NEAR, 0.0f, 0.0f, 0.0f, GX_DISABLE, GX_DISABLE, GX_ANISO_1 );
		GXInitTexObjLOD( &s_xWorkTex,GX_NEAR, GX_LINEAR, 0.0f, 0.0f, 0.0f, GX_DISABLE, GX_DISABLE, GX_ANISO_1 );
	} 
	else
	{
		Asura_Assert( false, "Asura_GX_Glow already initialised" );
	}
}

void Asura_GX_Glow::Shutdown()
{
	// Release buffers
	delete [] s_pucTopLeftBackupBuffer;
	s_pucTopLeftBackupBuffer = NULL;
	delete [] s_pucWorkBuffer;
	s_pucWorkBuffer = NULL;
}

void Asura_GX_Glow::CopyToWorkBuffer(const int iWidth, const int iHeight)
{
	GXSetTexCopySrc( 0, 0, iWidth, iHeight );
	GXSetTexCopyDst(iWidth, iHeight, GX_CTF_R8, GX_DISABLE );
	GXCopyTex( s_pucWorkBuffer, GX_DISABLE );

	// Be sure texture is completely copied out
	GXPixModeSync();
	// flush texture cache
	GXInvalidateTexAll();
}

void Asura_GX_Glow::RenderQuad(const float fWidth, const float fHeight)
{
	// Vertices
	GXBegin(GX_QUADS, GX_VTXFMT2, 4);
		GXPosition2f32(0.0F, 0.0F);
		GXTexCoord2f32(0.0F, 0.0F);
		GXPosition2f32(fWidth, 0.0F);
		GXTexCoord2f32(1.0F, 0.0F);
		GXPosition2f32(fWidth, fHeight);
		GXTexCoord2f32(1.0F, 1.0F);
		GXPosition2f32(0.0F, fHeight);
		GXTexCoord2f32(0.0F, 1.0F);
	GXEnd();
}

DEBUGVAR bool dbg_bRestoreBackup = true;
DEBUGVAR bool dbg_bRenderGlow = true;
DEBUGVAR bool dbg_bSquareValues = true;
DEBUGVAR bool dbg_bHBlur = true;
DEBUGVAR bool dbg_bVBlur = true;
DEBUGVAR bool dbg_bTestHBlur = false;
void Asura_GX_Glow::Render()
{
	const u_int uNumGlowSamples = sizeof(auGLOW_SAMPLES) / sizeof(u_int);
	const u_int uMiddleSample = (uNumGlowSamples - 1) / 2;
	u_int uGlowSamplesTotal = 0;
	for(u_int u = 0; u < uNumGlowSamples; ++u)
	{
		uGlowSamplesTotal += auGLOW_SAMPLES[u];
	}

	const int iScreenWidth = Asura_GX::GetScreenWidth();
	const int iScreenHeight = Asura_GX::GetScreenHeight();
	const float fScreenWidth = static_cast<float>(iScreenWidth);
	const float fScreenHeight = static_cast<float>(iScreenHeight);

	const int iHalfWidth = iScreenWidth / 2;
	const int iHalfHeight = iScreenHeight / 2;

	const float fHalfWidth = static_cast<float>(iHalfWidth);
	const float fHalfHeight = static_cast<float>(iHalfHeight);

	Asura_Render::AllowFullScreenRendering();

	// Copy Z buffer to texture
	const GXRenderModeObj& xRenderMode = Asura_GX::GetRenderModeObject();

	// Disable the filter as it's doing nasty things to the exact pixel values I'd like.
	GXSetCopyFilter(GX_FALSE, xRenderMode.sample_pattern, GX_FALSE, xRenderMode.vfilter);

	// Copy out top left quarter backup
	GXSetTexCopySrc( 0, 0, iHalfWidth, iHalfHeight );
	GXSetTexCopyDst(iHalfWidth, iHalfHeight, GX_TF_RGBA8, GX_DISABLE );
	GXCopyTex( s_pucTopLeftBackupBuffer, GX_DISABLE );

	// Copy out full screen to I8 texture format
	GXSetTexCopySrc( 0, 0, iScreenWidth, iScreenHeight );
	GXSetTexCopyDst(iHalfWidth, iHalfHeight, GX_TF_I8, GX_ENABLE );
	GXCopyTex( s_pucWorkBuffer, GX_DISABLE );

	// Be sure texture is completely copied out
	GXPixModeSync();

	// flush texture cache
	GXInvalidateTexAll();

	GXSetNumChans(0);

	GXSetNumTexGens(1);
	GXSetTexCoordGen(GX_TEXCOORD0, GX_TG_MTX2x4, GX_TG_TEX0, GX_IDENTITY);

	GXSetNumTevStages(1);
	GXSetTevOrder( GX_TEVSTAGE0, GX_TEXCOORD0, GX_TEXMAP0, GX_COLOR_NULL );
	GXSetTevOp(GX_TEVSTAGE0, GX_REPLACE);

	GXSetTevSwapMode(GX_TEVSTAGE0, GX_TEV_SWAP0, GX_TEV_SWAP0);

	Asura_GX::Set3DProjection(false);
	Asura_Render::CurrentStates.RequestAlphaTest(false, 0);
	Asura_Render::CurrentStates.RequestCullMode(ASURA_CULLMODE_NONE);
	Asura_Render::CurrentStates.RequestTranslucencyMode(ASURA_TRANSLUCENCY_OFF);
	Asura_Render::CurrentStates.RequestZBufferEnabled(false);
	Asura_Render::CurrentStates.RequestZBufferWriteEnabled(false);

#ifdef ASURA_USE_SCREENSHOT_SYSTEM
	// Make sure we are working in exact screenspace.
	GXSetViewport(0.f, 0.f, fScreenWidth, fScreenHeight, 0.f, 1.f);
#endif

	// vertex descriptor settings
	GXClearVtxDesc( );
	GXSetVtxDesc( GX_VA_POS, GX_DIRECT );
	GXSetVtxDesc( GX_VA_TEX0, GX_DIRECT );

	GXLoadTexObj( &s_xWorkTex, GX_TEXMAP0 );

	if(dbg_bSquareValues)
	{
		// Actually, this will cube them.
		GXSetTevColorIn(GX_TEVSTAGE0, GX_CC_ZERO, GX_CC_TEXA, GX_CC_TEXA, GX_CC_ZERO);
		GXSetTevColorOp(GX_TEVSTAGE0, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, GX_ENABLE, GX_TEVPREV );
		GXSetTevAlphaIn(GX_TEVSTAGE0, GX_CA_ZERO, GX_CA_ZERO, GX_CA_ZERO, GX_CA_TEXA);
		GXSetTevAlphaOp(GX_TEVSTAGE0, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, GX_ENABLE, GX_TEVPREV );

		GXSetBlendMode(GX_BM_BLEND, GX_BL_SRCALPHA, GX_BL_ZERO, GX_LO_NOOP);

		GXSetTexCoordGen(GX_TEXCOORD0, GX_TG_MTX2x4, GX_TG_TEX0, GX_IDENTITY);

		RenderQuad(fHalfWidth, fHalfHeight);

		// Copy squared back into work buffer
		CopyToWorkBuffer(iHalfWidth, iHalfHeight);
	}
	else
	{
		// Just render straight out.
		GXSetTevColorIn(GX_TEVSTAGE0, GX_CC_ZERO, GX_CC_ZERO, GX_CC_ZERO, GX_CC_TEXA);
		GXSetTevColorOp(GX_TEVSTAGE0, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, GX_ENABLE, GX_TEVPREV );
		GXSetTevAlphaIn(GX_TEVSTAGE0, GX_CA_ZERO, GX_CA_ZERO, GX_CA_ZERO, GX_CA_ZERO);
		GXSetTevAlphaOp(GX_TEVSTAGE0, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, GX_ENABLE, GX_TEVPREV );

		GXSetBlendMode(GX_BM_BLEND, GX_BL_ONE, GX_BL_ZERO, GX_LO_NOOP);

		GXSetTexCoordGen(GX_TEXCOORD0, GX_TG_MTX2x4, GX_TG_TEX0, GX_IDENTITY);

		RenderQuad(fHalfWidth, fHalfHeight);
	}


	// Horizontal blur into top left
	if(dbg_bHBlur)
	{
		GXSetTexCoordGen(GX_TEXCOORD0, GX_TG_MTX2x4, GX_TG_TEX0, GX_TEXMTX0);

		GXSetTevKColorSel(GX_TEVSTAGE0, GX_TEV_KCSEL_K0);
		GXSetTevKAlphaSel(GX_TEVSTAGE0, GX_TEV_KASEL_K0_A);

		GXSetTevColorIn(GX_TEVSTAGE0, GX_CC_ZERO, GX_CC_TEXA, GX_CC_KONST, GX_CC_ZERO);
		GXSetTevColorOp(GX_TEVSTAGE0, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, GX_ENABLE, GX_TEVPREV );
		GXSetTevAlphaIn(GX_TEVSTAGE0, GX_CA_ZERO, GX_CA_ZERO, GX_CA_ZERO, GX_CA_KONST);
		GXSetTevAlphaOp(GX_TEVSTAGE0, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, GX_ENABLE, GX_TEVPREV );
		GXSetBlendMode(GX_BM_BLEND, GX_BL_ONE, GX_BL_SRCALPHA, GX_LO_NOOP);

		for(u_int u = 0; u < uNumGlowSamples; ++u)
		{
			// Middle sample already exists in the framebuffer, so we simply blend it with the first sample.
			if(u == uMiddleSample)
			{
				continue;
			}
			Mtx xTexMtx;
			MTXTrans(xTexMtx, afGLOW_OFFSETS[u] / fHalfWidth, 0.f, 0.f);
			GXLoadTexMtxImm(xTexMtx, GX_TEXMTX0, GX_MTX2x4);

			const float fAlpha = dbg_fGlowBrightness * static_cast<float>(auGLOW_SAMPLES[u]) / static_cast<float>(uGlowSamplesTotal);
			GXColor xKonst;
			xKonst.r = xKonst.g = xKonst.b = static_cast<u_char>(fAlpha * 0xff);
			if(u)
			{
				xKonst.a = 0xff;
			}
			else
			{
				// See comment at top of loop
				const float fDestAlpha = static_cast<float>(auGLOW_SAMPLES[uMiddleSample]) / static_cast<float>(uGlowSamplesTotal);
				xKonst.a = static_cast<u_char>(fDestAlpha * 0xff);
			}
			GXSetTevKColor(GX_KCOLOR0, xKonst);
			RenderQuad(fHalfWidth, fHalfHeight);
		}

		// Copy horizontally blurred back into work buffer
		CopyToWorkBuffer(iHalfWidth, iHalfHeight);

		if(dbg_bTestHBlur)
		{
			GXSetTexCoordGen(GX_TEXCOORD0, GX_TG_MTX2x4, GX_TG_TEX0, GX_IDENTITY);
			GXSetTevOp(GX_TEVSTAGE0, GX_REPLACE);

			Asura_Render::CurrentStates.RequestTranslucencyMode(ASURA_TRANSLUCENCY_OFF);
			RenderQuad(fHalfWidth, fHalfHeight);
		}
	}

	if(dbg_bVBlur)
	{
		GXSetTexCoordGen(GX_TEXCOORD0, GX_TG_MTX2x4, GX_TG_TEX0, GX_TEXMTX0);

		GXSetTevKColorSel(GX_TEVSTAGE0, GX_TEV_KCSEL_K0);
		GXSetTevKAlphaSel(GX_TEVSTAGE0, GX_TEV_KASEL_K0_A);

		GXSetTevColorIn(GX_TEVSTAGE0, GX_CC_ZERO, GX_CC_TEXA, GX_CC_KONST, GX_CC_ZERO);
		GXSetTevColorOp(GX_TEVSTAGE0, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, GX_ENABLE, GX_TEVPREV );
		GXSetTevAlphaIn(GX_TEVSTAGE0, GX_CA_ZERO, GX_CA_ZERO, GX_CA_ZERO, GX_CA_KONST);
		GXSetTevAlphaOp(GX_TEVSTAGE0, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, GX_ENABLE, GX_TEVPREV );
		GXSetBlendMode(GX_BM_BLEND, GX_BL_ONE, GX_BL_SRCALPHA, GX_LO_NOOP);

		for(u_int u = 0; u < uNumGlowSamples; ++u)
		{
			// Middle sample already exists in the framebuffer, so we simply blend it with the first sample.
			if(u == uMiddleSample)
			{
				continue;
			}
			Mtx xTexMtx;
			MTXTrans(xTexMtx, 0.f, afGLOW_OFFSETS[u] / fHalfHeight, 0.f);
			GXLoadTexMtxImm(xTexMtx, GX_TEXMTX0, GX_MTX2x4);

			const float fAlpha = static_cast<float>(auGLOW_SAMPLES[u]) / static_cast<float>(uGlowSamplesTotal);
			GXColor xKonst;
			xKonst.r = xKonst.g = xKonst.b = static_cast<u_char>(fAlpha * 0xff);
			if(u)
			{
				xKonst.a = 0xff;
			}
			else
			{
				// See comment at top of loop
				const float fDestAlpha = static_cast<float>(auGLOW_SAMPLES[uMiddleSample]) / static_cast<float>(uGlowSamplesTotal);
				xKonst.a = static_cast<u_char>(fDestAlpha * 0xff);
			}
			GXSetTevKColor(GX_KCOLOR0, xKonst);
			RenderQuad(fHalfWidth, fHalfHeight);
		}

		// Copy blurred back into work buffer
		CopyToWorkBuffer(iHalfWidth, iHalfHeight);
	}

	if(dbg_bRestoreBackup)
	{
		GXSetTexCoordGen(GX_TEXCOORD0, GX_TG_MTX2x4, GX_TG_TEX0, GX_IDENTITY);
		GXSetTevOp(GX_TEVSTAGE0, GX_REPLACE);

		GXSetBlendMode(GX_BM_BLEND, GX_BL_ONE, GX_BL_ZERO, GX_LO_NOOP);
		// Put top left quarter backup back in
		GXLoadTexObj( &s_xTopLeftBackupTex, GX_TEXMAP0 );

		RenderQuad(fHalfWidth, fHalfHeight);
	}

	if(dbg_bRenderGlow)
	{
		GXSetTexCoordGen(GX_TEXCOORD0, GX_TG_MTX2x4, GX_TG_TEX0, GX_IDENTITY);
		GXSetTevOp(GX_TEVSTAGE0, GX_REPLACE);
		// Now add the glow on top.
		GXSetBlendMode(GX_BM_BLEND, GX_BL_INVDSTCLR, GX_BL_ONE, GX_LO_NOOP);

		GXLoadTexObj( &s_xWorkTex, GX_TEXMAP0 );
		RenderQuad(fScreenWidth, fScreenHeight);

		Asura_Render::CurrentStates.RequestTranslucencyMode(ASURA_TRANSLUCENCY_NORMAL);
		Asura_Render::CurrentStates.RequestTranslucencyMode(ASURA_TRANSLUCENCY_OFF);
	}
	// Re-enable filter
	GXSetCopyFilter(GX_FALSE, xRenderMode.sample_pattern, GX_TRUE, xRenderMode.vfilter);

#ifdef ASURA_USE_SCREENSHOT_SYSTEM
	// Set the viewport correctly again
	Asura_GX::Set3DProjection(false, true);
#endif
}