// Ryzom - MMORPG Framework <http://dev.ryzom.com/projects/ryzom/>
// Copyright (C) 2010  Winch Gate Property Limited
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

#include "stdpch.h"
#include "nel/misc/matrix.h"
#include "nel/3d/u_scene.h"
#include "nel/3d/u_transform.h"
#include "nel/3d/u_skeleton.h"
#include "nel/3d/u_bone.h"
#include "attached_fx.h"
#include "character_cl.h"
#include "user_entity.h"
#include "entities.h"
#include "time_client.h"
#include "fx_manager.h"

extern NL3D::UScene *Scene;


using namespace NLMISC;
using namespace NL3D;

extern CUserEntity	*UserEntity;

// *************************************************************************************
CAttachedFX::CAttachedFX()
{
	clear();
}

// *************************************************************************************
CAttachedFX::~CAttachedFX()
{
	clear();
}

// ***********************************************************************************************************************
void CAttachedFX::clear()
{
	if (!FX.empty())
	{
		if (StickMode == CFXStickMode::SpawnPermanent)
		{
			FXMngr.addFX(FX, 100.f);
		}
		else
		{
			Scene->deleteInstance(FX);
		}
	}
	FX = NULL;
	AniFX = NULL;
	TimeOutDate = FX_MANAGER_DEFAULT_TIMEOUT;
	StickMode = CFXStickMode::Follow;
	MaxAnimCount = 0;
	UserBoneID = ~0;
	TargeterUserBoneID = 0xff;
}

// ***********************************************************************************************************************
void CAttachedFX::create(CCharacterCL					&parent,
						 const CBuildInfo				&buildInfo,
					     const CTargeterInfo			&targeterInfo
						)
{
	clear();
	if (!buildInfo.Sheet) return;
	UParticleSystemInstance instance = buildInfo.Sheet->createMatchingInstance();
	// TODO nico : user params are not in the buildInfo, but are set by createMatchingInstance then by the caller (they could be set directly by createMatchingInstance)
	if (instance.empty()) return;
	create(parent, instance, buildInfo, targeterInfo);
}

