// NeL - MMORPG Framework <http://dev.ryzom.com/projects/nel/>
// 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/>.

/*

	Script commands:
		OUTPUT <output file name>
		FIELD <field name>
		SOURCE <field name>
		SCANFILES <extension>


*/


// Misc
#include "nel/misc/types_nl.h"
#include "nel/misc/path.h"
#include "nel/misc/file.h"
#include "nel/misc/smart_ptr.h"
#include "nel/misc/command.h"
#include "nel/misc/path.h"
//#include "nel/memory/memory_manager.h"
#include "nel/misc/i18n.h"
#include "nel/misc/sstring.h"
#include "nel/misc/algo.h"
// Georges
#include "nel/georges/u_form.h"
#include "nel/georges/u_form_elm.h"
#include "nel/georges/u_form_dfn.h"
#include "nel/georges/u_form_loader.h"
#include "nel/georges/load_form.h"
// Georges, bypassing interface
#include "../../../src/georges/stdgeorges.h"
#include "../../../src/georges/form.h"

// Basic C++
#include <iostream>
//#include <conio.h>
#include <stdio.h>
#include <limits>
//#include <io.h>

// stl
#include <map>

using namespace NLMISC;
using namespace std;
using namespace NLGEORGES;


/*
	some handy prototypes
*/
void setOutputFile(char *);
void addField(char *);
void addSource(char *);
void scanFiles(std::string extension);
void executeScriptFile(const string &);

/*
	Some globals
*/
FILE *Outf = NULL;

class CField
{
public:
	std::string _name;
//	bool _evaluated;
	UFormElm::TEval	_evaluated;
	CField(const std::string &name, UFormElm::TEval eval) 
		: _name(name), _evaluated(eval)
	{ }
};
std::vector<CField> fields;
std::vector<std::string> files;

vector<string>		inputScriptFiles;
vector<string>		inputCsvFiles;
vector<string>		inputSheetPaths;
bool				inputSheetPathLoaded = false;
map<string, string>	inputSheetPathContent;

const char	*SEPARATOR = ";";
const char	*ARRAY_SEPARATOR = "|";


class	CDfnField
{
public:

	explicit	CDfnField	(const std::string	&name)	:	_isAnArray(false), _name(name)
	{}

	CDfnField	(const std::string	&name, const bool &isAnArray)	:	_isAnArray(isAnArray), _name(name)
	{}

	virtual	~CDfnField	()
	{}

	bool	operator	<(const CDfnField &other)	const
	{
		return	_name<other._name;
	}

	bool	operator	==(const CDfnField &other)	const
	{
		return	_name==other._name;
	}

	const	std::string	&getName		()	const
	{
		return	_name;
	}

	const	bool	&isAnArray	()	const
	{
		return	_isAnArray;
	}

private:
	bool		_isAnArray;
	std::string	_name;
};


/** Replace _false_ and _true_ with true and false
 *	this is used because excell force true and false in
 *	uppercase when is save the file in cvs mode.
 */
void replaceTrueAndFalseTagFromCsv(vector<string> &args)
{
	for (uint i=0; i<args.size(); ++i)
	{
		CSString str = args[i];

		str = str.replace("_false_", "false");
		str = str.replace("_true_", "true");

		args[i] = str;
	}
}

/** Replace false and true with _false_ and _true_
 *	this is used because excell force true and false in
 *	uppercase when is save the file in cvs mode.
 *	NB : this do the opposite jobs of the previous function
 */
void replaceTrueAndFalseTagToCsv(string &arg)
{
	CSString str = arg;

	str.replace("false", "_false_");
	str.replace("true", "_true_");

	arg = str;
}


/*
	Some routines for dealing with script input
*/
void setOutputFile(const CSString &filename)
{
	if (Outf!=NULL)
		fclose(Outf);
	Outf=fopen(filename.c_str(), "wt");
	if (Outf == NULL)
	{
		fprintf(stderr, "Can't open output file '%s' ! aborting.", filename.c_str());
		getchar();
		exit(1);
	}
	fields.clear();
}

void addField(const CSString &name)
{
	fields.push_back(CField(name, UFormElm::Eval));
}

void addSource(const CSString &name)
{
	fields.push_back(CField(name, UFormElm::NoEval));
}

