//-----------------------------------------------------------------------------
// Torque Game Engine 
// Copyright (C) GarageGames.com, Inc.
//-----------------------------------------------------------------------------

#include "platform/platform.h"
#include "platform/event.h"
#include "platform/platformAssert.h"
#include "console/console.h"
#include "console/consoleTypes.h"
#include "math/mathTypes.h"
#include "langc/langc.h"

#include "core/fileStream.h"

#include "i18n/lang.h"

#include <stdlib.h>

#include "langcomp.h"


//////////////////////////////////////////////////////////////////////////
// Simple hash function for ID lookups in translations
//////////////////////////////////////////////////////////////////////////

static U32 hashString(const UTF8 *str)
{
	S32 i;
	U32 h = 0;
	
	for(i = 0;str[i];i++)
	{
		h += (str[i] * i);
	}
	return h;
}

//////////////////////////////////////////////////////////////////////////
// LFileWriter Class
//////////////////////////////////////////////////////////////////////////

LFileWriter::LFileWriter(LangFile *langFile) : mLangFile(langFile)
{
}

bool LFileWriter::Open(const UTF8 *basename)
{
	UTF8 filename[256];
	
	GetFilename(basename, filename, sizeof(filename));
	return mStream.open((const char*)filename, FileStream::Write);
}

void LFileWriter::Close()
{
	mStream.close();
}

//////////////////////////////////////////////////////////////////////////
// Writer Classes
//////////////////////////////////////////////////////////////////////////

class LHeaderWriter : public LFileWriter
{
public:
	LHeaderWriter(LangFile *l) : LFileWriter(l)
	{
	}

	virtual void GetFilename(const UTF8 *basename, UTF8 *buf, U32 bufsize)
	{
		dSprintf(buf, bufsize, "%s.h", basename);
	}

	virtual void WriteHeader()
	{
		mStream.writeLine((U8 *)"// Automatically generated. DO NOT EDIT\n");
		mStream.writeLine((U8 *)"#ifndef _TGE_I18N_AUTOGEN_H_");
		mStream.writeLine((U8 *)"#define _TGE_I18N_AUTOGEN_H_\n");
	}

	virtual void WriteFooter()
	{
		UTF8 buf[LCL_MAXSTRINGLENGTH + 4];
		
		dSprintf(buf, sizeof(buf), "\n#define I18N_NUM_STRINGS\t%d", mLangFile->getNumStrings());
		mStream.writeLine((U8 *)&buf);
		
		mStream.writeLine((U8 *)"\n#endif // _TGE_I18N_AUTOGEN_H_");
	}
	
	virtual void WriteString(UTF8 *idstr, U32 idnum, UTF8 *str)
	{
		UTF8 buf[LCL_MAXSTRINGLENGTH + 4];
		
		dSprintf(buf, sizeof(buf), "// %s", str);
		mStream.writeLine((U8 *)&buf);
		dSprintf(buf, sizeof(buf), "#define %s\t%d", idstr, idnum);
		mStream.writeLine((U8 *)&buf);
	}
};

class LScriptWriter : public LFileWriter
{
public:
	LScriptWriter(LangFile *l) : LFileWriter(l)
	{
	}

	virtual void GetFilename(const UTF8 *basename, UTF8 *buf, U32 bufsize)
	{
		dSprintf(buf, bufsize, "%s.cs", basename);
	}

	virtual void WriteHeader()
	{
		mStream.writeLine((U8 *)"// Automatically generated. DO NOT EDIT\n");
	}
	
	virtual void WriteFooter()
	{
	}
	
	virtual void WriteString(UTF8 *idstr, U32 idnum, UTF8 *str)
	{
		UTF8 buf[LCL_MAXSTRINGLENGTH + 4];
		
		dSprintf(buf, sizeof(buf), "// %s", str);
		mStream.writeLine((U8 *)&buf);
		dSprintf(buf, sizeof(buf), "$%s = %d;", idstr, idnum);
		mStream.writeLine((U8 *)&buf);
	}
};

class LDefaultsWriter : public LFileWriter
{
public:
	LDefaultsWriter(LangFile *l) : LFileWriter(l)
	{
	}

	virtual void GetFilename(const UTF8 *basename, UTF8 *buf, U32 bufsize)
	{
		dSprintf(buf, bufsize, "%sDefaults.cc", basename);
	}

	virtual void WriteHeader()
	{
		mStream.writeLine((U8 *)"// Automatically generated. DO NOT EDIT\n");
		mStream.writeLine((U8 *)"const UTF8 *gI18NDefaultStrings[] =\n{");
	}
	
	virtual void WriteFooter()
	{
		mStream.writeLine((U8 *)"};");
	}
	