// ***********************************************************************************************************************
void CAttachedFX::create(CCharacterCL					 &parent,
						 NL3D::UParticleSystemInstance   instance,
						 const CBuildInfo				 &buildInfo,
						 const CTargeterInfo			 &targeterInfo
					    )
{
	nlassert(buildInfo.Sheet);
	TimeOutDate = buildInfo.TimeOut;
	const CFXStickMode *stickMode = buildInfo.StickMode ? buildInfo.StickMode : &buildInfo.Sheet->Sheet->StickMode;
	if (!instance.empty())
	{
		instance.setClusterSystem(parent.getClusterSystem());
		instance.setTransformMode(NL3D::UTransformable::DirectMatrix);
		instance.show();
	}
	FX = instance;
	AniFX = buildInfo.Sheet;
	StickMode = stickMode->Mode;
	SpawnTime = TimeInSec;
	MaxAnimCount = buildInfo.MaxNumAnimCount;
	TargeterInfo = targeterInfo;
	switch(stickMode->Mode)
	{
		case CFXStickMode::StaticMatrix:
		{
			const CMatrix &staticMatrix = buildInfo.StaticMatrix ? *buildInfo.StaticMatrix : NLMISC::CMatrix::Identity;
			instance.setMatrix(staticMatrix);
			SpawnPos = staticMatrix.getPos();
		}
		break;
		case CFXStickMode::Spawn:
		case CFXStickMode::SpawnPermanent:
		{
			parent.alignFX(instance, parent.getScalePos());
			SpawnPos = parent.pos() + buildInfo.StickOffset;
			if (stickMode->Mode == CFXStickMode::SpawnPermanent)
			{
				instance.forceInstanciate();
			}
		}
		break;
		case CFXStickMode::UserBone:
		{
			if (parent.skeleton())
			{
				sint boneID = parent.skeleton()->getBoneIdByName(NLMISC::CStringMapper::unmap(stickMode->UserBoneName));
				UserBoneID = boneID;
				if (boneID != -1)
				{
					parent.skeleton()->stickObjectEx(instance, boneID, true);
					NLMISC::CMatrix mat = instance.getMatrix();
					mat.scale(buildInfo.Sheet->Sheet->ScaleFX ? parent.getScaleRef() : 1.f);
					mat.setPos(buildInfo.StickOffset);
					if (!instance.empty()) instance.setMatrix(mat);
					SpawnPos = buildInfo.StickOffset;
					// if parent is hidden, then force to compute the bone at least once
					if (parent.skeleton()->getVisibility() == NL3D::UTransform::Hide)
					{
						parent.forceEvalAnim();
						// force to compute fx at least once
						parent.skeleton()->forceComputeBone(UserBoneID);
					}
					break;
				}
			}
			// bone not found or no skeleton
			if (!parent.instance().empty())
			{
				instance.parent(parent.instance());
				SpawnPos = buildInfo.StickOffset;
			}
			else
			{
				// just spawn at position of entity
				SpawnPos = parent.pos() + buildInfo.StickOffset;
			}
		}
		break;
		case CFXStickMode::UserBoneOrientedTowardTargeter:
			if (parent.skeleton())
			{
				UserBoneID = parent.skeleton()->getBoneIdByName(NLMISC::CStringMapper::unmap(stickMode->UserBoneName));
				if (UserBoneID == 0xff)
				{
					nlwarning("Bad bone name : %s", NLMISC::CStringMapper::unmap(stickMode->UserBoneName).c_str());
				}
			}
			else
			{
				UserBoneID = 0xff;
			}
			SpawnPos = buildInfo.StickOffset;
			update(parent, CMatrix::Identity);
		break;
		case CFXStickMode::UserBoneRay:
			if (parent.skeleton())
			{
				UserBoneID = parent.skeleton()->getBoneIdByName(NLMISC::CStringMapper::unmap(stickMode->UserBoneName));
				if (UserBoneID == 0xff)
				{
					nlwarning("Bad bone name : %s", NLMISC::CStringMapper::unmap(stickMode->UserBoneName).c_str());
				}
			}
			else
			{
				UserBoneID = 0xff;
			}
			SpawnPos = buildInfo.StickOffset;
			TargeterUserBoneID =0xff;
			if (targeterInfo.StickMode.UserBoneName != 0)
			{
				CEntityCL *targeter =  EntitiesMngr.entity(TargeterInfo.Slot);
				if (targeter && targeter->skeleton())
				{
					TargeterUserBoneID = parent.skeleton()->getBoneIdByName(NLMISC::CStringMapper::unmap(TargeterInfo.StickMode.UserBoneName));
				}
			}
			update(parent, CMatrix::Identity);
		break;
		case CFXStickMode::OrientedTowardTargeter:
			SpawnPos = buildInfo.StickOffset;
			update(parent, CMatrix::Identity);
		break;
		default: // -> stick fx	in 'Follow' mode
			parent.alignFX(instance, parent.getScalePos());
			SpawnPos = buildInfo.StickOffset;
		break;
	}
}


