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

// ***************************************************************************
/*
	This small tool was made for Graphist, to edit automatically *.animation_set sheets, so anims
	get correct MeleeImpactDelay data

	It uses an anim.txt file that was generated by inserting code (give at end of this file)
	in the client.

	This tool had to be "used one time only". hence its crappiest usage :)
*/


#include "nel/misc/path.h"
#include "nel/misc/file.h"
#include "nel/misc/common.h"
#include "nel/misc/algo.h"


using namespace std;
using namespace NLMISC;


// ***************************************************************************
// Config
bool	ReplaceExistingMeleeImpactDelay= true;
float	MeleeImpactTimeFactor= 0.35f;

// Inited in main()
map<string, string>		StateNameToStateCode;


// ***************************************************************************
class CAnimCombatState
{
public:
	void	build(const string & line);

	// A1, A2 etc...
	string	StateCode;
	// Mean Animation Time of all sub animations of this state
	float	MeanAnimTime;

public:
	bool	operator<(const CAnimCombatState &o) const {return StateCode<o.StateCode;}
};

class CAnimCombatSet
{
public:
	// name of the anim set
	string						Name;

	// set of CAnimCombatState
	set<CAnimCombatState>		States;

public:
	bool	operator<(const CAnimCombatSet &o) const {return Name<o.Name;}
};


// ***************************************************************************
void CAnimCombatState::build(const string &line)
{
	StateCode= line.substr(4, 2);
	string time= line.substr(10, 5);
	NLMISC::fromString(time, MeanAnimTime);
}


// ***************************************************************************
void	makeAnimMeleeImpact(const std::string &animSetFile, const set<CAnimCombatSet> &combatAnimSets)
{
	// look if this animSetFile is in the combat list to patch
	string	shortName= NLMISC::toLower(CFile::getFilenameWithoutExtension(animSetFile));
	CAnimCombatSet	key;
	key.Name= shortName;
	set<CAnimCombatSet>::const_iterator	it= combatAnimSets.find(key);
	if(it == combatAnimSets.end())
		return;

	const CAnimCombatSet	&currentCombatAnimSet= *it;

	InfoLog->displayRawNL("patching %s", animSetFile.c_str());


	// *** Read the animset file.
	CIFile	iFile;
	iFile.open(animSetFile, true);
	// Read all text
	static vector<string>	animSetText;
	animSetText.clear();
	while(!iFile.eof())
	{
		char	tmp[50000];
		iFile.getline(tmp, 50000);
		animSetText.push_back(tmp);
	}
	iFile.close();


	bool	someChangeDone= false;

	// *** Parse the animSet
	{
		// For each line of the animSet
		sint	structLevel= 0;
		sint	meleeImpactDelayLine= -1;
		string	currentStateName;
		for(uint j=0;j<animSetText.size();j++)
		{
			string	line= animSetText[j];
			string	lineLwr= toLower(line);

			// Find <LOG> TAg? => stop
			if(line.find("<LOG>")!=string::npos)
				break;

			// Find a STRUCT start?
			if(line.find("<STRUCT")!=string::npos)
			{
				// inc struct
				structLevel++;

				// if start a new State block
				if(structLevel==2)
				{
					// reset info for this state
					currentStateName.clear();
					meleeImpactDelayLine= -1;
					
					// try to get the name
					const string	tagStart= "name=\"";
					std::string::size_type	start= lineLwr.find(tagStart);
					if(start!=string::npos)
					{
						start+= tagStart.size();
						std::string::size_type	end= lineLwr.find("\"", start);
						if(end!=string::npos)
							currentStateName= lineLwr.substr(start, end-start);
					}
				}
			}

			// Find a STRUCT end?
			if(line.find("</STRUCT>")!=string::npos)
			{
				// if end a state block, may add or replace MeleeDelayImpact
				if(structLevel==2 && !currentStateName.empty())
				{
					// If the state is not in the combat state, no need to patch anything
					static CAnimCombatState		key;
					// must translate for instance "attack1" to "A1"
					key.StateCode= StateNameToStateCode[currentStateName];
					set<CAnimCombatState>::const_iterator		it= currentCombatAnimSet.States.find(key);
					if(it!=currentCombatAnimSet.States.end())
					{
						// else take the mean anim time
						string	format= "      <ATOM Name=\"MeleeImpactDelay\" Value=\"%.3f\"/>";
						string	newLine= toString(format.c_str(), it->MeanAnimTime * MeleeImpactTimeFactor);

						// melee impact delay doesn't exist?
						if(meleeImpactDelayLine==-1)
						{
							// add just before this line the Melee Impact Atom
							animSetText.insert(animSetText.begin()+j, newLine);
							j++;
							someChangeDone= true;
						}
						// else exist and want to replace?
						else if(ReplaceExistingMeleeImpactDelay)
						{
							animSetText[meleeImpactDelayLine]= newLine;
							someChangeDone= true;
						}
					}
				}

				// dec struct level
				structLevel--;
			}
				
			// if we are in level 2 structure, try to get the line to modify (if exist)
			if(structLevel==2)
			{
				if( line.find("Name=\"MeleeImpactDelay\"")!=string::npos )
					meleeImpactDelayLine= j;
			}
		}
	}

	// *** Write the animset file.
	if(someChangeDone)
	{
		COFile	oFile;
		oFile.open(animSetFile, false, true);
		// Write all text
		for(uint i=0;i<animSetText.size();i++)
		{
			string	str= animSetText[i];
			str+= "\n";
			oFile.serialBuffer((uint8*)str.c_str(), (uint)str.size());
		}
	}
}