void buildFileVector(std::vector<std::string> &filenames, const std::string &filespec)
{
	uint i,j;
	// split up the filespec into chains
	CSString filters = filespec;
	filters.strip();
	std::vector<std::string> in, out;

	while (!filters.empty())
	{
		CSString filter = filters.strtok(" \t");
		if (filter.empty())
			continue;

		switch (filter[0])
		{
		case '+':
			in.push_back(filter.leftCrop(1)); break;
			break;
		case '-':
			out.push_back(filter.leftCrop(1)); break;
			break;
		default:
			fprintf(stderr,"Error in '%s' : filter must start with '+' or '-'\n",
				filter.c_str());
			getchar(); 
			exit(1);
		}
	}

/*	for (i=0;i<filespec.size();)
	{
		for (j=i;j<filespec.size() && filespec[j]!=' ' && filespec[j]!='\t';j++) {}
		switch(filespec[i])
		{
		case '+': 
			in.push_back(filespec.substr(i+1,j-i-1)); break;
		case '-': 
			out.push_back(filespec.substr(i+1,j-i-1)); break;
		default: 
			fprintf(stderr,"Filter must start with '+' or '-'\n",&(filespec[i])); getchar(); exit(1);
		}
		i=j;
		while (i<filespec.size() && (filespec[i]==' ' || filespec[i]=='\t')) i++; // skip white space
	}
*/
	// use the filespec as a filter while we build the sheet file vector
	for (i=0;i<files.size();i++)
	{
		bool ok=true;

		// make sure the filename includes all of the include strings
		for (j=0;j<in.size() && ok;j++)
		{
			if (!testWildCard(CFile::getFilename(files[i]), in[j]))
			{
				ok=false;
			}
		}

		// make sure the filename includes none of the exclude strings
		for (j=0;j<out.size() && ok;j++)
		{
			if (testWildCard(CFile::getFilename(files[i]), out[j]))
			{
				ok=false;
			}
		}

		// if the filename matched all of the above criteria then add it to the list
		if (ok)
		{
			printf("Added: %s\n",CFile::getFilename(files[i]).c_str());
			filenames.push_back(files[i]);
		}
	}
	printf("Found: %zu matching files (from %zu)\n",filenames.size(),files.size());

}


void	addQuotesRoundString	(std::string	&valueString)
{
	// add quotes round strings
	std::string hold=valueString;
	valueString.erase();
	valueString='\"';
	for (unsigned i=0;i<hold.size();i++)
	{
		if (hold[i]=='\"')
			valueString+="\"\"";
		else
			valueString+=hold[i];
	}
	valueString+='\"';	
}

void	setErrorString	(std::string	&valueString, const UFormElm::TEval &evaluated, const UFormElm::TWhereIsValue	&where)
{
	if	(evaluated==UFormElm::NoEval)
	{
		switch(where)
		{
		case UFormElm::ValueForm:			valueString="ValueForm"; break;
		case UFormElm::ValueParentForm:		valueString="ValueParentForm"; break;
		case UFormElm::ValueDefaultDfn:		valueString="ValueDefaultDfn"; break;
		case UFormElm::ValueDefaultType:	valueString="ValueDefaultType"; break;
		default: valueString="ERR";
		}
		
	}
	else
	{
		valueString="ERR";
	}
	
}



