
/*
	file : CCal3dSceneNode.cpp

	Dragon Wings (c) 2005 Philipp Roßberger, Sebastian Roll
		www.einszunull.net
		www.sebastianroll.de

	This software is provided 'as-is', without any express or implied
	warranty.  In no event will the authors be held liable for any damages
	arising from the use of this software.

	Permission is granted to anyone to use this software for any purpose,
	including commercial applications, and to alter it and redistribute it
	freely, subject to the following restrictions:

	1. The origin of this software must not be misrepresented; you must not
		claim that you wrote the original software. If you use this software
		in a product, an acknowledgment in the product documentation would be
		appreciated but is not required.
	2. Altered source versions must be plainly marked as such, and must not be
		misrepresented as being the original software.
	3. This notice may not be removed or altered from any source distribution.

	Used software libraries from other people (thank you very much):

	The Irrlicht Engine:
		Copyright (c) 2002-2005 Nikolaus Gebhardt
		www.irrlicht.sourceforge.net
  
	The Character Animation Library (cal3D):
		Copyright (c) 2001, 2002 Bruno 'Beosil' Heidelberger
		www.cal3d.sourceforge.net

	The FMOD audio library:
		Copyright (c) Firelight Technologies Pty, Ltd. 1999-2004
		www.fmod.org

	Article - Using Cal3D with Irrlicht:
		Copyright (c) Thomas Suter 2004
		www.thomas.webtracker.ch/jahia/Jahia/cache/off/pid/672
*/

#include "CCal3dSceneNode.h"

#define DUMP_OFFLINE_ANIMATION false

#include <fstream>
#include <vector>

//! constructor
//! @param cf, configuration file
//! @param parent, parent scene node
//! @param mgr, scene manager
//! @param id, scenenode id
CCal3DSceneNode::CCal3DSceneNode(const char* cf,
	irr::scene::ISceneNode* parent, 
	irr::scene::ISceneManager* mgr, irr::s32 id)
	:irr::scene::ISceneNode(parent,mgr,id), configFile(cf)
{
	m_scale = 1.0f;
	m_blendTime = 0.05f;
	m_lodLevel = 1.0f;
	lastTick=0;
	m_bPaused=false;
	bInitialized=false;
	
	mediaPath=configFile.substr(0,configFile.find_last_of("/"));
	
	material.Wireframe=false;
	material.Lighting = true;
	//material.MaterialType=irr::video::EMT_TRANSPARENT_VERTEX_ALPHA;
	material.MaterialType=irr::video::EMT_SOLID;
	material.ZWriteEnable=true;
	material.BackfaceCulling=false;

	setDebugDataVisible(false);
}



//! destructur
CCal3DSceneNode::~CCal3DSceneNode()
{
	delete m_calModel;
	delete m_calCoreModel;
}



////////////////////////////////////////////////
// irrlicht rendering stuff
////////////////////////////////////////////////

void CCal3DSceneNode::OnPreRender()
{
	if (bInitialized && IsVisible) 
	{
		SceneManager->registerNodeForRendering(this);
	}

	irr::scene::ISceneNode::OnPreRender();
}