// ***********************************************************************************************************************
void CAttachedFX::evalTargeterStickPos(NLMISC::CVector &dest) const
{
	CEntityCL *targeter = EntitiesMngr.entity(TargeterInfo.Slot);
	if (!targeter)
	{
		dest = TargeterInfo.DefaultPos;
		return;
	}
	switch(TargeterInfo.StickMode.Mode)
	{
		case  CFXStickMode::Spawn:
		case CFXStickMode::SpawnPermanent:
		case  CFXStickMode::Follow:
		case  CFXStickMode::FollowNoRotation:
		case  CFXStickMode::OrientedTowardTargeter:
			dest = targeter->pos();
		break;
		case  CFXStickMode::StaticMatrix:
			nlwarning("Not implemented"); // this case is not used for now
			dest = targeter->pos();
		break;
		case CFXStickMode::UserBoneOrientedTowardTargeter:
		case CFXStickMode::UserBoneRay:
		case CFXStickMode::UserBone:
			if (targeter->skeleton() && TargeterUserBoneID != 0xff)
			{
				const UBone bone = targeter->skeleton()->getBone(TargeterUserBoneID);
				targeter->forceEvalAnim();
				targeter->skeleton()->forceComputeBone(TargeterUserBoneID);
				dest = bone.getLastWorldMatrixComputed() * TargeterInfo.StickOffset;
			}
			else
			{
				dest = TargeterInfo.DefaultPos;
			}
		break;
	};
}