	virtual void WriteString(UTF8 *idstr, U32 idnum, UTF8 *str)
	{
		UTF8 buf[LCL_MAXSTRINGLENGTH + 4];
	
		dSprintf(buf, sizeof(buf), "\t// %s = %d", idstr, idnum);
		mStream.writeLine((U8 *)&buf);
		dSprintf(buf, sizeof(buf), "\t(const UTF8*)\"%s\",", str);
		mStream.writeLine((U8 *)&buf);
	}
};

class LTransWriter : public LFileWriter
{
public:
	LTransWriter(LangFile *l) : LFileWriter(l)
	{
	}
	
	virtual void GetFilename(const UTF8 *basename, UTF8 *buf, U32 bufsize)
	{
		dSprintf(buf, bufsize, "%s.tran", basename);
	}
	
	virtual void WriteHeader()
	{
		mStream.writeLine((U8 *)"# Automatically generated.\n");
	}
	
	virtual void WriteFooter()
	{
	}
	
	virtual void WriteString(UTF8 *idstr, U32 idnum, UTF8 *str)
	{
		UTF8 buf[LCL_MAXSTRINGLENGTH + 4];
		
		dSprintf(buf, sizeof(buf), "# [%s:%d] %s", idstr, idnum, str);
		mStream.writeLine((U8 *)&buf);
		dSprintf(buf, sizeof(buf), "%s=\n", idstr);
		mStream.writeLine((U8 *)&buf);
	}
};

//////////////////////////////////////////////////////////////////////////
// LString Class
//////////////////////////////////////////////////////////////////////////

LString::LString(UTF8 *i /* = NULL */, UTF8 *s /* = NULL */, UTF8 *sclean /* = NULL */) : str(NULL), strclean(NULL), id(NULL)
{
	if(s)
	{
		str = new UTF8 [dStrlen(s) + 1];
		dStrcpy(str, s);
	}

	if(sclean)
	{
		strclean = new UTF8 [dStrlen(sclean) + 1];
		dStrcpy(strclean, sclean);
	}
	
	if(i)
	{
		id = new UTF8 [dStrlen(i) + 1];
		dStrcpy(id, i);
	}
}

LString::~LString()
{
	if(str)
		delete [] str;
	if(id)
		delete [] id;
}

//////////////////////////////////////////////////////////////////////////
// LangComp Class
//////////////////////////////////////////////////////////////////////////

LangComp::LangComp(U32 options /* = 0L */) : mOptions(options), mLangFile(NULL),
	mCurrentFilename(NULL), mCurrentLine(0), mNumErrors(0), mNumWarn(0),
	mTransLookup(LC_ID_LOOKUP_SIZE)
{
}

LangComp::~LangComp()
{
	Free();
}

//////////////////////////////////////////////////////////////////////////
// Protected Methods
//////////////////////////////////////////////////////////////////////////

void LangComp::Free()
{
	if(mLangFile)
	{
		delete mLangFile;
		mLangFile = NULL;
	}
	
	if(mCurrentFilename)
	{
		delete [] mCurrentFilename;
		mCurrentFilename = NULL;
	}
	mCurrentLine = 0;

	S32 i;
	for(i = 0;i < mFileWriters.size();i++)
	{
		delete mFileWriters[i];
	}
	mFileWriters.empty();
}

void LangComp::Error(const UTF8 *msg, ...)
{
	UTF8 buf[512];
	va_list va;

	va_start(va, msg);
	dVsprintf(buf, sizeof(buf), msg, va);
	va_end(va);

	if(mCurrentFilename)
		dPrintf("error at %s:%d: %s\n", mCurrentFilename, mCurrentLine, buf);
	else
		dPrintf("error: %s\n", buf);
	
	mNumErrors++;
}

void LangComp::Warn(const UTF8 *msg, ...)
{
	UTF8 buf[512];
	va_list va;
	
	if(mOptions & LCO_NOWARNINGS)
		return;

	va_start(va, msg);
	dVsprintf(buf, sizeof(buf), msg, va);
	va_end(va);
	
	if(mCurrentFilename)
		dPrintf("warning at %s:%d: %s\n", mCurrentFilename, mCurrentLine, buf);
	else
		dPrintf("warning: %s\n", buf);

	mNumWarn++;
}