// ***************************************************************************
int usage()
{
	printf("Usage: make_anim_melee_impact animset_dir");
	return -1;
}


// ***************************************************************************
int main(int argc, char *argv[])
{
	NLMISC::createDebug();

	// make_anim_melee_impact animset_dir
	if(argc!=2)
		return usage();
	string animSetDir= argv[1];

	// *** parse the anim.txt file
	set<CAnimCombatSet>		combatAnimSets;
	CIFile	animFile;
	if(!animFile.open("anim.txt", true))
	{
		nlwarning("Can't open anim.txt file. abort");
		return 0;
	}
	else
	{
		char	tmp[5000];
		CAnimCombatSet	lastAnimSet;
		// parse all lines
		while(!animFile.eof())
		{
			animFile.getline(tmp, 5000);
			string	line= tmp;
			if(line.empty())
				continue;

			// new anim set?
			if(line[0]!=' ')
			{
				// insert the last anim state
				if(!lastAnimSet.States.empty())
					combatAnimSets.insert(lastAnimSet);
				lastAnimSet.States.clear();
				lastAnimSet.Name= line;
			}
			// new anim state?
			else if(!lastAnimSet.Name.empty())
			{
				CAnimCombatState	state;
				state.build(line);
				lastAnimSet.States.insert(state);
			}
		}

		// append the last anim set if needed
		if(!lastAnimSet.States.empty())
			combatAnimSets.insert(lastAnimSet);

		animFile.close();
	}
	
	// *** Get the list of .animset to make by race
	vector<string>	files;
	files.clear();
	CPath::getPathContent(animSetDir, true, false, true, files);
	vector<string>	animSetList;
	InfoLog->displayRawNL("");
	InfoLog->displayRawNL("*****************************");
	InfoLog->displayRawNL("**** .animation_set list ****");
	InfoLog->displayRawNL("*****************************");
	for(uint i=0;i<files.size();i++)
	{
		if(testWildCard(files[i], "*.animation_set"))
		{
			animSetList.push_back(files[i]);
			InfoLog->displayRawNL(animSetList.back().c_str());
		}
	}
	
	// *** Init StateNameToStateCode
	StateNameToStateCode["attack1"]= "A1";
	StateNameToStateCode["attack2"]= "A2";
	StateNameToStateCode["walk atk"]= "Wa";
	StateNameToStateCode["run atk"]= "Ra";
	StateNameToStateCode["backward atk"]= "Ba";
	StateNameToStateCode["default atk low"]= "Dl";
	StateNameToStateCode["default atk middle"]= "Dm";
	StateNameToStateCode["default atk high"]= "Dh";
	StateNameToStateCode["powerful atk low"]= "Pl";
	StateNameToStateCode["powerful atk middle"]= "Pm";
	StateNameToStateCode["powerful atk high"]= "Ph";
	StateNameToStateCode["area atk low"]= "Al";
	StateNameToStateCode["area atk middle"]= "Am";
	StateNameToStateCode["area atk high"]= "Ah";

	
	// *** For each animset, test if can replace some anim
	InfoLog->displayRawNL("");
	InfoLog->displayRawNL("**************************");
	InfoLog->displayRawNL("**** Starting Process ****");
	InfoLog->displayRawNL("**************************");
	for(uint i=0;i<animSetList.size();i++)
	{
		makeAnimMeleeImpact(animSetList[i], combatAnimSets);
	}

	return 0;
}




// ***************************************************************************
// ***************************************************************************
// ***************************************************************************
// ***************************************************************************

/*
To generate the anim.txt file, this code has to be inserted in the client, in
	entity_animation_manager.cpp, CEntityAnimationManager::load(), after


	if(_AnimationSet)
	{
		_AnimationSet->build();
		....
	}

	** insert here ***


  animSet Sheets and 3D anim data (fauna_animations.bnp and characters_animations.bnp) must be up to date
*/