void CCal3DSceneNode::render()
{
	
	if(bInitialized)
	{
		irr::video::IVideoDriver* driver=SceneManager->getVideoDriver();
		
		
		driver->setTransform(irr::video::ETS_WORLD, AbsoluteTransformation);
		
		
		irr::video::S3DVertex tmp;
		irr::scene::SMeshBuffer mb;

		unsigned char meshColor[4]; // r g b a


		// get the renderer of the model
		CalRenderer *pCalRenderer;
		pCalRenderer = m_calModel->getRenderer();
		pCalRenderer->setNormalization(true);

		if(this->DebugDataVisible)
		{
			irr::video::SMaterial mat;
			mat.Wireframe=false;
			mat.Lighting=false;
			driver->setMaterial(mat);
			driver->draw3DBox(Box);

			CalSkeleton* pCalSkeleton = m_calModel->getSkeleton();
			pCalSkeleton->calculateBoundingBoxes();
			std::vector<CalBone*> &vectorCoreBone = pCalSkeleton->getVectorBone();
			irr::core::aabbox3df b;
			CalVector p[8];
			Vector3 v[8];
			

			for(size_t boneId=0;boneId<vectorCoreBone.size();++boneId)
			{
				CalBone* bone=vectorCoreBone[boneId];
				CalBoundingBox & calBoundingBox = bone->getBoundingBox();
				calBoundingBox.computePoints(p);
				
				for(int i=0;i<8;++i) v[i].set(p[i].x,p[i].y,p[i].z);
				
				driver->setMaterial(mat);
				// draw the box
				driver->draw3DLine(v[0],v[1],irr::video::SColor(255,0,0,255));
				driver->draw3DLine(v[0],v[2],irr::video::SColor(255,0,0,255));
				driver->draw3DLine(v[1],v[3],irr::video::SColor(255,0,0,255));
				driver->draw3DLine(v[2],v[3],irr::video::SColor(255,0,0,255));
				driver->draw3DLine(v[4],v[5],irr::video::SColor(255,0,0,255));
				driver->draw3DLine(v[4],v[6],irr::video::SColor(255,0,0,255));
				driver->draw3DLine(v[5],v[7],irr::video::SColor(255,0,0,255));
				driver->draw3DLine(v[6],v[7],irr::video::SColor(255,0,0,255));
				driver->draw3DLine(v[0],v[4],irr::video::SColor(255,0,0,255));
				driver->draw3DLine(v[1],v[5],irr::video::SColor(255,0,0,255));
				driver->draw3DLine(v[2],v[6],irr::video::SColor(255,0,0,255));
				driver->draw3DLine(v[3],v[7],irr::video::SColor(255,0,0,255));
			}
		}

		// begin the rendering loop
		if(pCalRenderer->beginRendering())
		{
			// get the number of meshes
			int meshCount;
			meshCount = pCalRenderer->getMeshCount();

			// render all meshes of the model
			int meshId;
			for(meshId = 0; meshId < meshCount; meshId++)
			{
				// get the number of submeshes
				int submeshCount;
				submeshCount = pCalRenderer->getSubmeshCount(meshId);

				// render all submeshes of the mesh
				int submeshId;
				for(submeshId = 0; submeshId < submeshCount; submeshId++)
				{
					// select mesh and submesh for further data access
					if(pCalRenderer->selectMeshSubmesh(meshId, submeshId))
					{

						// set the material ambient color
						pCalRenderer->getAmbientColor(&meshColor[0]);
						material.AmbientColor.set(meshColor[3],meshColor[0],meshColor[1],meshColor[2]);

						// set the material diffuse color
						pCalRenderer->getDiffuseColor(&meshColor[0]);
						material.DiffuseColor.set(meshColor[3],meshColor[0],meshColor[1],meshColor[2]);

						// set the material specular color
						pCalRenderer->getSpecularColor(&meshColor[0]);
						material.SpecularColor.set(meshColor[3],meshColor[0],meshColor[1],meshColor[2]);

						// set the material shininess factor
						material.Shininess = pCalRenderer->getShininess();

						// get the transformed vertices of the submesh
						static float meshVertices[30000][3];
						int vertexCount;
						vertexCount = pCalRenderer->getVertices(&meshVertices[0][0]);

						// get the transformed normals of the submesh
						static float meshNormals[30000][3];
						int normalsCount=pCalRenderer->getNormals(&meshNormals[0][0]);

						// get the texture coordinates of the submesh
						static float meshTextureCoordinates[30000][2];
						int textureCoordinateCount;
						textureCoordinateCount = pCalRenderer->getTextureCoordinates(0, &meshTextureCoordinates[0][0]);

						// get the faces of the submesh
						static CalIndex meshFaces[50000][3];
						int faceCount =pCalRenderer->getFaces(&meshFaces[0][0]);
						
						if((pCalRenderer->getMapCount() > 0) && (textureCoordinateCount > 0))
						{
							irr::video::ITexture* t=static_cast<irr::video::ITexture*>(pCalRenderer->getMapUserData(0));
							material.Texture1=t;
						}
						
						mb.Vertices.clear();
						for(int i=0;i<vertexCount;i++)
						{
							tmp.Pos.set(meshVertices[i][0],meshVertices[i][1],meshVertices[i][2]);
							tmp.Normal.set(meshNormals[i][0],meshNormals[i][1],meshNormals[i][2]);
							tmp.TCoords.set(meshTextureCoordinates[i][0],meshTextureCoordinates[i][1]);
							tmp.Color=irr::video::SColor(255,255,255,255);
							mb.Vertices.push_back(tmp);
						}

						mb.Indices.clear();
						for(int i=0; i<faceCount; ++i){
							mb.Indices.push_back(meshFaces[i][0]);
							mb.Indices.push_back(meshFaces[i][1]);
							mb.Indices.push_back(meshFaces[i][2]);
						}

						// draw 
						driver->setMaterial(material);

						driver->drawIndexedTriangleList(mb.Vertices.const_pointer(),
							mb.Vertices.size(),mb.Indices.const_pointer(),faceCount);
						
						//CLog::GetSingleton().write(LOG_APP,"#Verts %d #Norm %d #Tex %d #Faces %d #Map %d",
						//	vertexCount,normalsCount,textureCoordinateCount,faceCount, pCalRenderer->getMapCount());
					}
				}
			}

			// end the rendering
			pCalRenderer->endRendering();
			
		}
	}
}