/*
	Scanning the files ... this is the business!!
*/
void scanFiles(const CSString &filespec)
{
	std::vector<std::string> filenames;

	buildFileVector(filenames, filespec);

	// if there s no file, nothing to do
	if (filenames.empty())
		return;

	// display the table header line
	fprintf(Outf,"FILE");
	for (unsigned i=0;i<fields.size();i++)
		fprintf(Outf,"%s%s",SEPARATOR, fields[i]._name.c_str());
	fprintf(Outf,"\n");

	UFormLoader *formLoader = NULL;
	NLMISC::TTime last = NLMISC::CTime::getLocalTime ();
	NLMISC::TTime start = NLMISC::CTime::getLocalTime ();

	NLMISC::CSmartPtr<UForm> form;


	for (uint j = 0; j < filenames.size(); j++)
	{
		if	(NLMISC::CTime::getLocalTime () > last + 5000)
		{
			last = NLMISC::CTime::getLocalTime ();
			if	(j>0)
			{
				nlinfo ("%.0f%% completed (%d/%d), %d seconds remaining", (float)j*100.0/filenames.size(),j,filenames.size(), (filenames.size()-j)*(last-start)/j/1000);
			}

		}

		//std::string p = NLMISC::CPath::lookup (filenames[j], false, false);
		std::string p = filenames[j];
		if (p.empty()) continue;

		// create the georges loader if necessary
		if (formLoader == NULL)
		{
			WarningLog->addNegativeFilter("CFormLoader: Can't open the form file");
			formLoader = UFormLoader::createLoader ();
		}

		// Load the form with given sheet id
//		form = formLoader->loadForm (sheetIds[j].toString().c_str ());
		form = formLoader->loadForm (filenames[j].c_str ());
		if	(form)
		{
			// the form was found so read the true values from George
//			std::string s;
			fprintf(Outf,"%s",CFile::getFilenameWithoutExtension(filenames[j]).c_str());
			for	(unsigned i=0;i<fields.size();i++)
			{
				UFormElm::TWhereIsValue where;
				UFormElm	*fieldForm=NULL;
				std::string	valueString;
				
				form->getRootNode ().getNodeByName(&fieldForm, fields[i]._name.c_str());

				if	(fieldForm)
				{
					if	(fieldForm->isArray())	//	if its an array
					{
						uint	arraySize=0,arrayIndex=0;
						fieldForm->getArraySize(arraySize);
						while (arrayIndex<arraySize)
						{
							if	(fieldForm->getArrayValue(valueString,arrayIndex,fields[i]._evaluated, &where))
								;//addQuotesRoundString	(valueString);
							else
								setErrorString	(valueString, fields[i]._evaluated, where);

							arrayIndex++;
							if (arrayIndex<arraySize)	//	another value in the array..
								valueString+=ARRAY_SEPARATOR;
						}

					}
					else
					{
						if	(form->getRootNode ().getValueByName(valueString,fields[i]._name.c_str(),fields[i]._evaluated,&where))	//fieldForm->getValue(valueString,fields[i]._evaluated))
							;//addQuotesRoundString	(valueString);
						else
							setErrorString	(valueString, fields[i]._evaluated, where);
					}
				}
//				else	//	node not found.
//				{
//					setErrorString	(valueString, fields[i]._evaluated, where);
//				}

				replaceTrueAndFalseTagToCsv(valueString);

				fprintf(Outf,"%s%s", SEPARATOR, valueString.c_str());
				
//				UFormElm::TWhereIsValue where;
//
//				bool result=form->getRootNode ().getValueByName(s,fields[i]._name.c_str(),fields[i]._evaluated,&where);
//				if (!result)
//				{
//					if (fields[i]._evaluated)
//					{
//						s="ERR";
//					}
//					else
//					{
//						switch(where)
//						{
//						case UFormElm::ValueForm: s="ValueForm"; break;
//						case UFormElm::ValueParentForm: s="ValueParentForm"; break;
//						case UFormElm::ValueDefaultDfn: s="ValueDefaultDfn"; break;
//						case UFormElm::ValueDefaultType: s="ValueDefaultType"; break;
//						default: s="ERR";
//						}
//						
//					}
//					
//				}
//				else
//				{
//					// add quotes round strings
//					std::string hold=s;
//					s.erase();
//					s='\"';
//					for (unsigned i=0;i<hold.size();i++)
//					{
//						if (hold[i]=='\"')
//							s+="\"\"";
//						else
//							s+=hold[i];
//					}
//					s+='\"';
//				}
//				fprintf(Outf,"%s%s", SEPARATOR, s);
			}
			fprintf(Outf,"\n");
		}

	}

	// free the georges loader if necessary
	if (formLoader != NULL)
	{
		UFormLoader::releaseLoader (formLoader);
		WarningLog->removeFilter ("CFormLoader: Can't open the form file");
	}

	// housekeeping
//	sheetIds.clear ();
	filenames.clear ();

	fields.clear();
}


//void executeScriptBuf(char *txt)
void executeScriptBuf(const string &text)
{
	CSString buf = text;
	CVectorSString	lines;

	vector<string>	tmpLines;
	NLMISC::explode(std::string(buf.c_str()), std::string("\n"), tmpLines, true);
	lines.resize(tmpLines.size());
	for (uint i=0; i<tmpLines.size();i++)
	{
		lines[i]= tmpLines[i];
	}

	for (uint i=0; i<lines.size(); ++i)
	{
		CSString line = lines[i];
		line = line.strip();
		if (line.empty() || line.find("//") == 0)
		{
			// comment or empty line, skip
			continue;
		}
		CSString command = line.strtok(" \t");
		line = line.strip();

		
		if (command == "DFNPATH")
		{
			//CPath::getPathContent(args,true,false,true,files);
			CPath::addSearchPath(line, true, false); // for the dfn files
		}
		else if (command == "PATH")
		{
			files.clear();
			CPath::getPathContent(line, true,false,true,files);
			CPath::addSearchPath(line, true, false); // for the dfn files
		}
		else if (command == "OUTPUT")
		{
			setOutputFile(line);
		}
		else if (command == "FIELD")
		{
			addField(line);
		}
		else if (command == "SOURCE")
		{
			addSource(line);
		}
		else if (command == "SCANFILES")
		{
			scanFiles(line);
		}
		else if (command == "SCRIPT")
		{
			executeScriptFile(line);
		}
		else
		{
			fprintf(stderr,"Unknown command: '%s' '%s'\n", command.c_str(), line.c_str());
		}
	}

	
}

void executeScriptFile(const string &filename)
{
	ucstring	temp;
	CI18N::readTextFile(filename, temp, false, false, false);

	if (temp.empty())
	{
		fprintf(stderr, "the field '%s' is empty.\n", filename.c_str());
		return;
	}
	string buf = temp.toString();

	executeScriptBuf(buf);
}

