// Emacs style mode select   -*- C++ -*-
//-----------------------------------------------------------------------------
//
// $Id: cdb6d422d6c562242eac09934d43b046399bb03c $
//
// Copyright (C) 1998-2006 by Randy Heit (ZDoom).
// Copyright (C) 2006-2026 by The Odamex Team.
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// 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 General Public License for more details.
//
// DESCRIPTION:
//	String Abstraction Layer (StringTable)
//
//-----------------------------------------------------------------------------


#include "odamex.h"

#include "stringtable.h"


#include "cmdlib.h"
#include "i_system.h"
#include "oscanner.h"
#include "stringenums.h"
#include "w_wad.h"

/**
 * @brief Map a ZDoom game name to Odamex's internals and returns true if
 *        the current game is the passed string.
 *
 * @param str String to check against.
 * @return True if the game matches the passed string, otherwise false.
 */
static bool IfGameZDoom(const std::string& str)
{
	// TODO: should this account for rekkr too? uzdoom seems to just have
	// doom, strife, heretic, hexen, and chex here
	if (!stricmp(str.c_str(), "doom") && !IsChexMission(::gamemission) &&
	    ::gamemode != undetermined && ::gamemission != commercial_hacx)
	{
		return true;
	}

	if (!stricmp(str.c_str(), "chex") && IsChexMission(::gamemission))
	{
		return true;
	}

	if (!stricmp(str.c_str(), "hacx") && ::gamemission == commercial_hacx)
	{
		return true;
	}

	// We don't support anything else.
	return false;
}

bool StringTable::canSetPassString(int pass, const std::string& name) const
{
	StringHash::const_iterator it = _stringHash.find(name);

	// New string?
	if (it == _stringHash.end())
		return true;

	// Found an entry, does the string exist?
	if ((*it).second.string.first == false)
		return true;

	// Was the string set with a less exact pass?
	if ((*it).second.pass >= pass)
		return true;

	return false;
}

void StringTable::clearStrings()
{
	_stringHash.clear();
}

//
// Loads a language
//
void StringTable::loadLanguage(const char* code, bool exactMatch, int pass, char* lump,
                               size_t lumpLen)
{
	OScannerConfig config = {
	    "LANGUAGE", // lumpName
	    false,      // semiComments
	    true,       // cComments
	};
	OScanner os = OScanner::openBuffer(config, lump, lump + lumpLen);
	while (os.scan())
	{
		// Parse a language section.
		bool shouldParseSection = false;

		os.assertTokenIs("[");
		while (os.scan())
		{
			// Code to check against.
			char checkCode[4] = {'\0', '\0', '\0', '\0'};

			if (os.compareToken("]"))
			{
				break;
			}
			else if (os.compareToken("default"))
			{
				// Default has a speical ID.
				strncpy(checkCode, "**", 3);
			}
			else
			{
				// Turn the language into an ID.
				const std::string& lang = os.getToken();

				if (lang.length() == 2 || lang.length() == 3)
				{
					strncpy(checkCode, lang.c_str(), lang.length());
				}
				else
				{
					os.error("Language identifier must be 2 or 3 characters");
				}
			}

			if (exactMatch && strncmp(code, checkCode, 3) == 0)
			{
				shouldParseSection = true;
			}
			else if (!exactMatch && strncmp(code, checkCode, 2) == 0)
			{
				shouldParseSection = true;
			}
		}

		if (shouldParseSection)
		{
			// Parse all of the strings in this section.
			while (os.scan())
			{
				if (os.compareToken("["))
				{
					// We reached the end of the section.
					os.unScan();
					break;
				}

				// $ifgame() does not appear to be documented in the wiki,
				// but it causes the next string to only be set it the game
				// matches up.
				bool skip = false;
				if (os.compareToken("$"))
				{
					os.scan();
					os.assertTokenIs("ifgame");
					os.scan();
					os.assertTokenIs("(");
					os.scan();
					skip = !IfGameZDoom(os.getToken());
					os.scan();
					os.assertTokenIs(")");
					os.scan();
				}

				// String name
				const std::string& name = os.getToken();

				// If we can find the token, skip past the string
				if (!canSetPassString(pass, name))
				{
					while (os.scan())
					{
						if (os.compareToken(";"))
							break;
					}
					continue;
				}

				os.scan();
				os.assertTokenIs("=");

				// Grab the string value.
				std::string value;
				while (os.scan())
				{
					const std::string piece = os.getToken();
					if (piece.compare(";") == 0)
					{
						// Found the end of the string, next batter up.
						break;
					}

					value += piece;
				}

				replaceEscapes(value);
				if (skip)
				{
					continue;
				}
				setPassString(pass, name, value);
			}
		}
		else
		{
			// Skip past all of the strings in this section.
			while (os.scan())
			{
				if (os.compareToken("["))
				{
					// Found another section, parse it.
					os.unScan();
					break;
				}
			}
		}
	}
}

