#include "Scene.h"
#include "OpenGLUtils.h"
#include "Camera.h"
#include "ObjectRenderer.h"
#include "ShadowRenderer.h"
#include "BillboardRenderer.h"
#include "Billboard.h"
#include "VisibleObject.h"

#include <Utility/Log.h>

//////////////////////////////////////////////////////////////
namespace Dubious
{
namespace Renderer
{

typedef std::shared_ptr<ObjectRenderer> ObjectRendererPtr;

//////////////////////////////////////////////////////////////
Scene::Scene()
{
}

//////////////////////////////////////////////////////////////
Scene::~Scene()
{
}

//////////////////////////////////////////////////////////////
void Scene::Render( CameraPtr pCamera )
{
	// The tablet don't clear the depth bit unless it's first
	// enabled for writing with glDepthMark
	glDepthMask( true );
	glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT );

	pCamera->ReadyForPhoto();

	glDisable( GL_TEXTURE_2D );
	glDisable( GL_BLEND );
	// setup general state for drawing the scene
	glEnable( GL_CULL_FACE );
	glEnable( GL_DEPTH_TEST );
	glCullFace( GL_BACK );
	glDepthFunc( GL_LESS );
	SetupLight( true );

	// step 1 is draw the scene in shadow
	auto iter = m_Objects.cbegin();
	auto end = m_Objects.cend();
	ObjectRendererPtr pRenderer;
	for( ; iter!=end; ++iter ){
		pRenderer = (*iter)->ObjectRenderer();
		if( pRenderer ) {
			pRenderer->RenderObject( *iter );
		}
	}

	// step 2 is draw the shadow volume
	glEnable( GL_STENCIL_TEST );
	glStencilFunc( GL_ALWAYS, 0, 0xffffffff );
	glColorMask( false, false, false, false );
	glDepthMask( false );
	iter = m_Objects.begin();
	for( ; iter!=end; ++iter ){
		ShadowRendererPtr pShadowRenderer = (*iter)->ShadowRenderer();
		if (pShadowRenderer)
			pShadowRenderer->RenderShadowVolume( *iter, (*iter)->CoordinateSpace().Transform( m_SceneLight.Position ) );
	}
	glColorMask( true, true, true, true );
	glDepthFunc( GL_EQUAL );
	glStencilFunc( GL_EQUAL, 0, 0xffffffff );
	glStencilOp( GL_KEEP, GL_KEEP, GL_KEEP );

	// step 3. Draw the scene again in light
	SetupLight( false );
	iter = m_Objects.begin();
	for( ; iter!=end; ++iter ){
		pRenderer = (*iter)->ObjectRenderer();
		if( pRenderer )
			pRenderer->RenderObject( *iter );
	}

	// step 4. Disable depth and stencils and draw
	// the billboards.  For now, Billboards=Explosions
	glDepthMask( false );
	glDisable( GL_LIGHTING );
	glEnable( GL_TEXTURE_2D );
	glDisable( GL_STENCIL_TEST );

	// The general strategy for blending billboards is to sort them
	// by greatest distance to the Eye and then draw them (furthest
	// first).  I'm attempting to do that here, but I am making a
	// bunch of assumptions (not reusable) about the camera (ie
	// that it's in third person mode).
	// You'll also note I'm sorting using >.  This is because I
	// want the furthest objects to be at the start of the list.
	// I'm also using a lambda, which is awesome.
	Math::Vector X, Y, Z;
	pCamera->CoordinateSpace().Axes( X, Y, Z );
	Z.SetLength( pCamera->ZAxisOffset() );
	m_Billboards.sort( [Z]( BillboardPtr pA, BillboardPtr pB ){
		return (Z - pA->Layers()[0].CoordinateSpace.Position()).GetLength2() >
			   (Z - pB->Layers()[0].CoordinateSpace.Position()).GetLength2();
	});
	auto BillboardIter = m_Billboards.cbegin();
	auto BillboardEnd = m_Billboards.cend();
	for (; BillboardIter != BillboardEnd; ++BillboardIter) {
		if (m_pBillboardRenderer)
			m_pBillboardRenderer->RenderBillboard( pCamera, *BillboardIter );
	}
}

//////////////////////////////////////////////////////////////
void Scene::AddObject( VisibleObjectPtr pObject )
{
	m_Objects.push_back( pObject );
}

//////////////////////////////////////////////////////////////
void Scene::AddBillboard( BillboardPtr pBillboard )
{
	m_Billboards.push_back( pBillboard );
}

//////////////////////////////////////////////////////////////
void Scene::RemoveObject( VisibleObjectPtr pObject )
{
	m_Objects.remove( pObject );
}

//////////////////////////////////////////////////////////////
void Scene::RemoveBillboard( BillboardPtr pBillboard )
{
	m_Billboards.remove( pBillboard );
}

//////////////////////////////////////////////////////////////
void Scene::RemoveAllObjects()
{
	m_Objects.clear();
}

//////////////////////////////////////////////////////////////
void Scene::RemoveAllBillboards()
{
	m_Billboards.clear();
}

//////////////////////////////////////////////////////////////
void Scene::SetupLight( bool InShadow ) const
{
	glEnable( GL_LIGHTING );
	glEnable( GL_LIGHT0 );

	OpenGLUtils::LightPosition(GL_LIGHT0, m_SceneLight.Position );
	if( InShadow ){
		OpenGLUtils::LightColor( GL_LIGHT0, GL_AMBIENT, m_SceneLight.Ambient );
		OpenGLUtils::LightColor( GL_LIGHT0, GL_DIFFUSE, Color::BLACK );
		OpenGLUtils::LightColor( GL_LIGHT0, GL_SPECULAR, Color::BLACK );
	} else {
		OpenGLUtils::LightColor( GL_LIGHT0, GL_AMBIENT, m_SceneLight.Ambient );
		OpenGLUtils::LightColor( GL_LIGHT0, GL_DIFFUSE, m_SceneLight.Diffuse );
		OpenGLUtils::LightColor( GL_LIGHT0, GL_SPECULAR, m_SceneLight.Specular );
	}
}

}
}