void CCal3DSceneNode::OnPostRender(irr::u32 timeMs)
{
	// calculate the amount of elapsed seconds
	float elapsedSeconds;
	elapsedSeconds = (float)(timeMs - lastTick) / 1000.0f;

	// update the model if not paused
	if(!m_bPaused && lastTick!=0)
	{
		// check if the time has come to blend to the next animation
		if(m_calCoreModel->getCoreAnimationCount() > 1)
		{
			m_leftAnimationTime -= elapsedSeconds;
			if(m_leftAnimationTime <= m_blendTime)
			{
				// get the next animation
				m_currentAnimationId = (m_currentAnimationId + 1) % m_calCoreModel->getCoreAnimationCount();

				// fade in the new animation
				m_calModel->getMixer()->executeAction(m_currentAnimationId, m_leftAnimationTime, m_blendTime);

				// adjust the animation time left until next animation flip
				m_leftAnimationTime = m_calCoreModel->getCoreAnimation(m_currentAnimationId)->getDuration() - m_blendTime;
			}
		}

		m_calModel->update(elapsedSeconds);

		CalVector p[8];
		m_calModel->getBoundingBox(true).computePoints(p);
		Box.reset(p[0].x,p[0].y,p[0].z);
		for(int i=0;i<8;++i) Box.addInternalPoint(p[i].x,p[i].y,p[i].z);
	}

	// update bones from special animation (physic etc.)

	// current tick will be last tick next round
	lastTick = timeMs;

	irr::scene::ISceneNode::OnPostRender(timeMs);
}



const irr::core::aabbox3df& CCal3DSceneNode::getBoundingBox() const
{
	return Box;
}



////////////////////////////////////////////////
// call 3d stuff
////////////////////////////////////////////////