LString * LangComp::ParseLine(UTF8 *line)
{
	UTF8 *p, id[LCL_MAXIDLENGTH], str[LCL_MAXSTRINGLENGTH];
	S32 i, warnCount;

	i = dStrlen(line)-1;
	if(line[i] == '\n') line[i] = 0;
	i = dStrlen(line)-1;
	if(line[i] == '\r') line[i] = 0;
	
	p = line;
	
	// Allowable comment delimiters: # ; //
	if(*p == '#' || *p == ';' || (*p == '/' && *(p+1) == '/') || *p == 0)
		return new LString();

	i = 0;
	while(*p)
	{
		if(*p == '=')
			break;

		if(i < (sizeof(id)-1))
		{
			if(dIsalnum(*p) || *p == '_')
			{
				id[i] = *p;
				i++;
			}
			else if(! dIsspace(*p) && ! (mOptions & LCO_DONTWARNINVALIDCHAR))
				Warn("invalid character ('%c') in identifier ignored", *p);
		}
		
		p++;
	}
	id[i] = 0;
	p++;

	// Identifiers cannot start with a number
	if(dIsdigit(id[0]))
	{
		Error("identifiers cannot start with a number");
		return NULL;
	}
	
	// Identifiers must be there
	if(id[0] == 0)
	{
		Error("no identifier");
		return NULL;
	}

	i = 0;
	bool foundStart = (mOptions & LCO_DONTSTRIPSPACES);
	while(*p)
	{
		if(i < (sizeof(str)-1))
		{
			if(!foundStart && ! dIsspace(*p))
				foundStart = true;

			if(foundStart && i < (sizeof(str)-1))
			{
				str[i] = *p;
				i++;
			}
		}
		
		p++;
	}
	str[i] = 0;

	if(mOptions & LCO_STRIPTRAILINGSPACE)
	{
		p = dStrchr(str, 0);
		while(dIsspace(*(--p)))
			*p = 0;
	}

	if(mOptions & LCO_WARNNOSTRING && str[0] == 0)
	{
		Warn("identifier '%s' is empty", id);
	}

	UTF8 strbuf[LCL_MAXSTRINGLENGTH];
	processSlashes(str, strbuf);

	return new LString(id, strbuf, str);
}

void LangComp::WriteString(UTF8 *idstr, U32 idnum, UTF8 *str)
{
	S32 i;

	for(i = 0;i < mFileWriters.size();i++)
	{
		mFileWriters[i]->WriteString(idstr, idnum, str);
	}
}

void LangComp::WriteFileHeader()
{
	S32 i;
	
	for(i = 0;i < mFileWriters.size();i++)
	{
		mFileWriters[i]->WriteHeader();
	}
}

void LangComp::WriteFileFooter()
{
	S32 i;
	
	for(i = 0;i < mFileWriters.size();i++)
	{
		mFileWriters[i]->WriteFooter();
	}
}


// [tom, 3/7/2005] There is no buffer size limit here as the size of the buffer
// in ParseLine() is the same size as the buffer the string is in. If this function
// is ripped for use elsewhere, I suggest adding a buffer size check.
UTF8 *LangComp::processSlashes(const UTF8 *string, UTF8 *buffer)
{
	const UTF8 *s = string;
	UTF8 *d = buffer;

	while(*s)
	{
		if(*s == '\\')
		{
			s++;
			switch(*s)
			{
				case 'n':
					*d++ = '\n';
					s++;
					break;

				case 'r':
					*d++ = '\r';
					s++;
					break;

				case 't':
					*d++ = '\t';
					s++;
					break;

				default:
					*d++ = *s++;
					break;
			}
		}
		else
		{
			*d++ = *s++;
		}
	}
	*d = 0;

	return buffer;
}

// [tom, 3/17/2005] No buffer check here either, this time out of lazyness.
// [tom, 3/17/2005] This function isnt actually used anymore. I'm keeping it
// here in case its useful in the future.
UTF8 *LangComp::quoteString(const UTF8 *string, UTF8 *buffer)
{
	static struct {
		unsigned char c;
		unsigned char q;
	} quoteTab[]=
	{
		{ '\n', 'n' },
		{ '\r', 'r' },
		{ '\t', 't' },
		{ '"',	'"' },
		{ '\'',	'\'' },
		{ '\\',	'\\' },

		{ 0, 0 }
	};
	const UTF8 *s = string;
	UTF8 *d = buffer;

	while(*s)
	{
		int i;
		bool rep = false;

		for(i = 0;quoteTab[i].c;i++)
		{
			if(*s == quoteTab[i].c)
			{
				*d++ = '\\';
				*d++ = quoteTab[i].q;
				rep = true;
				break;
			}
		}

		if(!rep)
			*d++ = *s;

		s++;
	}
	*d = 0;

	return buffer;
}

bool LangComp::AddFileWriter(LFileWriter *lfw, const UTF8 *basename)
{
	if(lfw->Open(basename))
	{
		mFileWriters.push_back(lfw);
		return true;
	}

	UTF8 buf[256];
	lfw->GetFilename(basename, buf, sizeof(buf));
	Warn("Could not open file \"%s\"", buf);
	
	delete lfw;

	return false;
}