void	loadSheetPath()
{
	if (inputSheetPathLoaded)
		return;

	NLMISC::createDebug();
	NLMISC::WarningLog->addNegativeFilter( "CPath::insertFileInMap" );

	vector<string> files;
	vector<string> pathsToAdd;
	for (uint i=0; i<inputSheetPaths.size(); ++i)
	{
		explode( inputSheetPaths[i], std::string("*"), pathsToAdd );
		for ( vector<string>::const_iterator ip=pathsToAdd.begin(); ip!=pathsToAdd.end(); ++ip )
		{
			CPath::addSearchPath( *ip, true, false );
			CPath::getPathContent( *ip, true, false, true, files );
		}
	}
	
	uint	i;
	for (i=0; i<files.size(); ++i)
	{
		string& filename = files[i];
//		string& filebase = CFile::getFilenameWithoutExtension(filename);
		const string& filebase = CFile::getFilename(filename);
		inputSheetPathContent[filebase] = filename;
	}

	inputSheetPathLoaded = true;
}


/*
 *
 */
void fillFromDFN( UFormLoader *formLoader, set<CDfnField>& dfnFields, UFormDfn *formDfn, const string& rootName, const string& dfnFilename )
{
	uint i;
	for ( i=0; i!=formDfn->getNumEntry(); ++i )
	{
		string entryName, rootBase;
		formDfn->getEntryName( i, entryName );
		rootBase = rootName.empty() ? "" : (rootName+".");

		UFormDfn::TEntryType entryType;
		bool array;
		formDfn->getEntryType( i, entryType, array );
		switch ( entryType )
		{
			case UFormDfn::EntryVirtualDfn:
			{
				CSmartPtr<UFormDfn> subFormDfn = formLoader->loadFormDfn( (entryName + ".dfn").c_str() );
				if ( ! subFormDfn )
					nlwarning( "Can't load virtual DFN %s", entryName.c_str() );
				else
					fillFromDFN( formLoader, dfnFields, subFormDfn, rootBase + entryName, entryName + ".dfn" );
				break;
			}
			case UFormDfn::EntryDfn:
			{
				UFormDfn *subFormDfn;
				if ( formDfn->getEntryDfn( i, &subFormDfn) )
				{
					string filename;
					formDfn->getEntryFilename( i, filename );
					fillFromDFN( formLoader, dfnFields, subFormDfn, rootBase + entryName, filename ); // recurse
				}
				break;
			}
			case UFormDfn::EntryType:
			{
				const	std::string	finalName(rootBase+entryName);
				dfnFields.insert( CDfnField(finalName, array) );
				//nlinfo( "DFN entry: %s (in %s)", (rootBase + entryName).c_str(), dfnFilename.c_str() );
				break;
			}
		}
	}
}


/*
 * Clear the form to reuse it (and all contents below node)
 */
void clearSheet( CForm *form, UFormElm* node )
{
	((CFormElm*)node)->clean();
	form->clean();
}


/*
 * - Remove CSV carriage returns.
 * - Ensure there is no non-ascii char (such as Excel's special blank crap), set them to ' '.
 */
void eraseCarriageReturnsAndMakeBlankNonAsciiChars( string& s )
{
	const char CR = '\n';
	string::size_type p = s.find( CR );
	while ( (p=s.find( CR )) != string::npos )
		s.erase( p, 1 );
	for ( p=0; p!=s.size(); ++p )
	{
		uint8& c = (uint8&)s[p]; // ensure the test is unsigned
		if ( c > 127 )
		{
			//nldebug( "Blanking bad char %u in '%s'", c, s.c_str() );
			s[p] = ' ';
		}
	}
}


string OutputPath;


/*
 * CSV -> Georges
 */