//! load and parse the configuration file
//! @param cf, configuration file name
//! @return true on success, false otherwise
bool CCal3DSceneNode::parseModelConfiguration(const std::string &cf)
{
	std::string fn;
	// open the model configuration file
	std::ifstream file;
	file.open(cf.c_str(), std::ios::in | std::ios::binary);
	if(!file)
	{
		//"Failed to open model configuration file '%s'." ,cf.c_str()
		return false;
	}

	// create a core model instance
	m_calCoreModel = new CalCoreModel("dummy");
	if(!m_calCoreModel)
	{
		// "CalError : %s",CalError::getLastErrorDescription().c_str()
		return false;
	}

	// parse all lines from the model configuration file
	int line;
	for(line = 1; ; line++)
	{
		// read the next model configuration line
		std::string strBuffer;
		std::getline(file, strBuffer);

		// stop if we reached the end of file
		if(file.eof()) break;

		// check if an error happend while reading from the file
		if(!file)
		{
			//"Error while reading from the model configuration file '%s'." ,cf.c_str()
			return false;
		}

		// find the first non-whitespace character
		std::string::size_type pos;
		pos = strBuffer.find_first_not_of(" \t");

		// check for empty lines
		if((pos == std::string::npos) || (strBuffer[pos] == '\n') || (strBuffer[pos] == '\r') || (strBuffer[pos] == 0)) continue;

		// check for comment lines
		if(strBuffer[pos] == '#') continue;

		// get the key
		std::string strKey;
		strKey = strBuffer.substr(pos, strBuffer.find_first_of(" =\t\n\r", pos) - pos);
		pos += strKey.size();

		// get the '=' character
		pos = strBuffer.find_first_not_of(" \t", pos);
		if((pos == std::string::npos) || (strBuffer[pos] != '='))
		{
			//"%s  (%d): Invalid syntax.",cf.c_str(),line
			return false;
		}

		// find the first non-whitespace character after the '=' character
		pos = strBuffer.find_first_not_of(" \t", pos + 1);

		// get the data
		std::string strData;
		strData = strBuffer.substr(pos, strBuffer.find_first_of("\n\r", pos) - pos);

		// handle the model creation
		if(strKey == "scale")
		{
			// set rendering scale factor
			m_scale = float(atof(strData.c_str()));
		}
		else if(strKey == "skeleton")
		{
			// load core skeleton
			fn.clear(); fn=mediaPath+"/"+strData;
			//"Loading skeleton '%s'...",fn.c_str()
			if(!m_calCoreModel->loadCoreSkeleton(fn))
			{
				//"CalError : %s",CalError::getLastErrorDescription().c_str()
				return false;
			}
		}
		else if(strKey == "animation")
		{
			fn.clear(); fn=mediaPath+"/"+strData;
			// load core animation
			//"Loading animation '%s'...",fn.c_str()
			if(m_calCoreModel->loadCoreAnimation(fn) == -1)
			{
				//"CalError : %s",CalError::getLastErrorDescription().c_str()
				return false;
			}
		}
		else if(strKey == "mesh")
		{
			fn.clear(); fn=mediaPath+"/"+strData;
			// load core mesh
			//"Loading mesh '%s'...",fn.c_str()
			if(m_calCoreModel->loadCoreMesh(fn) == -1)
			{
				//"CalError : %s",CalError::getLastErrorDescription().c_str()
				return false;
			}
		}
		else if(strKey == "material")
		{
			fn.clear(); fn=mediaPath+"/"+strData;
			// load core material
			//"Loading material '%s'...",fn.c_str()
			if(m_calCoreModel->loadCoreMaterial(fn) == -1)
			{
				//"CalError : %s",CalError::getLastErrorDescription().c_str()
				return false;
			}
		}
		else
		{
			// everything else triggers an error message, but is ignored
			//"%s  (%d): Invalid syntax.",cf.c_str(),line
		}
	}

	// explicitely close the file
	file.close();

	return true;
}



