// 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 //
/////////////

#include "animation_state.h"
#include "debug_client.h"
#include "client_cfg.h"
// misc
#include "nel/misc/debug.h"
// Georges
#include "nel/georges/u_form_elm.h"

#ifdef DEBUG_NEW
#define new DEBUG_NEW
#endif

///////////
// USING //
///////////

using namespace NLGEORGES;
using namespace std;

////////////
// METHOD //
////////////

//-----------------------------------------------
//-----------------------------------------------
EGSPD::CPeople::TPeople		CAnimationState::_FallBackToDefaultRace[2][CAnimationStateSheet::StaticStateCount];
bool						CAnimationState::_FallBackToDefaultRaceInited= false;

//-----------------------------------------------
//-----------------------------------------------
// Yoyo: ugly: because of players, we have to disable racial animation if they don't want them
void	CAnimationState::initFallBackToDefaultRace()
{
	_FallBackToDefaultRaceInited= true;

	// Default
	for(uint i=0;i<CAnimationStateSheet::StaticStateCount;i++)
	{
		_FallBackToDefaultRace[0][i]= EGSPD::CPeople::Matis;
		_FallBackToDefaultRace[1][i]= EGSPD::CPeople::Matis;
	}

	// Special for Sit mode. Use fyros for female, and tryker for male
	_FallBackToDefaultRace[0][CAnimationStateSheet::SitMode]= EGSPD::CPeople::Tryker;
	_FallBackToDefaultRace[1][CAnimationStateSheet::SitMode]= EGSPD::CPeople::Fyros;
	_FallBackToDefaultRace[0][CAnimationStateSheet::SitEnd]= EGSPD::CPeople::Tryker;
	_FallBackToDefaultRace[1][CAnimationStateSheet::SitEnd]= EGSPD::CPeople::Fyros;
	_FallBackToDefaultRace[0][CAnimationStateSheet::Idle]= EGSPD::CPeople::Tryker;
	_FallBackToDefaultRace[1][CAnimationStateSheet::Idle]= EGSPD::CPeople::Fyros;
}

//-----------------------------------------------
//-----------------------------------------------
// Yoyo: ugly: because of players, we have to disable some racial animation since they don't want them AT ALL
bool	CAnimationState::isOldRaceAnimationForced(EGSPD::CPeople::TPeople race, GSGENDER::EGender gender) const
{
	TAnimStateId animState= state();
	// consider Idle as sit.... This is false if the animation MODE is not SIT, but don't care,
	// since idle is never a run/walk state....
	bool	isSit= animState==CAnimationStateSheet::SitMode ||
		animState==CAnimationStateSheet::SitEnd ||
		animState==CAnimationStateSheet::Idle;
	// disable Tryker male run/walk
	if(race==EGSPD::CPeople::Tryker && gender==GSGENDER::male && !isSit)
		return true;
	// disable Fyros male Sit
	if(race==EGSPD::CPeople::Fyros && gender==GSGENDER::male && isSit)
		return true;
	// disable Matis (male/female) Sit
	if(race==EGSPD::CPeople::Matis && isSit)
		return true;

	// otherwise, allow every other racial animation
	return false;
}


//-----------------------------------------------
// CAnimationState :
// Constructor.
//-----------------------------------------------
CAnimationState::CAnimationState()
{
	_Sheet = NULL;
}// CAnimationState //


//-----------------------------------------------
// recursMarkTraverseNext
//-----------------------------------------------
void CAnimationState::recursMarkTraverseNext(sint idAnim, std::vector<bool> &traversedAnims, bool rootCall)
{
	if(idAnim<0 || idAnim>=(sint)_Animations.size())
		return;

	// if this animation has already been traversed, then don't need to recurse (already done)
	if(traversedAnims[idAnim])
		return;

	// mark this anim as traversed (if not from rootcall)
	if(!rootCall)
		traversedAnims[idAnim]= true;

	// mark, and traverse recurs
	CAnimation &anim= _Animations[idAnim];
	const std::vector<sint8>	&nextAnim= anim.getNextAnimList();
	for(uint i=0;i<nextAnim.size();i++)
		recursMarkTraverseNext(nextAnim[i], traversedAnims, false);
}

//-----------------------------------------------
// init
//-----------------------------------------------
void CAnimationState::init(CAnimationStateSheet *sheet, NL3D::UAnimationSet *animationSet)
{
	uint32 i;

	_Sheet = sheet;

	// **** init if needed _FallBackToDefaultRace
	if(!_FallBackToDefaultRaceInited)
		initFallBackToDefaultRace();

	// **** build animation list
	_Animations.resize(sheet->Animations.size());
	for (i = 0; i < _Animations.size(); ++i)
	{
		_Animations[i].init(&sheet->Animations[i], animationSet);
	}

	// **** build list of root animation
	// flag each animation to know if it is reachable through "NextAnim" system
	std::vector<bool> traversedAnims;
	traversedAnims.clear();
	traversedAnims.resize(_Animations.size(), false);
	for (i = 0; i < _Animations.size(); ++i)
	{
		recursMarkTraverseNext(i, traversedAnims, true);
	}
	// Build the final list of root animaions
	_RootAnimations.clear();
	for (i = 0; i < _Animations.size(); ++i)
	{
		// root animations are not reachable. NB: also consider always the 0th animation as a root one
		if(i==0 || !traversedAnims[i])
			_RootAnimations.push_back(i);
	}

}// init//