void StringTable::loadStringsLump(const int lump, const char* lumpname, const bool engOnly)
{
	// Can't use Z_Malloc this early, so we use raw new/delete.
	size_t len = W_LumpLength(lump);
	char* languageLump = new char[len + 1];
	W_ReadLump(lump, languageLump);
	languageLump[len] = '\0';

	// String replacement pass.  Strings in an later pass can be replaced
	// by a string in an earlier pass from another lump.
	int pass = 1;

	if (!engOnly)
	{
		// Load language-specific strings.
		for (size_t i = 0; i < ARRAY_LENGTH(::LanguageIDs); i++)
		{
			// Deconstruct code into something less confusing.
			char code[4];
			UNMAKE_ID(code, ::LanguageIDs[i]);

			// Language codes are up to three letters long.
			code[3] = '\0';

			// Try the full language code (enu).
			loadLanguage(code, true, pass++, languageLump, len);

			// Try the partial language code (en).
			code[2] = '\0';
			loadLanguage(code, true, pass++, languageLump, len);

			// Try an inexact match for all languages in the same family (en_).
			loadLanguage(code, false, pass++, languageLump, len);
		}
	}

	// Load string defaults.
	loadLanguage("**", true, pass++, languageLump, len);

	delete[] languageLump;
}

void StringTable::prepareIndexes()
{
	// All of the default strings have index numbers that represent their
	// position in the now-removed enumeration.  This function simply sets
	// them all up.
	for (size_t i = 0; i < ARRAY_LENGTH(::stringIndexes); i++)
	{
		OString name = *(::stringIndexes[i]);
		StringHash::iterator it = _stringHash.find(name);
		if (it == _stringHash.end())
		{
			TableEntry entry = {std::make_pair(false, ""), 0xFF, static_cast<int>(i)};
			_stringHash.emplace(name, entry);
		}
	}
}

void StringTable::replaceEscapes(std::string& str)
{
	size_t index = 0;

	for (;;)
	{
		// Find the initial slash.
		index = str.find("\\", index);
		if (index == std::string::npos || index == str.length() - 1)
			break;

		// Substitute the escape string.
		switch (str.at(index + 1))
		{
		case 'n':
			str.replace(index, 2, "\n");
			break;
		case '\\':
			str.replace(index, 2, "\\");
			break;
		}
		index += 1;
	}
}

//
// Dump all strings to the console.
//
// Sometimes a blunt instrument is what is necessary.
//
void StringTable::dumpStrings()
{
	for (const auto& [first, second] : _stringHash)
	{
		PrintFmt(PRINT_HIGH, "{} (pass: {}, index: {}) = {}\n", first,
		         second.pass, second.index, second.string.second);
	}
}

//
// See if a string exists in the table.
//
bool StringTable::hasString(const OString& name) const
{
	StringHash::const_iterator it = _stringHash.find(name);
	if (it == _stringHash.end())
		return false;
	if ((*it).second.string.first == false)
		return false;

	return true;
}

//
// Load strings from all LANGUAGE lumps in all loaded WAD files.
//
void StringTable::loadStrings(const bool engOnly)
{
	clearStrings();
	prepareIndexes();

	int lump = -1;

	lump = -1;
	while ((lump = W_FindLump("LANGUAGE", lump)) != -1)
	{
		loadStringsLump(lump, "LANGUAGE", engOnly);
	}
}

//
// Find a string with the same text.
//
const OString& StringTable::matchString(const OString& string) const
{
	for (const auto& [first, second] : _stringHash)
	{
		if (second.string.first == false)
			continue;
		if (second.string.second == string)
			return first;
	}

	static OString empty = "";
	return empty;
}

//
// Set a string to something specific by name.
//
// Overrides the existing string, if it exists.
//
void StringTable::setString(const OString& name, const OString& string)
{
	StringHash::iterator it = _stringHash.find(name);
	if (it == _stringHash.end())
	{
		// Stringtable entry does not exist, insert it.
		TableEntry entry = {std::make_pair(true, string), 0, -1};
		_stringHash.emplace(name, entry);
	}
	else
	{
		// Stringtable entry exists, update it.
		(*it).second.string.first = true;
		(*it).second.string.second = string;
	}
}

//
// Set a string to something specific by name.
//
// Does not set the string if it already exists.
//
void StringTable::setPassString(int pass, const OString& name, const OString& string)
{
	StringHash::iterator it = _stringHash.find(name);
	if (it == _stringHash.end())
	{
		// Stringtable entry does not exist.
		TableEntry entry = {std::make_pair(true, string), pass, -1};
		_stringHash.emplace(name, entry);
	}
	else
	{
		// Stringtable entry exists, but has not been set yet.
		(*it).second.string.first = true;
		(*it).second.string.second = string;
		(*it).second.pass = pass;
	}
}

//
// Number of entries in the stringtable.
//
size_t StringTable::size() const
{
	return _stringHash.size();
}

VERSION_CONTROL(stringtable_cpp, "$Id: cdb6d422d6c562242eac09934d43b046399bb03c $")