// load all textures
bool CCal3DSceneNode::init()
{
	bool ok=true;

	if(!parseModelConfiguration(configFile))
	{
		bInitialized=false;
		return false;
	}

	m_calCoreModel->scale(m_scale);

	// make one material thread for each material
	// NOTE: this is not the right way to do it, but this viewer can't do the right
	// mapping without further information on the model etc., so this is the only
	// thing we can do here.
	for(int materialId = 0; materialId < m_calCoreModel->getCoreMaterialCount(); materialId++)
	{
		// create the a material thread
		m_calCoreModel->createCoreMaterialThread(materialId);

		// initialize the material thread
		m_calCoreModel->setCoreMaterialId(materialId, 0, materialId);
	}

	m_calCoreModel->getCoreSkeleton()->calculateBoundingBoxes(m_calCoreModel);

	// load all textures and store the texture id in the corresponding map in the material
	for(int materialId = 0; materialId < m_calCoreModel->getCoreMaterialCount(); materialId++)
	{
		// get the core material
		CalCoreMaterial *pCoreMaterial;
		pCoreMaterial = m_calCoreModel->getCoreMaterial(materialId);

		// loop through all maps of the core material
		int mapId;
		for(mapId = 0; mapId < pCoreMaterial->getMapCount(); mapId++)
		{
			// get the filename of the texture
			std::string fn=mediaPath+"/"+pCoreMaterial->getMapFilename(mapId);
			
			// load the texture from the file
			irr::video::ITexture* texture=SceneManager->getVideoDriver()->getTexture(fn.c_str());
			if(texture)
			{
				// store the texture id in the user data of the map
				pCoreMaterial->setMapUserData(mapId, (Cal::UserData)texture);
			}
			else
			{
				ok=false;
				//"Loading texture '%s' failed.",fn.c_str()
			}

		}
	}

	// create the model instance from the loaded core model
	m_calModel = new CalModel(m_calCoreModel);
	if(!m_calModel)
	{
		//"CalError : %s",CalError::getLastErrorDescription().c_str()
		return false;
	}

	// attach all meshes to the model
	int meshId;
	for(meshId = 0; meshId < m_calCoreModel->getCoreMeshCount(); meshId++)
	{
		m_calModel->attachMesh(meshId);
	}
	// set the material set of the whole model
	m_calModel->setMaterialSet(0);

	// set initial animation state
	if(m_calCoreModel->getCoreAnimationCount() > 0)
	{
		m_currentAnimationId = 0;
		m_leftAnimationTime = m_calCoreModel->getCoreAnimation(m_currentAnimationId)->getDuration() - m_blendTime;
		if(m_calCoreModel->getCoreAnimationCount() > 1)
		{
			m_calModel->getMixer()->executeAction(m_currentAnimationId, 0.0f, m_blendTime);
		}
		else
		{
			m_calModel->getMixer()->blendCycle(m_currentAnimationId, 1.0f, 0.0f);
		}
	}
	else
	{
		m_currentAnimationId = -1;
		m_leftAnimationTime = -1.0f;
	}

	m_calModel->setLodLevel(m_lodLevel);
	m_calModel->update(0);

	// calculate the irrlicht bounding box
	CalVector p[8];
	m_calModel->getBoundingBox(true).computePoints(p);
	Box.reset(p[0].x,p[0].y,p[0].z);
	for(int i=0;i<8;++i) Box.addInternalPoint(p[i].x,p[i].y,p[i].z);

	//"Init done."
	bInitialized=ok;
	
	return ok;
}

////////////////////////////////////////////////
// dragon stuff
////////////////////////////////////////////////

void CCal3DSceneNode::moveBone(int boneID, CalQuaternion& direction)
{
	if (! DUMP_OFFLINE_ANIMATION)
	{
		CalQuaternion oldPos;
		CalQuaternion newPos;
		oldPos = getCoreBoneRotation(boneID);
		newPos.w = oldPos.w + direction.w;
		newPos.x = oldPos.x + direction.x;
		newPos.y = oldPos.y + direction.y;
		newPos.z = oldPos.z + direction.z;
		setBoneRotation(boneID, newPos);
	}
	else
	{
		dumpBoneQaternion(boneID);
	}
}

CalQuaternion CCal3DSceneNode::getCoreBoneRotation(int boneID)
{
	CalQuaternion q(0,0,0,0);
	q = m_calCoreModel->getCoreSkeleton()->getCoreBone(boneID)->getRotation();
	
	return q;
}

CalQuaternion CCal3DSceneNode::getBoneRotation(int boneID)
{
	CalQuaternion q(0,0,0,0);
	q = m_calModel->getSkeleton()->getBone(boneID)->getRotation();
	
	return q;
}

