/*
 * This file is part of OpenTTD.
 * OpenTTD 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, version 2.
 * OpenTTD 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. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see <https://www.gnu.org/licenses/old-licenses/gpl-2.0>.
 */

/** @file game_text.cpp Implementation of handling translated strings. */

#include "../stdafx.h"
#include "../strgen/strgen.h"
#include "../debug.h"
#include "../fileio_func.h"
#include "../tar_type.h"
#include "../script/squirrel_class.hpp"
#include "../strings_func.h"
#include "game_text.hpp"
#include "game.hpp"
#include "game_info.hpp"

#include "table/strings.h"
#include "../table/strgen_tables.h"

#include "../safeguards.h"

void CDECL StrgenWarningI(const std::string &msg)
{
	Debug(script, 0, "{}:{}: warning: {}", _strgen.file, _strgen.cur_line, msg);
	_strgen.warnings++;
}

void CDECL StrgenErrorI(const std::string &msg)
{
	Debug(script, 0, "{}:{}: error: {}", _strgen.file, _strgen.cur_line, msg);
	_strgen.errors++;
}

void CDECL StrgenFatalI(const std::string &msg)
{
	Debug(script, 0, "{}:{}: FATAL: {}", _strgen.file, _strgen.cur_line, msg);
	throw std::exception();
}

/**
 * Read all the raw language strings from the given file.
 * @param file The file to read from.
 * @return The raw strings, or nullptr upon error.
 */
LanguageStrings ReadRawLanguageStrings(const std::string &file)
{
	size_t to_read;
	auto fh = FioFOpenFile(file, "rb", Subdirectory::Gs, &to_read);
	if (!fh.has_value()) return LanguageStrings();

	auto pos = file.rfind(PATHSEPCHAR);
	if (pos == std::string::npos) return LanguageStrings();
	std::string langname = file.substr(pos + 1);

	/* Check for invalid empty filename */
	if (langname.empty() || langname.front() == '.') return LanguageStrings();

	LanguageStrings ret(langname.substr(0, langname.find('.')));

	char buffer[2048];
	while (to_read != 0 && fgets(buffer, sizeof(buffer), *fh) != nullptr) {
		std::string_view view{buffer};
		ret.lines.emplace_back(StrTrimView(view, StringConsumer::WHITESPACE_OR_NEWLINE));

		if (view.size() > to_read) {
			to_read = 0;
		} else {
			to_read -= view.size();
		}
	}

	return ret;
}


/** A reader that simply reads using fopen. */
struct StringListReader : StringReader {
	StringList::const_iterator p;   ///< The current location of the iteration.
	StringList::const_iterator end; ///< The end of the iteration.

	/**
	 * Create the reader.
	 * @param data        The data to fill during reading.
	 * @param strings     The language strings we are reading.
	 * @param master      Are we reading the master file?
	 * @param translation Are we reading a translation?
	 */
	StringListReader(StringData &data, const LanguageStrings &strings, bool master, bool translation) :
			StringReader(data, strings.language, master, translation), p(strings.lines.begin()), end(strings.lines.end())
	{
	}

	std::optional<std::string> ReadLine() override
	{
		if (this->p == this->end) return std::nullopt;
		return *this->p++;
	}
};

/** Class for writing an encoded language. */
struct TranslationWriter : LanguageWriter {
	StringList &strings; ///< The encoded strings.

	/**
	 * Writer for the encoded data.
	 * @param strings The string table to add the strings to.
	 */
	TranslationWriter(StringList &strings) : strings(strings)
	{
	}

	void WriteHeader(const LanguagePackHeader *) override
	{
		/* We don't use the header. */
	}

	void Finalise() override
	{
		/* Nothing to do. */
	}

	void WriteLength(size_t) override
	{
		/* We don't write the length. */
	}

	void Write(std::string_view buffer) override
	{
		this->strings.emplace_back(buffer);
	}
};

/** Class for writing the string IDs. */
struct StringNameWriter : HeaderWriter {
	StringList &strings; ///< The string names.