void	convertCsvFile( const string &file, bool generate, const string& sheetType )
{
	const uint			BUFFER_SIZE = 16*1024;
	char			lineBuffer[BUFFER_SIZE];
	FILE			*s;

	vector<string>	fields;
	vector<string>	args;

	if ((s = fopen(file.c_str(), "r")) == NULL)
	{
		fprintf(stderr, "Can't find file %s to convert\n", file.c_str());
		return;
	}

	if (!fgets(lineBuffer, BUFFER_SIZE, s))
	{
		nlwarning("fgets() failed");
		return;
	}

	loadSheetPath();

	UFormLoader *formLoader = UFormLoader::createLoader ();
	NLMISC::CSmartPtr<CForm> form;
	NLMISC::CSmartPtr<UFormDfn> formDfn;

	explode(std::string(lineBuffer), std::string(SEPARATOR), fields);

	vector<bool> activeFields( fields.size(), true );

	// Load DFN (generation only)
	set<CDfnField>	dfnFields;
	if ( generate )
	{
		formDfn = formLoader->loadFormDfn( (sheetType + ".dfn").c_str() );
		if ( ! formDfn )
			nlerror( "Can't find DFN for %s", sheetType.c_str() );
		fillFromDFN( formLoader, dfnFields, formDfn, "", sheetType );

		// Display missing fields and check fields against DFN
		uint i;
		for ( i=1; i!=fields.size(); ++i )
		{
			eraseCarriageReturnsAndMakeBlankNonAsciiChars( fields[i] );
			if ( fields[i].empty() )
			{
				nlinfo( "Skipping field #%u (empty)", i );
				activeFields[i] = false;
			}
			else if ( nlstricmp( fields[i], "parent" ) == 0 )
			{
				fields[i] = toLower( fields[i] );
			}
			else
			{
				set<CDfnField>::iterator ist = dfnFields.find( CDfnField(fields[i]) );
				if ( ist == dfnFields.end() )
				{
					nlinfo( "Skipping field #%u (%s, not found in %s DFN)", i, fields[i].c_str(), sheetType.c_str() );
					activeFields[i] = false;
				}
			}
		}
		for ( i=1; i!=fields.size(); ++i )
		{
			if ( activeFields[i] )
				nlinfo( "Selected field: %s", fields[i].c_str() );
		}
	}

	string addExtension = "." + sheetType;
	uint dirmapLetterIndex = std::numeric_limits<uint>::max();
	bool dirmapLetterBackward = false;
	vector<string> dirmapDirs;
	string dirmapSheetCode;
	bool WriteEmptyProperties = false, WriteSheetsToDisk = true;
	bool ForceInsertParents = false;

	if ( generate )
	{
		// Get the directory mapping
		try
		{
			CConfigFile dirmapcfg;
			dirmapcfg.load( sheetType + "_dirmap.cfg" );

			if ( OutputPath.empty() )
			{
				CConfigFile::CVar *path = dirmapcfg.getVarPtr( "OutputPath" );
				if ( path )
					OutputPath = path->asString();
				if ( ! OutputPath.empty() )
				{
					if ( OutputPath[OutputPath.size()-1] != '/' )
						OutputPath += '/';
					else if ( ! CFile::isDirectory( OutputPath ) )
						nlwarning( "Output path does not exist" );
				}
			}

			CConfigFile::CVar *letterIndex1 = dirmapcfg.getVarPtr( "LetterIndex" );
			if ( letterIndex1 && letterIndex1->asInt() > 0 )
			{
				dirmapLetterIndex = letterIndex1->asInt() - 1;

				CConfigFile::CVar *letterWay = dirmapcfg.getVarPtr( "LetterWay" );
				dirmapLetterBackward = (letterWay && (letterWay->asInt() == 1));
				
				CConfigFile::CVar dirs = dirmapcfg.getVar( "Directories" );
				for ( uint idm=0; idm!=dirs.size(); ++idm )
				{
					dirmapDirs.push_back( dirs.asString( idm ) );
					nlinfo( "Directory: %s", dirmapDirs.back().c_str() );
					if ( ! CFile::isExists( OutputPath + dirmapDirs.back() ) )
					{
						CFile::createDirectory( OutputPath + dirmapDirs.back() );
					}
					else
					{
						if ( ! CFile::isDirectory( OutputPath + dirmapDirs.back() ) )
						{
							nlwarning( "Already existing but not a directory!" );
						}
					}
				}

				nlinfo( "Mapping letter #%u (%s) of sheet name to directory", dirmapLetterIndex + 1, dirmapLetterBackward?"backward":"forward" );
			}
			
			CConfigFile::CVar *sheetCode = dirmapcfg.getVarPtr( "AddSheetCode" );
			if ( sheetCode )
				dirmapSheetCode = sheetCode->asString();
			nlinfo( "Sheet code: %s", dirmapSheetCode.c_str() );

			if ( ! dirmapLetterBackward )
				dirmapLetterIndex += (uint)dirmapSheetCode.size();

			CConfigFile::CVar *wep = dirmapcfg.getVarPtr( "WriteEmptyProperties" );
			if ( wep )
				WriteEmptyProperties = (wep->asInt() == 1);
			nlinfo( "Write empty properties mode: %s", WriteEmptyProperties ? "ON" : "OFF" );

			CConfigFile::CVar *wstd = dirmapcfg.getVarPtr( "WriteSheetsToDisk" );
			if ( wstd )
				WriteSheetsToDisk = (wstd->asInt() == 1);
			nlinfo( "Write sheets to disk mode: %s", WriteSheetsToDisk ? "ON" : "OFF" );

			CConfigFile::CVar *fiparents = dirmapcfg.getVarPtr( "ForceInsertParents" );
			if ( fiparents )
				ForceInsertParents = (fiparents->asInt() == 1);
			nlinfo( "Force insert parents mode: %s", ForceInsertParents ? "ON" : "OFF" );
		}
		catch ( EConfigFile& e )
		{
			nlwarning( "Problem in directory mapping: %s", e.what() );
		}
		

		nlinfo( "Using output path: %s", OutputPath.c_str() );
		nlinfo( "Press a key to generate *.%s", sheetType.c_str() );
		getchar();
		nlinfo( "Generating...." );

	}
	else
		nlinfo("Updating modifications (only modified fields are updated)");

	set<string> newSheets;
	uint nbNewSheets = 0, nbModifiedSheets = 0, nbUnchangedSheets = 0, nbWritten = 0;
	while (!feof(s))
	{
		lineBuffer[0] = '\0';
		if (!fgets(lineBuffer, BUFFER_SIZE, s))
		{
			nlwarning("fgets() failed");
			break;
		}

		explode(std::string(lineBuffer), std::string(SEPARATOR), args);

		if (args.size() < 1)
			continue;

		eraseCarriageReturnsAndMakeBlankNonAsciiChars( args[0] );
		replaceTrueAndFalseTagFromCsv(args);

		// Skip empty lines
		if ( args[0].empty() || (args[0] == string(".")+sheetType) )
			continue;

		//nldebug( "%s: %u", args[0].c_str(), args.size() );
		string	filebase = dirmapSheetCode+args[0]; /*+"."+sheetType;*/
		if (filebase.find("."+sheetType) == string::npos)
		{
			filebase += "." + sheetType;
		}
		filebase = toLower(filebase);
		string	filename, dirbase;
		bool	isNewSheet=true;

		// Locate existing sheet
//		map<string, string>::iterator it = inputSheetPathContent.find( CFile::getFilenameWithoutExtension( filebase ) );
		map<string, string>::iterator it = inputSheetPathContent.find( CFile::getFilename( filebase ) );
		
		if	(it == inputSheetPathContent.end())
		{
			// Not found
			if ( ! generate )
			{
				if ( ! filebase.empty() )
				{
					nlwarning( "Sheet %s not found", filebase.c_str( )); 
					continue;
				}
			}
			else
			{
				// Load template sheet
				filename = toLower(filebase);
				form = (CForm*)formLoader->loadForm( (string("_empty.") + sheetType).c_str() );
				if (form == NULL)
				{
					nlerror( "Can't load sheet _empty.%s", sheetType.c_str() );
				}

				// Deduce directory from sheet name
				if ( dirmapLetterIndex != std::numeric_limits<uint>::max() )
				{
					if ( dirmapLetterIndex < filebase.size() )
					{
						uint letterIndex;
						char c;
						if ( dirmapLetterBackward )
							letterIndex = (uint)(filebase.size() - 1 - (CFile::getExtension( filebase ).size()+1)) - dirmapLetterIndex;
						else
							letterIndex = dirmapLetterIndex;
						c = tolower( filebase[letterIndex] );
						vector<string>::const_iterator idm;
						for ( idm=dirmapDirs.begin(); idm!=dirmapDirs.end(); ++idm )
						{
							if ( (! (*idm).empty()) && (tolower((*idm)[0]) == c) )
							{
								dirbase = (*idm) + "/";
								break;
							}
						}
						if ( idm==dirmapDirs.end() )
						{
							nlinfo( "Directory mapping not found for %s (index %u)", filebase.c_str(), letterIndex );
							dirbase = ""; // put into root
						}
					}
					else
					{
						nlerror( "Can't map directory with letter #%u, greater than size of %s + code", dirmapLetterIndex, filebase.c_str() );
					}
				}

				nlinfo( "New sheet: %s", filebase.c_str() );
				++nbNewSheets;
				if ( ! newSheets.insert( filebase ).second )
					nlwarning( "Found duplicate sheet: %s", filebase.c_str() );
				isNewSheet = true;
			}
		}
		else // an existing sheet was found
		{

			// Load sheet (skip if failed)
			dirbase = "";
			filename = (*it).second; // whole path
			form = (CForm*)formLoader->loadForm( filename.c_str() );
			if (form == NULL)
			{
				nlwarning( "Can't load sheet %s", filename.c_str() );
				continue;
			}

			isNewSheet = false;
		}

		const	UFormElm	&rootForm=form->getRootNode();
		bool displayed = false;
		bool isModified = false;
		uint i;
		for (	i=1;	i<args.size		()
					&&	i<fields.size	();	++i )
		{
			const string	&var = fields[i];
			string			&val = args[i];

			eraseCarriageReturnsAndMakeBlankNonAsciiChars( val );

			// Skip column with inactive field (empty or not in DFN)
			if ( (! activeFields[i]) )
				continue;

			// Skip setting of empty cell except if required
			if ( (! WriteEmptyProperties) && val.empty() )
				continue;

			// Special case for parent sheet
			if	(var == "parent") // already case-lowered
			{
				vector<string> parentVals;
				explode( val, std::string(ARRAY_SEPARATOR), parentVals );
				if ( (parentVals.size() == 1) && (parentVals[0].empty()) )
					parentVals.clear();

				if ( (isNewSheet || ForceInsertParents) && (! parentVals.empty()) )
				{
					// This is slow. Opti: insertParent() should have an option to do it without loading the form
					//	parent have same type that this object (postulat).
					uint	nbinsertedparents=0;

					for ( uint p=0; p!=parentVals.size(); ++p )
					{						
						string	localExtension=(parentVals[p].find(addExtension)==string::npos)?addExtension:"";
						string	parentName=parentVals[p]+localExtension;

						CSmartPtr<CForm> parentForm = (CForm*)formLoader->loadForm(CFile::getFilename(parentName.c_str()).c_str());
						if ( ! parentForm )
						{
							nlwarning( "Can't load parent form %s", parentName.c_str() );
						}
						else
						{
							form->insertParent( p, parentName.c_str(), parentForm );
							isModified=true;
							displayed = true;
							nbinsertedparents++;
						}

					}
					nlinfo( "Inserted %u parent(s)", nbinsertedparents );
				}
				// NOTE: Changing the parent is not currently implemented!
				continue;
			}

			const	UFormElm	*fieldForm=NULL;
						
			if	(rootForm.getNodeByName(&fieldForm, var.c_str()))
			{
				UFormDfn	*dfnForm=const_cast<UFormElm&>(rootForm).getStructDfn();
				nlassert(dfnForm);
								
				vector<string> memberVals;
				explode( val, std::string(ARRAY_SEPARATOR), memberVals );
				uint32	memberIndex=0;
				
				while (memberIndex<memberVals.size())
				{
					const	uint	currentMemberIndex=memberIndex;
					std::string		memberVal=memberVals[memberIndex];
					memberIndex++;
					
					if	(!memberVal.empty())
					{
						if (memberVal[0] == '"')
							memberVal.erase(0, 1);
						if (memberVal.size()>0 && memberVal[memberVal.size()-1] == '"')
							memberVal.resize(memberVal.size()-1);
						
						if (memberVal == "ValueForm" ||
							memberVal == "ValueParentForm" ||
							memberVal == "ValueDefaultDfn" ||
							memberVal == "ValueDefaultType" ||
							memberVal == "ERR")
							continue;
					}


					//				nlassert(fieldDfn);
					//				virtual bool getEntryFilenameExt (uint entry, std::string &name) const = 0;
					//				virtual bool getEntryFilename (uint entry, std::string &name) const = 0;
					if (dfnForm)
					{
						string	fileName;
						string	fileNameExt;
						bool	toto=false;
						static	string	filenameTyp("filename.typ");
						string	extension;
			
						uint	fieldIndex;
						if (dfnForm->getEntryIndexByName (fieldIndex, var))	//	field exists.
						{
							dfnForm->getEntryFilename(fieldIndex,fileName);
							if (fileName==filenameTyp)
							{
								dfnForm->getEntryFilenameExt(fieldIndex,fileNameExt);
								if (	!fileNameExt.empty()
									&&	fileNameExt!="*.*")
								{
									string::size_type	index=fileNameExt.find(".");
									if (index==string::npos)	//	not found.
									{
										extension=fileNameExt;
									}
									else
									{
										extension=fileNameExt.substr(index+1);
									}
									
									if	(memberVal.find(extension)==string::npos)	// extension not found.
									{
										memberVal=NLMISC::toString("%s.%s",memberVal.c_str(),extension.c_str());
									}
									
								}
								
							}
							
						}
						
					}
					

					if	(dfnForm->isAnArrayEntryByName(var))
					{
						if	(	!isNewSheet
							&&	fieldForm!=NULL)
						{
							uint arraySize;
							const UFormElm *arrayNode = NULL;
							if (fieldForm->isArray() 
								&& fieldForm->getArraySize(arraySize) && arraySize == memberVals.size())
							{
								string	test;
								if	(	fieldForm->getArrayValue(test, currentMemberIndex)
									&&	test==memberVal	)
								{
									continue;
								}
							}
						}
						//nldebug( "%s: %s '%s'", args[0].c_str(), var.c_str(), memberVal.c_str() );
						// need to put the value at the correct index.
						const	std::string	fieldName=NLMISC::toString("%s[%d]", var.c_str(), currentMemberIndex).c_str();
						const_cast<UFormElm&>(rootForm).setValueByName(memberVal.c_str(), fieldName.c_str());
						isModified=true;
						displayed = true;
					}
					else
					{
						if	(!isNewSheet)
						{
							string	test;
							if	(	rootForm.getValueByName(test,var.c_str())
								&&	test==memberVal	)
							{
								continue;
							}
							
						}					
						//nldebug( "%s: %s '%s'", args[0].c_str(), var.c_str(), memberVal.c_str() );
						const_cast<UFormElm&>(rootForm).setValueByName(memberVal.c_str(), var.c_str());
						isModified=true;
						displayed = true;
					}
					
					if	(!isNewSheet)
					{
						isModified = true;
						if (!displayed)
							nlinfo("in %s:", filename.c_str());
						displayed = true;
						nlinfo("%s = %s", var.c_str(), memberVal.c_str());
					}

				}

			}
			else	//	field Node not found :\ (bad)
			{
			}
			
		}

		if ( ! isNewSheet )
		{
			if ( isModified )
				++nbModifiedSheets;
			else
				++nbUnchangedSheets;
		}

		// Write sheet
		if ( isNewSheet || displayed )
		{
			if ( WriteSheetsToDisk )
			{
				++nbWritten;
				string	path = isNewSheet ? OutputPath : "";
				string	ext = (filename.find( addExtension ) == string::npos) ? addExtension : "";
				string	absoluteFileName=path + dirbase + filename + ext;
				
//				nlinfo("opening: %s",  absoluteFileName.c_str() );
				COFile	output(absoluteFileName);
				if	(!output.isOpen())
				{
					nlinfo("creating path: %s",  (path + dirbase).c_str() );
					NLMISC::CFile::createDirectory(path + dirbase);
				}

//				nlinfo("opening2: %s",  absoluteFileName.c_str() );
				output.open	(absoluteFileName);
				
				if (!output.isOpen())
				{					
					nlinfo("ERROR! cannot create file path: %s",  absoluteFileName.c_str() );
				}
				else
				{
					form->write(output, true);
					output.close();
					
					if	(!CPath::exists(filename + ext))
						CPath::addSearchFile(absoluteFileName);					
				}					

			}
			clearSheet( form, &form->getRootNode() );
		}

	}
	nlinfo( "%u sheets processed (%u new, %u modified, %u unchanged - %u written)", nbNewSheets+nbModifiedSheets+nbUnchangedSheets, nbNewSheets, nbModifiedSheets, nbUnchangedSheets, nbWritten );
	UFormLoader::releaseLoader (formLoader);
}

