/*
** stringtable.cpp
** Implements the FStringTable class
**
**---------------------------------------------------------------------------
** Copyright 1998-2006 Randy Heit
** All rights reserved.
**
** Redistribution and use in source and binary forms, with or without
** modification, are permitted provided that the following conditions
** are met:
**
** 1. Redistributions of source code must retain the above copyright
**    notice, this list of conditions and the following disclaimer.
** 2. Redistributions in binary form must reproduce the above copyright
**    notice, this list of conditions and the following disclaimer in the
**    documentation and/or other materials provided with the distribution.
** 3. The name of the author may not be used to endorse or promote products
**    derived from this software without specific prior written permission.
**
** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
**---------------------------------------------------------------------------
**
*/

#include <string.h>

#include "stringtable.h"
#include "cmdlib.h"
#include "w_wad.h"
#include "i_system.h"
#include "sc_man.h"
#include "c_dispatch.h"
#include "v_text.h"
#include "gi.h"

// PassNum identifies which language pass this string is from.
// PassNum 0 is for DeHacked.
// PassNum 1 is for * strings.
// PassNum 2+ are for specific locales.

struct FStringTable::StringEntry
{
	StringEntry *Next;
	char *Name;
	uint8_t PassNum;
	char String[];
};

FStringTable::FStringTable ()
{
	for (int i = 0; i < HASH_SIZE; ++i)
	{
		Buckets[i] = NULL;
	}
}

FStringTable::~FStringTable ()
{
	FreeData ();
}

void FStringTable::FreeData ()
{
	for (int i = 0; i < HASH_SIZE; ++i)
	{
		StringEntry *entry = Buckets[i], *next;
		Buckets[i] = NULL;
		while (entry != NULL)
		{
			next = entry->Next;
			M_Free (entry);
			entry = next;
		}
	}
}

void FStringTable::FreeNonDehackedStrings ()
{
	for (int i = 0; i < HASH_SIZE; ++i)
	{
		StringEntry *entry, *next, **pentry;

		for (pentry = &Buckets[i], entry = *pentry; entry != NULL; )
		{
			next = entry->Next;
			if (entry->PassNum != 0)
			{
				*pentry = next;
				M_Free (entry);
			}
			else
			{
				pentry = &entry->Next;
			}
			entry = next;
		}
	}
}

void FStringTable::LoadStrings (bool enuOnly)
{
	int lastlump, lump;
	int i, j;

	FreeNonDehackedStrings ();

	lastlump = 0;

	while ((lump = Wads.FindLump ("LANGUAGE", &lastlump)) != -1)
	{
		j = 0;
		if (!enuOnly)
		{
			LoadLanguage (lump, MAKE_ID('*',0,0,0), true, ++j);
			for (i = 0; i < 4; ++i)
			{
				LoadLanguage (lump, LanguageIDs[i], true, ++j);
				LoadLanguage (lump, LanguageIDs[i] & MAKE_ID(0xff,0xff,0,0), true, ++j);
				LoadLanguage (lump, LanguageIDs[i], false, ++j);
			}
		}

		// Fill in any missing strings with the default language
		LoadLanguage (lump, MAKE_ID('*','*',0,0), true, ++j);
	}
}

void FStringTable::LoadLanguage (int lumpnum, uint32_t code, bool exactMatch, int passnum)
{
	static bool errordone = false;
	const uint32_t orMask = exactMatch ? 0 : MAKE_ID(0,0,0xff,0);
	uint32_t inCode = 0;
	StringEntry *entry, **pentry;
	uint32_t bucket;
	int cmpval;
	bool skip = true;

	code |= orMask;

	FScanner sc(lumpnum);
	sc.SetCMode (true);
	while (sc.GetString ())
	{
		if (sc.Compare ("["))
		{ // Process language identifiers
			bool donot = false;
			bool forceskip = false;
			skip = true;
			sc.MustGetString ();
			do
			{
				size_t len = sc.StringLen;
				if (len != 2 && len != 3)
				{
					if (len == 1 && sc.String[0] == '~')
					{
						donot = true;
						sc.MustGetString ();
						continue;
					}
					if (len == 1 && sc.String[0] == '*')
					{
						inCode = MAKE_ID('*',0,0,0);
					}
					else if (len == 7 && stricmp (sc.String, "default") == 0)
					{
						inCode = MAKE_ID('*','*',0,0);
					}
					else
					{
						sc.ScriptError ("The language code must be 2 or 3 characters long.\n'%s' is %lu characters long.",
							sc.String, len);
					}
				}
				else
				{
					inCode = MAKE_ID(tolower(sc.String[0]), tolower(sc.String[1]), tolower(sc.String[2]), 0);
				}
				if ((inCode | orMask) == code)
				{
					if (donot)
					{
						forceskip = true;
						donot = false;
					}
					else
					{
						skip = false;
					}
				}
				sc.MustGetString ();
			} while (!sc.Compare ("]"));
			if (donot)
			{
				sc.ScriptError ("You must specify a language after ~");
			}
			skip |= forceskip;
		}
		else
		{ // Process string definitions.
			if (inCode == 0)
			{
				// LANGUAGE lump is bad. We need to check if this is an old binary
				// lump and if so just skip it to allow old WADs to run which contain
				// such a lump.
				if (!sc.isText())
				{
					if (!errordone) Printf("Skipping binary 'LANGUAGE' lump.\n"); 
					errordone = true;
					return;
				}
				sc.ScriptError ("Found a string without a language specified.");
			}

			bool savedskip = skip;
			if (sc.Compare("$"))
			{
				sc.MustGetStringName("ifgame");
				sc.MustGetStringName("(");
				sc.MustGetString();
				skip |= !sc.Compare(GameTypeName());
				sc.MustGetStringName(")");
				sc.MustGetString();

			}

			if (skip)
			{ // We're not interested in this language, so skip the string.
				sc.MustGetStringName ("=");
				sc.MustGetString ();
				do
				{
					sc.MustGetString ();
				} 
				while (!sc.Compare (";"));
				skip = savedskip;
				continue;
			}

			FString strName (sc.String);
			sc.MustGetStringName ("=");
			sc.MustGetString ();
			FString strText (sc.String, ProcessEscapes (sc.String));
			sc.MustGetString ();
			while (!sc.Compare (";"))
			{
				ProcessEscapes (sc.String);
				strText += sc.String;
				sc.MustGetString ();
			}

			// Does this string exist? If so, should we overwrite it?
			bucket = MakeKey (strName.GetChars()) & (HASH_SIZE-1);
			pentry = &Buckets[bucket];
			entry = *pentry;
			cmpval = 1;
			while (entry != NULL)
			{
				cmpval = stricmp (entry->Name, strName.GetChars());
				if (cmpval >= 0)
					break;
				pentry = &entry->Next;
				entry = *pentry;
			}
			if (cmpval == 0 && entry->PassNum >= passnum)
			{
				*pentry = entry->Next;
				M_Free (entry);
				entry = NULL;
			}
			if (entry == NULL || cmpval > 0)
			{
				entry = (StringEntry *)M_Malloc (sizeof(*entry) + strText.Len() + strName.Len() + 2);
				entry->Next = *pentry;
				*pentry = entry;
				strcpy (entry->String, strText.GetChars());
				strcpy (entry->Name = entry->String + strText.Len() + 1, strName.GetChars());
				entry->PassNum = passnum;
			}
		}
	}
}