// ***********************************************************************************************************************
void CAttachedFX::update(CCharacterCL &parent, const NLMISC::CMatrix &alignMatrix)
{
	if (AniFX && !FX.empty())
	{
		NLMISC::CVector trackPos;
		// see if fx has a track applied on it
		if (AniFX->PosTrack)
		{
			// eval current pos
			AniFX->PosTrack->interpolate((float) (TimeInSec - SpawnTime), trackPos);
		}
		else
		{
			trackPos.set(0.f, 0.f, 0.f);
		}
		// apply pos depending on mode
		switch(StickMode)
		{
			case CFXStickMode::UserBone:
			{
				FX.setClusterSystem(parent.getClusterSystem());
				if (!AniFX->PosTrack)
				{
					// no track for fx,
					if (parent.skeleton() && UserBoneID != 0xff)
					{
						if (parent.skeleton()->getVisibility() == UTransform::Hide)
						{
							// if user no visible, force to compute the bone.
							parent.forceEvalAnim();
							parent.skeleton()->forceComputeBone(UserBoneID);
						}
					}
					return;
				}
				if (parent.skeleton())
				{
					if (UserBoneID != 0xff)
					{
						CMatrix mat = FX.getMatrix();
						mat.setPos(trackPos + SpawnPos);
						mat.setScale(AniFX->Sheet->ScaleFX ? parent.getScaleRef() : 1.f);
						FX.setMatrix(mat);
						if (&parent == UserEntity && UserEntity->skeleton())
						{
							if (UserEntity->skeleton()->getVisibility() == UTransform::Hide)
							{
								// if user no visible, force to compute the bone.
								parent.forceEvalAnim();
								UserEntity->skeleton()->forceComputeBone(UserBoneID);
							}
						}
					}
				}
				// no skeleton or bone not found
				if (!parent.instance().empty())
				{
					CMatrix mat = FX.getMatrix();
					mat.setPos(trackPos + SpawnPos);
					mat.setScale(AniFX->Sheet->ScaleFX ? parent.getScaleRef() : 1.f);
					FX.setMatrix(mat);
				}
				else
				{
					// no skeleton, no instance
					CMatrix mat = FX.getMatrix();
					mat.setPos(trackPos + SpawnPos + parent.pos());
					mat.setScale(AniFX->Sheet->ScaleFX ? parent.getScaleRef() : 1.f);
					FX.setMatrix(mat);
				}
			}
			break;
			case CFXStickMode::Follow:
				// just change local pos
				parent.alignFX(FX, alignMatrix, AniFX->Sheet->ScaleFX ? parent.getScaleRef() : 1.f, trackPos + SpawnPos);
			break;
			case CFXStickMode::FollowNoRotation:
			{
				// just update the pos
				CMatrix mat = FX.getMatrix();
				mat.setScale(AniFX->Sheet->ScaleFX ? parent.getScaleRef() : 1.f);
				mat.setPos(trackPos + parent.pos().asVector());
				FX.setMatrix(mat);
				FX.setClusterSystem(parent.getClusterSystem());
			}
			break;
			case CFXStickMode::StaticObjectCastRay:
			{
				// if not animated need no updates
				if (!AniFX->PosTrack) return;
				nlwarning("Not implemented");
			}
			break;
			case CFXStickMode::Spawn:
			case CFXStickMode::SpawnPermanent:
			{
				if (!AniFX->PosTrack) return;
				// put in local basis and offset spawn pos
				CMatrix mat = FX.getMatrix();
				mat.setScale(AniFX->Sheet->ScaleFX ? parent.getScaleRef() : 1.f);
				mat.setPos(SpawnPos + FX.getMatrix().mulVector(trackPos));
				FX.setMatrix(mat);
				// Do not update the cluster system because fx stays in place
			}
			break;
			case CFXStickMode::OrientedTowardTargeter:
			{
				CEntityCL *targeter = EntitiesMngr.entity(TargeterInfo.Slot);
				if (targeter)
				{
					CVectorD orientD = parent.pos() - targeter->pos();
					CVector  J((float) orientD.x, (float) orientD.y, 0.f); // project on XY plane
					J.normalize();
					CVector I = J ^ CVector::K;
					CMatrix mat;
					mat.setRot(I, J, CVector::K);
					mat.setScale(AniFX->Sheet->ScaleFX ? parent.getScaleRef() : 1.f);
					mat.setPos(trackPos + parent.pos().asVector());
					FX.setMatrix(mat);
					FX.setClusterSystem(parent.getClusterSystem());
				}
			}
			break;
			case CFXStickMode::UserBoneOrientedTowardTargeter:
			{
				CEntityCL *targeter = EntitiesMngr.entity(TargeterInfo.Slot);
				if (targeter)
				{
					CVector orientD;
					CMatrix orientMat;
					if (UserBoneID == 0xff || !parent.skeleton())
					{
						// bone not found -> use parent position instead
						orientD = parent.pos() - targeter->pos();
						orientMat.setPos(trackPos + parent.pos());
					}
					else
					{
						const UBone bone = parent.skeleton()->getBone(UserBoneID);
						parent.forceEvalAnim();
						parent.skeleton()->forceComputeBone(UserBoneID);
						const CMatrix &wm = bone.getLastWorldMatrixComputed();
						// compute orientation toward targeter, and build a matrix from it
						orientD = wm.getPos() - targeter->pos().asVector();
						orientMat.setPos(trackPos + wm.getPos());
					}
					CVector  J(orientD.x, orientD.y, 0.f); // project on XY plane
					J.normalize();
					CVector I = J ^ CVector::K;
					float scale = AniFX->Sheet->ScaleFX ? parent.getScaleRef() : 1.f;
					orientMat.setRot(scale * I, scale * J, scale * CVector::K, true);
					FX.setMatrix(orientMat);
					FX.setClusterSystem(parent.getClusterSystem());
				}
			}
			break;
			case CFXStickMode::UserBoneRay:
			{
				CVector aimingPoint;
				evalTargeterStickPos(aimingPoint);
				CVector startPoint = CVector::Null;
				if (UserBoneID != 0xff && parent.skeleton())
				{
					const UBone bone = parent.skeleton()->getBone(UserBoneID);
					parent.forceEvalAnim();
					parent.skeleton()->forceComputeBone(UserBoneID);
					startPoint = bone.getLastWorldMatrixComputed().getPos();
				}
				CMatrix rayMat;
				CVector ray = aimingPoint - startPoint;
				CVector I = ray.normed();
				CVector K = (CVector::K - (I * CVector::K) * I).normed();
				CVector J = K ^ I;
				if (AniFX)
				{
					I *= ray.norm() / AniFX->Sheet->RayRefLength;
				}
				rayMat.setRot(I, J, K);
				rayMat.setPos(startPoint + 0.5f * AniFX->Sheet->RayRefLength * I);
				FX.setMatrix(rayMat);
				// don't clusterize ray, because it can be quite large
				FX.setForceClipRoot(true);

			}

		}
	}
}