//
void	usage(char *argv0, FILE *out)
{
	fprintf(out, "\n");
	fprintf(out, "Syntax: %s [-p <sheet path>] [-s <field_separator>] [-g <sheet type>] [-o <output path>] [<script file name> | <csv file name>]", argv0);
	fprintf(out, "(-g = generate sheet files, needs template sheet _empty.<sheet type> and <sheet type>_dirmap.cfg in the current folder");
	fprintf(out, "\n");
	fprintf(out, "Script commands:\n");
	fprintf(out, "\tDFNPATH\t\t<search path for george dfn files>\n");
	fprintf(out, "\tPATH\t\t<search path for files to scan>\n");
	fprintf(out, "\tOUTPUT\t\t<output file>\n");
	fprintf(out, "\tFIELD\t\t<field in george file>\n");
	fprintf(out, "\tSOURCE\t\t<field in george file>\n");
	fprintf(out, "\tSCANFILES\t[+<text>|-<text>[...]]\n");
	fprintf(out, "\tSCRIPT\t\t<script file to execute>\n");
	fprintf(out, "\n");
}

int main(int argc, char* argv[])
{
	bool generate = false;
	string sheetType;

	// parse command line
	uint	i;
	for (i=1; (sint)i<argc; i++)
	{
		const char	*arg = argv[i];
		if (arg[0] == '-')
		{
			switch (arg[1])
			{
			case 'p':
				++i;
				if ((sint)i == argc)
				{
					fprintf(stderr, "Missing <sheet path> after -p option\n");
					usage(argv[0], stderr);
					exit(0);
				}
				inputSheetPaths.push_back(argv[i]);
				break;
			case 's':
				++i;
				if ((sint)i == argc)
				{
					fprintf(stderr, "Missing <field_separator> after -s option\n");
					usage(argv[0], stderr);
					exit(0);
				}
				SEPARATOR = argv[i];
				break;
			case 'g':
				++i;
				if ((sint)i == argc)
				{
					fprintf(stderr, "Missing <sheetType> after -g option\n");
					usage(argv[0], stderr);
					exit(0);
				}
				generate = true;
				sheetType = string(argv[i]);
				break;
			case 'o':
				++i;
				if ((sint)i == argc)
				{
					fprintf(stderr, "Missing <output path> after -o option\n");
					usage(argv[0], stderr);
					exit(0);
				}
				OutputPath = string(argv[i]);
				break;
			default:
				fprintf(stderr, "Unrecognized option '%c'\n", arg[1]);
				usage(argv[0], stderr);
				exit(0);
				break;
			}
		}
		else
		{
			if (CFile::getExtension(arg) == "csv")
			{
				inputCsvFiles.push_back(arg);
			}
			else
			{
				inputScriptFiles.push_back(arg);
			}
		}
	}

	if (inputScriptFiles.empty() && inputCsvFiles.empty())
	{
		fprintf(stderr, "Missing input script file or csv file\n");
		usage(argv[0], stderr);
		exit(0);
	}



	for (i=0; i<inputScriptFiles.size(); ++i)
		executeScriptFile(inputScriptFiles[i]);

	for (i=0; i<inputCsvFiles.size(); ++i)
		convertCsvFile(inputCsvFiles[i], generate, sheetType);

	fprintf(stderr,"\nDone.\n");
	getchar();
	return 0;
}