	/**
	 * Writer for the string names.
	 * @param strings The string table to add the strings to.
	 */
	StringNameWriter(StringList &strings) : strings(strings)
	{
	}

	void WriteStringID(const std::string &name, size_t stringid) override
	{
		if (stringid == this->strings.size()) this->strings.emplace_back(name);
	}

	void Finalise(const StringData &) override
	{
		/* Nothing to do. */
	}
};

/**
 * Scanner to find language files in a GameScript directory.
 */
class LanguageScanner : protected FileScanner {
private:
	std::weak_ptr<GameStrings> gs; ///< The (already) loaded game strings.
	std::string exclude; ///< The file name to exclude during scanning.

public:
	/**
	 * Initialise the scanner.
	 * @param gs The (already) loaded game strings to add to.
	 * @param exclude The file name to exclude during sanning.
	 */
	LanguageScanner(std::weak_ptr<GameStrings> gs, const std::string &exclude) : gs(gs), exclude(exclude) {}

	/**
	 * Actually run the scan.
	 * @param directory The directory to scan in.
	 */
	void Scan(const std::string &directory)
	{
		this->FileScanner::Scan(".txt", directory, false);
	}

	bool AddFile(const std::string &filename, size_t, const std::string &) override
	{
		if (exclude == filename) return true;

		auto ls = ReadRawLanguageStrings(filename);
		if (!ls.IsValid()) return false;

		if (auto sp = this->gs.lock()) {
			sp->raw_strings.push_back(std::move(ls));
			return true;
		}

		return false;
	}
};

/**
 * Load all translations that we know of.
 * @return Container with all (compiled) translations.
 */
static std::shared_ptr<GameStrings> LoadTranslations()
{
	const GameInfo *info = Game::GetInfo();
	assert(info != nullptr);
	std::string basename(info->GetMainScript());
	auto e = basename.rfind(PATHSEPCHAR);
	if (e == std::string::npos) return nullptr;
	basename.erase(e + 1);

	std::string filename = basename + "lang" PATHSEP "english.txt";
	if (!FioCheckFileExists(filename, Subdirectory::Gs)) return nullptr;

	auto ls = ReadRawLanguageStrings(filename);
	if (!ls.IsValid()) return nullptr;

	auto gs = std::make_shared<GameStrings>();
	try {
		gs->raw_strings.push_back(std::move(ls));

		/* Scan for other language files */
		LanguageScanner scanner(gs, filename);
		std::string ldir = basename + "lang" PATHSEP;

		const std::string tar_filename = info->GetTarFile();
		TarList::iterator iter;
		if (!tar_filename.empty() && (iter = _tar_list[Subdirectory::Gs].find(tar_filename)) != _tar_list[Subdirectory::Gs].end()) {
			/* The main script is in a tar file, so find all files that
			 * are in the same tar and add them to the langfile scanner. */
			for (const auto &[name, entry] : _tar_filelist[Subdirectory::Gs]) {
				/* Not in the same tar. */
				if (entry.tar_filename != iter->first) continue;

				/* Check the path and extension. */
				if (!name.starts_with(ldir)) continue;
				if (!name.ends_with(".txt")) continue;

				scanner.AddFile(name, 0, tar_filename);
			}
		} else {
			/* Scan filesystem */
			scanner.Scan(ldir);
		}

		gs->Compile();
		return gs;
	} catch (...) {
		return nullptr;
	}
}

static StringParam::ParamType GetParamType(const CmdStruct *cs)
{
	if (cs->value == SCC_RAW_STRING_POINTER) return StringParam::RAW_STRING;
	if (cs->value == SCC_STRING || cs != TranslateCmdForCompare(cs)) return StringParam::STRING;
	return StringParam::OTHER;
}

