/*
-----------------------------------------------------------------------------
This source file is part of OGRE
(Object-oriented Graphics Rendering Engine)
For the latest info, see http://www.ogre3d.org/

Copyright (c) 2000-2012 Torus Knot Software Ltd
Also see acknowledgements in Readme.html

You may use this sample code for anything you like, it is not covered by the
same license as the rest of the engine.
-----------------------------------------------------------------------------
*/

#include "DLight.h"

#include "OgreHardwareBufferManager.h"
#include "OgreCamera.h"
#include "OgreSceneNode.h"
#include "OgreLight.h"
#include "GeomUtils.h"
#include "LightMaterialGenerator.h"
#include "OgreTechnique.h"
#include "OgreSceneManager.h"

#define ENABLE_BIT(mask, flag) (mask) |= (flag)
#define DISABLE_BIT(mask, flag) (mask) &= ~(flag)

using namespace Ogre;
//-----------------------------------------------------------------------
DLight::DLight(MaterialGenerator *sys, Ogre::Light* parentLight):
    mParentLight(parentLight), bIgnoreWorld(false), mGenerator(sys), mPermutation(0)
{
	// Set up geometry
	// Allocate render operation
	mRenderOp.operationType = RenderOperation::OT_TRIANGLE_LIST;
	mRenderOp.indexData = 0;
	mRenderOp.vertexData = 0;
	mRenderOp.useIndexes = true;

	updateFromParent();
}
//-----------------------------------------------------------------------
DLight::~DLight()
{
	// need to release IndexData and vertexData created for renderable
    delete mRenderOp.indexData;
    delete mRenderOp.vertexData;
}
//-----------------------------------------------------------------------
void DLight::setAttenuation(float c, float b, float a)
{
	// Set Attenuation parameter to shader
	//setCustomParameter(3, Vector4(c, b, a, 0));
	float outerRadius = mParentLight->getAttenuationRange();
	/// There is attenuation? Set material accordingly
	if(c != 1.0f || b != 0.0f || a != 0.0f)
	{
		ENABLE_BIT(mPermutation, LightMaterialGenerator::MI_ATTENUATED);
		if (mParentLight->getType() == Light::LT_POINT)
		{
			//// Calculate radius from Attenuation
			int threshold_level = 10;// difference of 10-15 levels deemed unnoticeable
			float threshold = 1.0f/((float)threshold_level/256.0f); 

			//// Use quadratic formula to determine outer radius
			c = c-threshold;
			float d=sqrt(b*b-4*a*c);
			outerRadius = (-2*c)/(b+d);
			outerRadius *= 1.2;
		}
	}
	else
	{
		DISABLE_BIT(mPermutation,LightMaterialGenerator::MI_ATTENUATED);
	}
    
	rebuildGeometry(outerRadius);
}
//-----------------------------------------------------------------------
void DLight::setSpecularColour(const ColourValue &col)
{
	//setCustomParameter(2, Vector4(col.r, col.g, col.b, col.a));
	/// There is a specular component? Set material accordingly
	
	if(col.r != 0.0f || col.g != 0.0f || col.b != 0.0f)
		ENABLE_BIT(mPermutation,LightMaterialGenerator::MI_SPECULAR);
	else
		DISABLE_BIT(mPermutation,LightMaterialGenerator::MI_SPECULAR);
		
}
//-----------------------------------------------------------------------
void DLight::rebuildGeometry(float radius)
{
	//Disable all 3 bits
	DISABLE_BIT(mPermutation, LightMaterialGenerator::MI_POINT);
	DISABLE_BIT(mPermutation, LightMaterialGenerator::MI_SPOTLIGHT);
	DISABLE_BIT(mPermutation, LightMaterialGenerator::MI_DIRECTIONAL);

	switch (mParentLight->getType())
	{
	case Light::LT_DIRECTIONAL:
		createRectangle2D();
        ENABLE_BIT(mPermutation,LightMaterialGenerator::MI_DIRECTIONAL);
		break;
	case Light::LT_POINT:
		/// XXX some more intelligent expression for rings and segments
		createSphere(radius, 10, 10);
		ENABLE_BIT(mPermutation,LightMaterialGenerator::MI_POINT);
		break;
	case Light::LT_SPOTLIGHT:
		Real height = mParentLight->getAttenuationRange();
		Radian coneRadiusAngle = mParentLight->getSpotlightOuterAngle() / 2;
        Real rad = Math::Tan(coneRadiusAngle) * height;
		createCone(rad, height, 20);
		ENABLE_BIT(mPermutation,LightMaterialGenerator::MI_SPOTLIGHT);
		break;
	}	
}
//-----------------------------------------------------------------------
void DLight::createRectangle2D()
{
	/// XXX this RenderOp should really be re-used between DLight objects,
	/// not generated every time
	delete mRenderOp.vertexData; 
	delete mRenderOp.indexData; 

	mRenderOp.vertexData = new VertexData();
    mRenderOp.indexData = 0;

	GeomUtils::createQuad(mRenderOp.vertexData);

    mRenderOp.operationType = RenderOperation::OT_TRIANGLE_STRIP; 
    mRenderOp.useIndexes = false; 

	// Set bounding
    setBoundingBox(AxisAlignedBox(-10000,-10000,-10000,10000,10000,10000));
	mRadius = 15000;
	bIgnoreWorld = true;
}
//-----------------------------------------------------------------------
void DLight::createSphere(float radius, int nRings, int nSegments)
{
	delete mRenderOp.vertexData; 
	delete mRenderOp.indexData;
	mRenderOp.operationType = RenderOperation::OT_TRIANGLE_LIST;
	mRenderOp.indexData = new IndexData();
	mRenderOp.vertexData = new VertexData();
	mRenderOp.useIndexes = true;

	GeomUtils::createSphere(mRenderOp.vertexData, mRenderOp.indexData
		, radius
		, nRings, nSegments
		, false // no normals
		, false // no texture coordinates
		);

	// Set bounding box and sphere
	setBoundingBox( AxisAlignedBox( Vector3(-radius, -radius, -radius), Vector3(radius, radius, radius) ) );
	mRadius = radius;
	bIgnoreWorld = false;
}
//-----------------------------------------------------------------------
void DLight::createCone(float radius, float height, int nVerticesInBase)
{
	delete mRenderOp.vertexData;
	delete mRenderOp.indexData;
	mRenderOp.operationType = RenderOperation::OT_TRIANGLE_LIST;
	mRenderOp.indexData = new IndexData();
	mRenderOp.vertexData = new VertexData();
	mRenderOp.useIndexes = true;

	GeomUtils::createCone(mRenderOp.vertexData, mRenderOp.indexData
		, radius
		, height, nVerticesInBase);

	// Set bounding box and sphere
	setBoundingBox( AxisAlignedBox( 
			Vector3(-radius, 0, -radius), 
			Vector3(radius, height, radius) ) );

	mRadius = radius;
	bIgnoreWorld = false;
}
//-----------------------------------------------------------------------
Real DLight::getBoundingRadius(void) const
{
	return mRadius;
}
//-----------------------------------------------------------------------
Real DLight::getSquaredViewDepth(const Camera* cam) const
{
	if(bIgnoreWorld)
	{
		return 0.0f;
	}
	else
	{
		Vector3 dist = cam->getDerivedPosition() - getParentSceneNode()->_getDerivedPosition();
		return dist.squaredLength();
	}
}
//-----------------------------------------------------------------------
const MaterialPtr& DLight::getMaterial(void) const
{
	return mGenerator->getMaterial(mPermutation);
}
//-----------------------------------------------------------------------
void DLight::getWorldTransforms(Matrix4* xform) const
{
	if (mParentLight->getType() == Light::LT_SPOTLIGHT)
	{
		Quaternion quat = Vector3::UNIT_Y.getRotationTo(mParentLight->getDerivedDirection());
		xform->makeTransform(mParentLight->getDerivedPosition(),
			Vector3::UNIT_SCALE, quat);
	}
	else
	{
		xform->makeTransform(mParentLight->getDerivedPosition(),
			Vector3::UNIT_SCALE, Quaternion::IDENTITY);
	}
	
}
//-----------------------------------------------------------------------
void DLight::updateFromParent()
{
	//TODO : Don't do this unless something changed
	setAttenuation(mParentLight->getAttenuationConstant(), 
		mParentLight->getAttenuationLinear(), mParentLight->getAttenuationQuadric());	
	setSpecularColour(mParentLight->getSpecularColour());

	if (getCastChadows())
	{
		ENABLE_BIT(mPermutation,LightMaterialGenerator::MI_SHADOW_CASTER);
	}
	else
	{
		DISABLE_BIT(mPermutation, LightMaterialGenerator::MI_SHADOW_CASTER);
	}
}
//-----------------------------------------------------------------------
bool DLight::isCameraInsideLight(Ogre::Camera* camera)
{
	switch (mParentLight->getType())
	{
	case Ogre::Light::LT_DIRECTIONAL:
		return false;
	case Ogre::Light::LT_POINT:
		{
		Ogre::Real distanceFromLight = camera->getDerivedPosition()
			.distance(mParentLight->getDerivedPosition());
		//Small epsilon fix to account for the fact that we aren't a true sphere.
		return distanceFromLight <= mRadius + camera->getNearClipDistance() + 0.1; 
		}
	case Ogre::Light::LT_SPOTLIGHT:
		{
		Ogre::Vector3 lightPos = mParentLight->getDerivedPosition();
		Ogre::Vector3 lightDir = mParentLight->getDerivedDirection();
		Ogre::Radian attAngle = mParentLight->getSpotlightOuterAngle();
		
		//Extend the analytic cone's radius by the near clip range by moving its tip accordingly.
		//Some trigonometry needed here.
		Ogre::Vector3 clipRangeFix = -lightDir * (camera->getNearClipDistance() / Ogre::Math::Tan(attAngle/2));
		lightPos = lightPos + clipRangeFix;
    
		Ogre::Vector3 lightToCamDir = camera->getDerivedPosition() - lightPos;
		Ogre::Real distanceFromLight = lightToCamDir.normalise();

		Ogre::Real cosAngle = lightToCamDir.dotProduct(lightDir);
		Ogre::Radian angle = Ogre::Math::ACos(cosAngle);
		//Check whether we will see the cone from our current POV.
		return (distanceFromLight <= (mParentLight->getAttenuationRange() + clipRangeFix.length()))
			&& (angle <= attAngle);
		}
	default:
		//Please the compiler
		return false;
	}
}
//-----------------------------------------------------------------------
bool DLight::getCastChadows() const
{
	return 
		mParentLight->_getManager()->isShadowTechniqueInUse() &&
		mParentLight->getCastShadows() && 
		(mParentLight->getType() == Light::LT_DIRECTIONAL || mParentLight->getType() == Light::LT_SPOTLIGHT);
}
//-----------------------------------------------------------------------
void DLight::updateFromCamera(Ogre::Camera* camera)
{
	//Set shader params
	const Ogre::MaterialPtr& mat = getMaterial();
	if (!mat->isLoaded()) 
	{
		mat->load();
	}
	Ogre::Technique* tech = mat->getBestTechnique();
	Ogre::Vector3 farCorner = camera->getViewMatrix(true) * camera->getWorldSpaceCorners()[4];

	for (unsigned short i=0; i<tech->getNumPasses(); i++) 
	{
		Ogre::Pass* pass = tech->getPass(i);
		// get the vertex shader parameters
		Ogre::GpuProgramParametersSharedPtr params = pass->getVertexProgramParameters();
		// set the camera's far-top-right corner
		if (params->_findNamedConstantDefinition("farCorner"))
			params->setNamedConstant("farCorner", farCorner);
	    
		params = pass->getFragmentProgramParameters();
		if (params->_findNamedConstantDefinition("farCorner"))
			params->setNamedConstant("farCorner", farCorner);

		//If inside light geometry, render back faces with CMPF_GREATER, otherwise normally
		if (mParentLight->getType() == Ogre::Light::LT_DIRECTIONAL)
		{
			pass->setCullingMode(Ogre::CULL_CLOCKWISE);
			pass->setDepthCheckEnabled(false);
		}
		else
		{
			pass->setDepthCheckEnabled(true);
			if (isCameraInsideLight(camera))
			{
				pass->setCullingMode(Ogre::CULL_ANTICLOCKWISE);
				pass->setDepthFunction(Ogre::CMPF_GREATER_EQUAL);
			}
			else
			{
				pass->setCullingMode(Ogre::CULL_CLOCKWISE);
				pass->setDepthFunction(Ogre::CMPF_LESS_EQUAL);
			}
		}

		Camera shadowCam("ShadowCameraSetupCam", 0);
		shadowCam._notifyViewport(camera->getViewport());
		SceneManager* sm = mParentLight->_getManager();
		sm->getShadowCameraSetup()->getShadowCamera(sm, 
			camera, camera->getViewport(), mParentLight, &shadowCam, 0);
			
		//Get the shadow camera position
		if (params->_findNamedConstantDefinition("shadowCamPos")) 
		{
			params->setNamedConstant("shadowCamPos", shadowCam.getPosition());
		}
		if (params->_findNamedConstantDefinition("shadowFarClip"))
		{
			params->setNamedConstant("shadowFarClip", shadowCam.getFarClipDistance());
		}

	}
}