/*

	// *************************************
	// CODE TO GENERATE MELEE IMPACT DELAY
	// *************************************
	CFileDisplayer	animLog("anim.txt", true);
	TAnimStateId	walkAtk= CAnimationStateSheet::getAnimationStateId("walk atk");
	TAnimStateId	runAtk= CAnimationStateSheet::getAnimationStateId("run atk");
	TAnimStateId	backAtk= CAnimationStateSheet::getAnimationStateId("backward atk");
	TAnimStateId	stateCombat[]= {CAnimationStateSheet::Attack1, CAnimationStateSheet::Attack2,
		walkAtk, runAtk, backAtk,
		CAnimationStateSheet::DefaultAtkLow,CAnimationStateSheet::DefaultAtkHigh,
		CAnimationStateSheet::DefaultAtkMiddle,CAnimationStateSheet::PowerfulAtkLow,
		CAnimationStateSheet::PowerfulAtkHigh,CAnimationStateSheet::PowerfulAtkMiddle,
		CAnimationStateSheet::AreaAtkLow,CAnimationStateSheet::AreaAtkHigh,
		CAnimationStateSheet::AreaAtkMiddle};
	string			stateCombatCode[]= {"A1", "A2", "Wa", "Ra", "Ba", 
		"Dl", "Dh", "Dm", "Pl", "Ph", "Pm", "Al", "Ah", "Am"};
	const uint32	nSC= sizeof(stateCombat) / sizeof(stateCombat[0]);
	nlctassert(nSC==sizeof(stateCombatCode) / sizeof(stateCombatCode[0]));
	{
		float	roughEval= 0.f;
		uint	nbRoughEval= 0;
		TAnimSet::iterator	it= _AnimSet.begin();
		for(;it!=_AnimSet.end();it++)
		{
			CAnimationSet	&animSet= it->second;
			bool			animSetDisplayed= false;
			for(uint i=0;i<nSC;i++)
			{
				// per anim state
				CAnimationState *state= const_cast<CAnimationState*>(animSet.getAnimationState(stateCombat[i]));
				if(state)
				{
					// first compute mean and anim name
					float	mean= 0.f;
					uint	nbValid= 0;
					string	animName;
					bool	extended= false;
					for(uint j=0;j<state->getNumAnimation();j++)
					{
						CAnimation			*anim= state->getAnimationByIndex(j);
						NL3D::UAnimation	*anim3d= NULL;
						if(anim)
							anim3d= _AnimationSet->getAnimation(anim->id());
						if(anim && anim3d)
						{
							// name
							string	name= NLMISC::toLower(_AnimationSet->getAnimationName(anim->id()));
							if(animName.empty())
								animName= name;
							else if(!extended)
							{
								extended= true;
								animName+= ", ...";
							}

							// meanLength and nb
							float	timeLen= anim3d->getEndTime()-anim3d->getBeginTime();
							mean+= timeLen;
							nbValid++;
						}
					}
					if(nbValid)
						mean/=nbValid;

					// compute standard and max deviation
					float	stdDev=0.f, maxDev=0.f;
					for(uint j=0;j<state->getNumAnimation();j++)
					{
						CAnimation *anim= state->getAnimationByIndex(j);
						NL3D::UAnimation	*anim3d= NULL;
						if(anim)
							anim3d= _AnimationSet->getAnimation(anim->id());
						if(anim && anim3d)
						{
							float	timeLen= anim3d->getEndTime()-anim3d->getBeginTime();
							stdDev+= (float)fabs(timeLen - mean);
							maxDev= max(maxDev, (float)fabs(timeLen - mean));
						}
					}
					if(nbValid)
						stdDev/= nbValid;

					// valid?
					if(nbValid)
					{
						// display first animSetName
						if(!animSetDisplayed)
						{
							string	msg= toString("%s\n", it->first.c_str() );
							animLog.display(CLog::TDisplayInfo(), msg.c_str());
							animSetDisplayed= true;
						}

						// then stats for this state
						string	msg= toString("    %s: mn%.03f, md%.03f, sd%.03f, ev%.03f (%s)\n", stateCombatCode[i].c_str(),
							mean, maxDev, stdDev, mean*0.4f, animName.c_str());
						animLog.display(CLog::TDisplayInfo(), msg.c_str());

						roughEval+= mean;
						nbRoughEval++;
					}
				}
			}
		}
		if(nbRoughEval)
		{
			roughEval/= nbRoughEval;
			nlinfo("    AnimDBG RoughEval: mn%.03f, ev%.03f", 
				roughEval, roughEval*0.4f);
		}
	}
	// *************************************
	// CODE TO GENERATE MELEE IMPACT DELAY
	// *************************************
	

*/