//-----------------------------------------------
// buildAnimFilter
//-----------------------------------------------
void CAnimationState::buildAnimFilter(vector<uint> &filteredRootAnimList, vector<bool>	&animFilterStates, uint32 jobSpecialisation, EGSPD::CPeople::TPeople race, GSGENDER::EGender gender) const
{
	uint i;

	// If the user doesn't want Racial Animation, force player race according to old animations
	// Plus force old race animation for some anim/race/gender case
	if(!ClientCfg.EnableRacialAnimation || isOldRaceAnimationForced(race, gender) )
	{
		// avoid problem with Gender= Neutral (beast).
		uint	uGender= gender;
		uint	uState= state();
		NLMISC::clamp(uGender, 0U, 1U);
		NLMISC::clamp(uState, 0U, uint(CAnimationStateSheet::StaticStateCount-1));
		// According to the state, and the sex of user, the choice may differ
		race= _FallBackToDefaultRace[uGender][uState];
	}

	// Mark each animation if ok or not
	animFilterStates.resize(_Animations.size());
	for(i=0;i<_Animations.size();i++)
	{
		animFilterStates[i]= _Animations[i].filterOk(jobSpecialisation, race);
	}

	// build list of filtered root animation
	filteredRootAnimList.clear();
	for(i=0;i<_RootAnimations.size();i++)
	{
		uint	idAnim= _RootAnimations[i];
		// if this root animation is filtered, add it
		if(idAnim<_Animations.size() && animFilterStates[idAnim])
			filteredRootAnimList.push_back(idAnim);
	}

}// buildAnimFilter //

//-----------------------------------------------
// chooseAnimationIndex :
// Choose a valid animation index in state.
// (there are more chances for the first animation)
// \warning This method does not check if _Animations is empty.
//-----------------------------------------------
uint CAnimationState::chooseAnimationIndex(const vector<uint> &filteredRootAnimList) const
{
	// error, none match filter, fallback to the first one
	if(filteredRootAnimList.empty())
		return 0;

	// 1 chance by 2 to choose the first animation
	if(filteredRootAnimList.size()==1 || (rand()%2))
		return filteredRootAnimList[0];
	else
		return filteredRootAnimList[rand()%filteredRootAnimList.size()];
}// chooseAnimationIndex //

//-----------------------------------------------
// chooseAnim :
// Choose an animation in the list.
// \return TAnimId : Id of the animation.
//-----------------------------------------------
CAnimation::TAnimId CAnimationState::chooseAnim(uint32 jobSpecialisation, EGSPD::CPeople::TPeople race, GSGENDER::EGender gender, double angToDest, CAnimation::TAnimId currentAnimIndex) const
{
	// If there is not animation return CAnimation::UnknownAnim.
	if(_Animations.empty())
		return CAnimation::UnknownAnim;

	// The animation to choose is not a rotation, no need to choose according to an angle.
	if(angToDest == 0.0)
	{
		// static to avoid reallocation each time
		static	vector<uint>	filteredRootAnimList;
		static	vector<bool>	animFilterStates;
		buildAnimFilter(filteredRootAnimList, animFilterStates, jobSpecialisation, race, gender);

		// Do not check if there is a sequence
		if(currentAnimIndex == CAnimation::UnknownAnim)
			return (CAnimation::TAnimId) chooseAnimationIndex(filteredRootAnimList);
		// Check the current animation index given is valid.
		if((uint)currentAnimIndex >= _Animations.size())
		{
			nlwarning("CAnimationState:chooseAnim: currentAnimIndex(%d) >= size", currentAnimIndex);
			return (CAnimation::TAnimId) chooseAnimationIndex(filteredRootAnimList);
		}
		// Get the next animation to play from the animation given.
		sint8 nextAnim = _Animations[(uint)currentAnimIndex].getNextAnim(animFilterStates);
		// Check the is a next animation defined
		if(nextAnim >= 0)
		{
			// Check the next animation is valid
			if((uint)nextAnim<_Animations.size())
				return (CAnimation::TAnimId) nextAnim;
			else
			{
				nlwarning("CAnimationState:chooseAnim: next animation index(%d) is invalid", nextAnim);
				return (CAnimation::TAnimId) chooseAnimationIndex(filteredRootAnimList);
			}
		}
		else
			return (CAnimation::TAnimId) chooseAnimationIndex(filteredRootAnimList);
	}
	// This is a rotation.
	else
	{
		uint i;
		uint best = 0;
		const uint count = (uint)_Animations.size ();
		double bestAng = 1000.0;	// Big value to be > to the first element.
		for (i=0; i<count; i++)
		{
			const CAnimation &anim = _Animations[i];
			double angTmp;
			if(anim.virtualRot() != 0.0)
				angTmp = fabs(fabs(angToDest)-anim.virtualRot());
			else
				angTmp = fabs(fabs(angToDest)-anim.getRot());
			if(angTmp < bestAng)
			{
				bestAng = angTmp;
				best = i;
			}
		}

		// Return the id for the closest animation for this angle.
		return (CAnimation::TAnimId)best;
	}
}// chooseAnim //

//-----------------------------------------------
// getAnimationByIndex
//-----------------------------------------------
CAnimation *CAnimationState::getAnimationByIndex(uint index)
{
	if (index >= _Animations.size()) return NULL;
	return &_Animations[index];
}// getAnimationByIndex