// Replace \ escape sequences in a string with the escaped characters.
size_t FStringTable::ProcessEscapes (char *iptr)
{
	char *sptr = iptr, *optr = iptr, c;

	while ((c = *iptr++) != '\0')
	{
		if (c == '\\')
		{
			c = *iptr++;
			if (c == 'n')
				c = '\n';
			else if (c == 'c')
				c = TEXTCOLOR_ESCAPE;
			else if (c == 'r')
				c = '\r';
			else if (c == 't')
				c = '\t';
			else if (c == '\n')
				continue;
		}
		*optr++ = c;
	}
	*optr = '\0';
	return optr - sptr;
}

// Finds a string by name and returns its value
const char *FStringTable::operator[] (const char *name) const
{
	if (name == NULL)
	{
		return NULL;
	}
	uint32_t bucket = MakeKey (name) & (HASH_SIZE - 1);
	StringEntry *entry = Buckets[bucket];

	while (entry != NULL)
	{
		int cmpval = stricmp (entry->Name, name);
		if (cmpval == 0)
		{
			return entry->String;
		}
		if (cmpval == 1)
		{
			return NULL;
		}
		entry = entry->Next;
	}
	return NULL;
}

// Finds a string by name and returns its value. If the string does
// not exist, returns the passed name instead.
const char *FStringTable::operator() (const char *name) const
{
	const char *str = operator[] (name);
	return str ? str : name;
}

// Find a string by name. pentry1 is a pointer to a pointer to it, and entry1 is a
// pointer to it. Return NULL for entry1 if it wasn't found.
void FStringTable::FindString (const char *name, StringEntry **&pentry1, StringEntry *&entry1)
{
	uint32_t bucket = MakeKey (name) & (HASH_SIZE - 1);
	StringEntry **pentry = &Buckets[bucket], *entry = *pentry;

	while (entry != NULL)
	{
		int cmpval = stricmp (entry->Name, name);
		if (cmpval == 0)
		{
			pentry1 = pentry;
			entry1 = entry;
			return;
		}
		if (cmpval == 1)
		{
			pentry1 = pentry;
			entry1 = NULL;
			return;
		}
		pentry = &entry->Next;
		entry = *pentry;
	}
	pentry1 = pentry;
	entry1 = entry;
}

// Find a string with the same exact text. Returns its name.
const char *FStringTable::MatchString (const char *string) const
{
	for (int i = 0; i < HASH_SIZE; ++i)
	{
		for (StringEntry *entry = Buckets[i]; entry != NULL; entry = entry->Next)
		{
			if (strcmp (entry->String, string) == 0)
			{
				return entry->Name;
			}
		}
	}
	return NULL;
}

void FStringTable::SetString (const char *name, const char *newString)
{
	StringEntry **pentry, *oentry;
	FindString (name, pentry, oentry);

	size_t newlen = strlen (newString);
	size_t namelen = strlen (name);

	// Create a new string entry
	StringEntry *entry = (StringEntry *)M_Malloc (sizeof(*entry) + newlen + namelen + 2);
	strcpy (entry->String, newString);
	strcpy (entry->Name = entry->String + newlen + 1, name);
	entry->PassNum = 0;

	// If this is a new string, insert it. Otherwise, replace the old one.
	if (oentry == NULL)
	{
		entry->Next = *pentry;
		*pentry = entry;
	}
	else
	{
		*pentry = entry;
		entry->Next = oentry->Next;
		M_Free (oentry);
	}
}
