/* -----------------------------------------------------------------------------
	GSFramework
	Copyright 2001-2013 Emmanuel Julien. All Rights Reserved.
----------------------------------------------------------------------------- */


		#include "gpu/gpu_renderer.h"
		#include "gpu/gpu_shader_object.h"
		#include "gpu/gpu_geometry.h"
		#include "gpu/gpu_draw_context.h"
		#include "core/renderer_environment_interface.h"
		#include "core/geometry.h"
		#include "core/terrain.h"
		#include "core/camera.h"
		#include "core/light.h"
		#include "async/job.h"
		#include "alloc/ialloc.h"
		#include "platform.h"

		using namespace GS;
		using namespace GS::GPU;


//------------------------------------------------------------------------------
void				Renderer::LightCullDisplayList(const Core::Light &light, const Stack <RenderPrimitive *> &in, Stack <RenderPrimitive *> &out)
{
	for (uint n = 0; n < in.GetCount(); ++n)
	{
		RenderPrimitive *dl = in[n];

	 	switch (light.model)
	 	{
			case Core::Light::Model_Point:
				if	(light.range > 0)
				{
					MinMax &mm = dl->dlst->minmax;
					Vector4 dl_lpos = light.GetMatrix().GetRow(3) * dl->item->GetInverseMatrix();

					if	(	((dl_lpos.x - light.range) < mm.mx.x) &&
							((dl_lpos.x + light.range) > mm.mn.x) &&
							((dl_lpos.y - light.range) < mm.mx.y) &&
							((dl_lpos.y + light.range) > mm.mn.y) &&
							((dl_lpos.z - light.range) < mm.mx.z) &&
							((dl_lpos.z + light.range) > mm.mn.z)		)
						out.Push(dl);
				}
				else
					out.Push(dl);
				break;

			case Core::Light::Model_Spot:
				if	(light.frustum.ClassifyMinMax(dl->dlst->minmax, &dl->item->GetMatrix()) != Frustum::Outside)
					out.Push(dl);
				break;

			case Core::Light::Model_Linear:
	 		default:
				out.Push(dl);
				break;
	 	}
	}
}
//------------------------------------------------------------------------------

//------------------------------------------------------------------------------
void				Renderer::SetupForwardContext(const DrawContext::Context &ctx)
{
	switch (ctx.draw)
	{
		case DrawContext::Base:
			switch (ctx.render)
			{
				case DrawContext::Opaque:
					EnableDepthTest(true);
					break;

				case DrawContext::Alpha:
					EnableDepthWrite(false);
					EnableBlending(true);
					SetBlendFunc(BlendSrcAlpha, BlendOneMinusSrcAlpha);
					break;
			}
			break;

		case DrawContext::Light:
			switch (ctx.render)
			{
				case DrawContext::Opaque:
					EnableBlending(true);
					SetBlendFunc(BlendSrcAlpha, BlendOne);
					break;

				case DrawContext::Alpha:
					EnableDepthWrite(false);
					EnableBlending(true);
					SetBlendFunc(BlendSrcAlpha, BlendOne);
					break;
			}
			break;
	}
}
//------------------------------------------------------------------------------

//------------------------------------------------------------------------------
class	GPU::PrepareLightJob : public ASync::Job
{
		const Renderer &renderer;
		const Core::Light &light;

		const Stack <RenderPrimitive *> &in;
		Stack <RenderPrimitive *> &out;

		DrawContext::Render rc;

public:

		NPLACEMENT_NEW(RendererJob)

		bool		use_shadow;

		bool		CullPrimitive(const Render::Material *m, const MinMax &mm, const Core::Item *item) const
		{
			if	(m->renderword & Core::Material::Render_Unlit)
				return false;	// exclude from lighting

			switch (light.model)
			{
				case Core::Light::Model_Point:
					if	(light.range > 0)
					{
						Vector4 dl_lpos = light.GetMatrix().GetRow(3) * item->GetInverseMatrix();

						if	(	((dl_lpos.x - light.range) < mm.mx.x) &&
								((dl_lpos.y - light.range) < mm.mx.y) &&
								((dl_lpos.z - light.range) < mm.mx.z) &&
								((dl_lpos.x + light.range) > mm.mn.x) &&
								((dl_lpos.y + light.range) > mm.mn.y) &&
								((dl_lpos.z + light.range) > mm.mn.z)		)
							return true;
					}
					else
						return true;
					break;

				case Core::Light::Model_Spot:
					if	(light.frustum.ClassifyMinMax(mm, &item->GetMatrix()) != Frustum::Outside)
						return true;
					break;

				default:
				case Core::Light::Model_Linear:
					return true;
			}
			return false;
		}
		void		Execute(uint)
		{
			for (uint n = 0; n < in.GetCount(); ++n)
			{
				RenderPrimitive *dl = in[n];

				bool r = false;
				switch (dl->type)
				{
					case RenderPrimitive::TypeDisplayList:
						r = CullPrimitive(dl->dlst->material, dl->dlst->minmax, dl->item);
						break;
					case RenderPrimitive::TypeTerrainPatch:
						r = CullPrimitive(dl->patch->terrain->render_data->material, dl->patch->minmax, dl->item);
						break;

					default:
					case RenderPrimitive::TypeEmitter:
						break;
				}
				if	(r)
					out.Push(dl);
			}

			// Prepare shadow map if required.
			if	(out.GetCount() == 0)
				use_shadow = false;

			else
			{
				use_shadow = renderer.gpu_config.enable_shadow && (light.shadow == Core::Light::Shadow_Map);
				if	((rc == DrawContext::Alpha) && (light.shadow_cast_all == false))
					use_shadow = false;

				bool use_shadow_matrix = false;
				if	((light.model == Core::Light::Model_Spot) && !light.projection_texture.IsEmpty())
					use_shadow_matrix = true;

				if	(use_shadow || use_shadow_matrix)
					renderer.PrepareShadowMap(light, use_shadow);
			}
		}