static void ExtractStringParams(const StringData &data, StringParamsList &params)
{
	for (size_t i = 0; i < data.max_strings; i++) {
		const LangString *ls = data.strings[i].get();

		if (ls != nullptr) {
			StringParams &param = params.emplace_back();
			ParsedCommandStruct pcs = ExtractCommandString(ls->english, false);

			for (auto it = pcs.consuming_commands.begin(); it != pcs.consuming_commands.end(); it++) {
				if (*it == nullptr) {
					/* Skip empty param unless a non empty param exist after it. */
					if (std::all_of(it, pcs.consuming_commands.end(), [](auto cs) { return cs == nullptr; })) break;
					param.emplace_back(StringParam::UNUSED, 1);
					continue;
				}
				const CmdStruct *cs = *it;
				param.emplace_back(GetParamType(cs), cs->consumes, cs->cmd);
			}
		}
	}
}

/** Compile the language. */
void GameStrings::Compile()
{
	StringData data(32);
	StringListReader master_reader(data, this->raw_strings[0], true, false);
	master_reader.ParseFile();
	if (_strgen.errors != 0) throw std::exception();

	this->version = data.Version();

	ExtractStringParams(data, this->string_params);

	StringNameWriter id_writer(this->string_names);
	id_writer.WriteHeader(data);

	for (const auto &p : this->raw_strings) {
		data.FreeTranslation();
		StringListReader translation_reader(data, p, false, p.language != "english");
		translation_reader.ParseFile();
		if (_strgen.errors != 0) throw std::exception();

		auto &strings = this->compiled_strings.emplace_back(p.language);
		TranslationWriter writer(strings.lines);
		writer.WriteLang(data);
	}
}

/** The currently loaded game strings. */
std::shared_ptr<GameStrings> _current_gamestrings_data = nullptr;

/**
 * Get the string pointer of a particular game string.
 * @param id The ID of the game string.
 * @return The encoded string.
 */
std::string_view GetGameStringPtr(StringIndexInTab id)
{
	if (_current_gamestrings_data == nullptr || _current_gamestrings_data->cur_language == nullptr || id.base() >= _current_gamestrings_data->cur_language->lines.size()) return GetStringPtr(STR_UNDEFINED);
	return _current_gamestrings_data->cur_language->lines[id];
}

/**
 * Get the string parameters of a particular game string.
 * @param id The ID of the game string.
 * @return The string parameters.
 */
const StringParams &GetGameStringParams(StringIndexInTab id)
{
	/* An empty result for STR_UNDEFINED. */
	static StringParams empty;

	if (id.base() >= _current_gamestrings_data->string_params.size()) return empty;
	return _current_gamestrings_data->string_params[id];
}

/**
 * Get the name of a particular game string.
 * @param id The ID of the game string.
 * @return The name of the string.
 */
const std::string &GetGameStringName(StringIndexInTab id)
{
	/* The name for STR_UNDEFINED. */
	static const std::string undefined = "STR_UNDEFINED";

	if (id.base() >= _current_gamestrings_data->string_names.size()) return undefined;
	return _current_gamestrings_data->string_names[id];
}

/**
 * Register the current translation to the Squirrel engine.
 * @param engine The engine to update/
 */
void RegisterGameTranslation(Squirrel &engine)
{
	_current_gamestrings_data = LoadTranslations();
	if (_current_gamestrings_data == nullptr) return;

	HSQUIRRELVM vm = engine.GetVM();
	sq_pushroottable(vm);
	sq_pushstring(vm, "GSText");
	if (SQ_FAILED(sq_get(vm, -2))) return;

	int idx = 0;
	for (const auto &p : _current_gamestrings_data->string_names) {
		sq_pushstring(vm, p);
		sq_pushinteger(vm, idx);
		sq_rawset(vm, -3);
		idx++;
	}

	sq_pop(vm, 2);

	ScriptText::SetPadParameterCount(vm);

	ReconsiderGameScriptLanguage();
}

/**
 * Reconsider the game script language, so we use the right one.
 */
void ReconsiderGameScriptLanguage()
{
	if (_current_gamestrings_data == nullptr) return;

	std::string language = FS2OTTD(_current_language->file.stem().native());
	for (auto &p : _current_gamestrings_data->compiled_strings) {
		if (p.language == language) {
			_current_gamestrings_data->cur_language = &p;
			return;
		}
	}

	_current_gamestrings_data->cur_language = &_current_gamestrings_data->compiled_strings[0];
}