//////////////////////////////////////////////////////////////////////////
// Public Methods
//////////////////////////////////////////////////////////////////////////

bool LangComp::Compile(const UTF8 *filename, const UTF8 *outbasename, const UTF8 *englishTable /* = NULL */)
{
	bool ret = true;
	UTF8 lsoname[256];
	
	Free();

	if((mOptions & LCO_COMPILETRANSLATION) && englishTable == NULL)
	{
		Error("you must specify the english language file when compiling translations.");
		return false;
	}

	if(mOptions & LCO_COMPILETRANSLATION)
	{
		if(! LoadLangForTranslation(englishTable))
		{
			Error("could not load %s", englishTable);
			return false;
		}
	}

	dSprintf(lsoname, sizeof(lsoname), "%s.lso", outbasename);

	mCurrentFilename = new UTF8 [dStrlen(filename) + 1];
	dStrcpy(mCurrentFilename, filename);

	mLangFile = new LangFile;
	
	FileStream fs;
	if(fs.open(filename, FileStream::Read))
	{
		if(mOptions & LCO_WRITEHEADER)
			AddFileWriter(new LHeaderWriter(mLangFile), outbasename);

		if(mOptions & LCO_WRITESCRIPT)
			AddFileWriter(new LScriptWriter(mLangFile), outbasename);

		if(mOptions & LCO_WRITECDEFAULTS)
			AddFileWriter(new LDefaultsWriter(mLangFile), outbasename);

		if(mOptions & LCO_WRITETRANSLATION)
			AddFileWriter(new LTransWriter(mLangFile), outbasename);

		WriteFileHeader();

		// This buffer must be able to hold the max lengths, the equals, end of line chars and a bit of leeway for spaces
		UTF8 buf[LCL_MAXIDLENGTH + LCL_MAXSTRINGLENGTH + 28];
		
		while(fs.getStatus() != Stream::EOS)
		{
			mCurrentLine++;
			fs.readLine((U8 *)&buf[0], sizeof(buf));

			LString *ls;
			if((ls = ParseLine(buf)) == NULL)
				break;

			if(ls->id)
			{
				U32 idnum;
				if(mOptions & LCO_COMPILETRANSLATION)
				{
					idnum = (U32)mTransLookup.retreive(hashString(ls->id));
					if(idnum == 0)
						Warn("id %s does not exist in the english table", ls->id);
					else
						idnum--;
					mLangFile->setString(idnum, ls->str);
				}
				else
					idnum = mLangFile->addString(ls->str);
				
				WriteString(ls->id, idnum, ls->strclean);
			}
			
			delete ls;
		}

		WriteFileFooter();

		S32 i;
		for(i = 0;i < mFileWriters.size();i++)
		{
			mFileWriters[i]->Close();
		}

		fs.close();
	}

	if(mOptions & LCO_WRITELANGTABLE)
	{
		if(ret && fs.open(lsoname, FileStream::Write))
		{
			mLangFile->save(&fs);
			fs.close();
		}
	}

	
	dPrintf("%s - ", lsoname);
	if(ret)
		dPrintf("%d string(s), ", mLangFile->getNumStrings());
	dPrintf("%d error(s), %d warning(s)\n", mNumErrors, mNumWarn);

	return ret;
}

bool LangComp::LoadLangForTranslation(const UTF8 *filename)
{
	bool ret = true;
	FileStream fs;

	UTF8 *fnBak = mCurrentFilename;
	U32 lineBak = mCurrentLine;

	mCurrentFilename = new UTF8 [dStrlen(filename) + 1];
	dStrcpy(mCurrentFilename, filename);
	mCurrentLine = 0;

	if(fs.open(filename, FileStream::Read))
	{
		// This buffer must be able to hold the max lengths, the equals, end of line chars and a bit of leeway for spaces
		UTF8 buf[LCL_MAXIDLENGTH + LCL_MAXSTRINGLENGTH + 28];
		U32 lastID = 1;	// [tom, 4/25/2005] 0 is used as an error indicator
		
		while(fs.getStatus() != Stream::EOS)
		{
			mCurrentLine++;
			fs.readLine((U8 *)&buf[0], sizeof(buf));
			
			LString *ls;
			if((ls = ParseLine(buf)) == NULL)
				break;
			
			if(ls->id)
			{
				U32 h = hashString(ls->id);
				mTransLookup.insert((U32 *)lastID, h);
				lastID++;
			}
			
			delete ls;
		}

		fs.close();
	}

	delete [] mCurrentFilename;
	mCurrentFilename = fnBak;
	mCurrentLine = lineBak;

	return ret;
}