		PrepareLightJob(const Renderer &_renderer, const Core::Light &_light, const Stack <RenderPrimitive *> &_in, Stack <RenderPrimitive *> &_out, DrawContext::Render _rc) : Job("Light Draw Setup"), renderer(_renderer), light(_light), in(_in), out(_out), rc(_rc) {}
};
//------------------------------------------------------------------------------

//------------------------------------------------------------------------------
static	MaterialShader::Variant	DispatchLightShaderVariant(Core::Light::Model type, bool shadow, bool projection_map)
{
static MaterialShader::Variant table[2][2][Core::Light::Model_Last] =
	{
		{
			{ MaterialShader::Last, MaterialShader::FS_PointLight, MaterialShader::FS_LinearLight, MaterialShader::FS_SpotLight },
			{ MaterialShader::Last, MaterialShader::FS_PointLightShadowMapping, MaterialShader::FS_LinearLightShadowMapping, MaterialShader::FS_SpotLightShadowMapping }
		},
		{
			{ MaterialShader::Last, MaterialShader::FS_PointLight, MaterialShader::FS_LinearLight, MaterialShader::FS_SpotLightProjection },
			{ MaterialShader::Last, MaterialShader::FS_PointLightShadowMapping, MaterialShader::FS_LinearLightShadowMapping, MaterialShader::FS_SpotLightProjectionShadowMapping }
		}
	};
	return table[projection_map ? 1 : 0][shadow ? 1 : 0][type];
}
void				Renderer::DrawForwardBaseConstant(DrawContext::Render rc, const Stack <RenderPrimitive *> &dl_list)
{
	ScopedPerfEvent event(this, __FUNCTION__, Color::Blue);

	DrawContext	dc(rc, DrawContext::Base, MaterialShader::FS_Constant);
	SetupForwardContext(dc.ctx);
	DrawList(dl_list, dc);
}
void				Renderer::DrawForwardLightContribution(DrawContext::Render rc, const List <Core::Light *> &light_list, const AutoStack <PrepareLightJob *> &setup_jobs, const Array <Stack <RenderPrimitive *> > &dl_list)
{
	ScopedPerfEvent event(this, "Render light contribution", Color::Blue);

	DrawContext	dc(rc, DrawContext::Light, MaterialShader::FS_Constant);
	SetupForwardContext(dc.ctx);

	EnableDepthWrite(false);
	SetDepthFunc(DepthLessEqual);

	uint job_count = 0;
	ListForeachPtr(Core::Light *, l, light_list)
	{
		if	(l->model == Core::Light::Model_None)
			continue;

		// Wait for the light setup job to complete.
		Platform::Get().job_manager->JoinJob(setup_jobs[job_count]);

		if	(dl_list[job_count].GetCount())
		{
			bool use_shadow = setup_jobs[job_count]->use_shadow;

			if	(use_shadow)
			{
				PerfBeginEvent("Render shadow map", Color::Yellow);

				EnableDepthWrite(true);
				RenderShadowMap(*l);
				EnableDepthWrite(false);

				SetCurrentFBO(gpu_config.use_rtt ? render_fbo : NULL);
				SetupForwardContext(dc.ctx);

				PerfEndEvent();
			}

			// Add contribution.
			dc.ctx.variant = DispatchLightShaderVariant(l->model, use_shadow, asbool(l->projection_texture));
			dc.light = l;

			DrawList(dl_list[job_count], dc);
		}

		++stats.light_processed;
		++job_count;
	}
}
void				Renderer::RenderListForward(Stack <RenderPrimitive *> &dl_list, DrawContext::Render rc)
{
	ScopedPerfEvent event(this, __FUNCTION__, Color::Red);

	if	(dl_list.GetCount() == 0)
		return;

	// Setup all lights.
	List <Core::Light *> frustum_light_list;
	environment_interface->GetLightsInFrustum(view_item->GetMatrix().GetRow(3), frustum, frustum_light_list);

	Array <Stack <RenderPrimitive *> > light_dl_list(frustum_light_list.GetCount());
	AutoStack <PrepareLightJob *> light_setup_job(frustum_light_list.GetCount());

	uint job_count = 0;
	ListForeachPtr(Core::Light *, l, frustum_light_list)
	{
		if	(l->model == Core::Light::Model_None)
			continue;

		if	(PrepareLightJob *job = new PrepareLightJob(*this, *l, dl_list, light_dl_list[job_count++], rc))
		{
			light_setup_job.Push(job);
			Platform::Get().job_manager->EnqueueJob(job);
		}
	}

	// Draw all passes.
	DrawForwardBaseConstant(rc, dl_list);
	DrawForwardLightContribution(rc, frustum_light_list, light_setup_job, light_dl_list);

	//
	CollectJobsPerf(light_setup_job, job_count, stats.bench_prepare_light);

	// Restore default state.
	SetCullFunc(CullFront);

	SetDepthFunc(DepthLess);
	EnableDepthWrite(true);

	EnableBlending(false);
	SetBlendFunc(BlendSrcAlpha, BlendOneMinusSrcAlpha);
}
//------------------------------------------------------------------------------