void CCal3DSceneNode::setBoneRotation(int boneID, CalQuaternion& newRotation)
{
	//m_calCoreModel->getCoreSkeleton()->getCoreBone(boneID)->setRotation(newRotation);
	m_calModel->getSkeleton()->getBone(boneID)->setRotation(newRotation);
	m_calModel->getSkeleton()->calculateState();
}

void CCal3DSceneNode::dumpBoneQaternion(int boneID)
{
	CalQuaternion q(0,0,0,0);
	//q = m_calCoreModel->getCoreSkeleton()->getCoreBone(boneID)->getRotation();
	q = m_calModel->getSkeleton()->getBone(boneID)->getRotation();
	
	//debug:
	std::cout << "getBoneRotation("<<boneID<<") q: x="<<q.x<<" y="<<q.y<<" z="<<q.z<<" w="<<q.w<<std::endl;

}

float CCal3DSceneNode::quaternionGetMagnitude(CalQuaternion qIn)
{
	/*
	How do I calculate the magnitude of a quaternion?
	The magnitude of a quaternion is calculated by multiplying the
	quaternion with its conjugate ie:

				 ------------
				/       --
	  |Qr| =  \/     Qr.Qr

	*/
    return( sqrt(qIn.w*qIn.w + qIn.x*qIn.x + qIn.y*qIn.y + qIn.z*qIn.z) );

}

void CCal3DSceneNode::quaternionNormalize(CalQuaternion& qInOut)
{
	/*
	How do I normalise a quaternion?
	A quaternion can be normalised in a way similar to vectors. The
	magnitude of the quaternion is calculated first. Then both the
	scalar and vector part of the quaternion are divided by this value.
	A unit quaternion will always have a magnitude of 1.0
	*/
	float magnitude = quaternionGetMagnitude(qInOut);
	qInOut.x = qInOut.x / magnitude;
	qInOut.y = qInOut.y / magnitude;
	qInOut.z = qInOut.z / magnitude;
	qInOut.w = qInOut.w / magnitude;
}

void CCal3DSceneNode::quaternionToAxisAndAngle(CalQuaternion qIn, CalVector& vOut, float& aOut)
{
    /* 
	How do I convert a quaternion to a rotation axis and angle?
	A quaternion can be converted back to a rotation axis and angle
	using the following algorithm:
 
	quaternion_normalise( qr );

    cos_angle  = qr -> qw;
    angle      = acos( cos_angle ) * 2 * RADIANS;
    sin_angle  = sqrt( 1.0 - cos_angle * cos_angle );

    if ( fabs( sin_angle ) < 0.0005 )
      sa = 1;

    axis -> vx = qr -> qx / sa;
    axis -> vy = qr -> qy / sa;
    axis -> vz = qr -> qz / sa;
	*/

	/*
	quaternionNormalize(qIn);

	float cosAngle = qIn.w;
	float angle = acos(cosAngle) * 2 * ( ( 2 * 3.14159265f ) / 360 );
	float sinAngle = sqrt( 1.0f - cosAngle * cosAngle );

	if ( fabs( sinAngle ) < 0.0005 )
		sinAngle = 1.0f;

	vOut.x = qIn.x / sinAngle;
	vOut.y = qIn.y / sinAngle;
	vOut.z = qIn.z / sinAngle;
	*/

	float s = qIn.x*qIn.x + qIn.y*qIn.y + qIn.z*qIn.z;
	vOut.x = qIn.x / s;
	vOut.y = qIn.y / s;
	vOut.z = qIn.z / s;
	aOut = 2 * acos(qIn.w);
}

// CalQuaternion qOut will represent a vector (x,y,z) and an angel (w) of qIn
void CCal3DSceneNode::quaternionToAxisAndAngle(CalQuaternion qIn, CalQuaternion& qOut)
{
	CalVector v;
	float angle;

	quaternionToAxisAndAngle(qIn, v, angle);

	qOut.x = v.x;
	qOut.y = v.y;
	qOut.z = v.z;
	qOut.x = angle;

}


