/*
 *  SPDX-License-Identifier: GPL-2.0-or-later
 *
 *  Copyright (C) 2020-2023  The DOSBox Staging Team
 *  Copyright (C) 2002-2021  The DOSBox 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.
 *
 *  You should have received a copy of the GNU General Public License along
 *  with this program; if not, write to the Free Software Foundation, Inc.,
 *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */

#include "dosbox.h"

#include <clocale>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <deque>
#include <map>

#include "std_filesystem.h"
#include <string>
#include <unordered_map>

#include "ansi_code_markup.h"
#include "checks.h"
#include "control.h"
#include "cross.h"
#include "fs_utils.h"
#include "setup.h"
#include "string_utils.h"
#include "support.h"

#define LINE_IN_MAXLEN 2048

CHECK_NARROWING();

static const char *msg_not_found = "Message not Found!\n";

class Message {
private:
	std::string markup_msg = {};
	std::string rendered_msg = {};

	std::map<uint16_t, std::string> rendered_msg_by_codepage = {};

	const char *CachedRenderString(const std::string &msg,
	                               std::map<uint16_t, std::string> &output_msg_by_codepage)
	{
		assert(msg.length());
		const uint16_t cp = get_utf8_code_page();
		if (output_msg_by_codepage[cp].empty()) {
			if (!utf8_to_dos(msg,
			                 output_msg_by_codepage[cp],
			                 UnicodeFallback::Box,
			                 cp)) {
				LOG_WARNING("LANG: Problem converting UTF8 string '%s' to DOS code page",
				            msg.c_str());
			}
			assert(output_msg_by_codepage[cp].length());
		}

		return output_msg_by_codepage[cp].c_str();
	}

public:
	Message() = delete;
	Message(const char *markup)
	{
		Set(markup);
	}

	const char *GetRaw()
	{
		assert(markup_msg.length());
		return markup_msg.c_str();
	}

	const char *GetRendered()
	{
		assert(markup_msg.length());
		if (rendered_msg.empty())
			rendered_msg = convert_ansi_markup(markup_msg.c_str());

		assert(rendered_msg.length());
		const uint16_t cp = get_utf8_code_page();
		if (rendered_msg_by_codepage[cp].empty()) {
			if (!utf8_to_dos(rendered_msg,
			                 rendered_msg_by_codepage[cp],
			                 UnicodeFallback::Box,
			                 cp)) {
				LOG_WARNING("LANG: Problem converting UTF8 string '%s' to DOS code page",
				            rendered_msg.c_str());
			}
			assert(rendered_msg_by_codepage[cp].length());
		}

		return rendered_msg_by_codepage[cp].c_str();
	}

	void Set(const char *markup)
	{
		assert(markup);
		markup_msg = markup;

		rendered_msg.clear();
		rendered_msg_by_codepage.clear();
	}
};

static std::unordered_map<std::string, Message> messages;
static std::deque<std::string> messages_order;

// Add the message if it doesn't exist yet
void MSG_Add(const char *name, const char *markup_msg)
{
	const auto &pair = messages.try_emplace(name, markup_msg);
	if (pair.second) // if the insertion was successful
		messages_order.emplace_back(name);
}

// Replace existing or add if it doesn't exist
static void msg_replace(const char *name, const char *markup_msg)
{
	auto it = messages.find(name);
	if (it == messages.end())
		MSG_Add(name, markup_msg);
	else
		it->second.Set(markup_msg);
}

static bool load_message_file(const std_fs::path &filename)
{
	if (filename.empty())
		return false;

	if (!path_exists(filename) || !is_readable(filename)) {
		LOG_MSG("LANG: Language file %s not found, skipping",
		        filename.string().c_str());
		return false;
	}

	FILE *mfile = fopen(filename.string().c_str(), "rt");
	if (!mfile) {
		LOG_MSG("LANG: Failed opening language file: %s, skipping",
		        filename.string().c_str());
		return false;
	}

	char linein[LINE_IN_MAXLEN];
	char name[LINE_IN_MAXLEN];
	char message[LINE_IN_MAXLEN * 10];
	/* Start out with empty strings */
	name[0] = 0;
	message[0] = 0;
	while (fgets(linein, LINE_IN_MAXLEN, mfile) != nullptr) {
		/* Parse the read line */
		/* First remove characters 10 and 13 from the line */
		char * parser=linein;
		char * writer=linein;
		while (*parser) {
			if (*parser!=10 && *parser!=13) {
				*writer++=*parser;
			}
			parser++;
		}
		*writer=0;
		/* New message name */
		if (linein[0]==':') {
			message[0] = 0;
			safe_strcpy(name, linein + 1);
			/* End of message marker */
		} else if (linein[0]=='.') {
			/* Replace/Add the message to the internal languagefile */
			/* Remove last newline (marker is \n.\n) */
			size_t ll = safe_strlen(message);
			// This second if should not be needed, but better be safe.
			if (ll && message[ll - 1] == '\n')
				message[ll - 1] = 0;
			msg_replace(name, message);
		} else {
			/* Normal message to be added */
			safe_strcat(message, linein);
			safe_strcat(message, "\n");
		}
	}
	fclose(mfile);
	LOG_MSG("LANG: Loaded language file: %s", filename.string().c_str());
	return true;
}

const char* MSG_Get(const char* requested_name)
{
	const auto it = messages.find(requested_name);
	if (it != messages.end()) {
		return it->second.GetRendered();
	}
	LOG_WARNING("LANG: Message '%s' not found", requested_name);
	return msg_not_found;
}

const char* MSG_GetRaw(const char* requested_name)
{
	const auto it = messages.find(requested_name);
	if (it != messages.end()) {
		return it->second.GetRaw();
	}
	LOG_WARNING("LANG: Message '%s' not found", requested_name);
	return msg_not_found;
}

bool MSG_Exists(const char *requested_name)
{
	return contains(messages, requested_name);
}

// Write the names and messages (in the order they were added) to the given location
bool MSG_Write(const char * location) {
	FILE *out = fopen(location, "w+t");
	if (!out)
		return false;

	for (const auto &name : messages_order)
		fprintf(out, ":%s\n%s\n.\n", name.data(), messages.at(name).GetRaw());

	fclose(out);
	return true;
}

// MSG_Init loads the requested language provided on the command line or
// from the language = conf setting.

// 1. The provided language can be an exact filename and path to the lng
//    file, which is the traditionnal method to load a language file.

// 2. It also supports the more convenient syntax without needing to provide a
//    filename or path: `-lang ru`. In this case, it constructs a path into the
//    platform's config path/translations/<lang>[.utf8].lng.

void MSG_Init([[maybe_unused]] Section_prop *section)
{
	// TODO: After migration to C++20 try to switch to constexpr
	static const std_fs::path subdir   = "translations";
	static const std::string extension = ".lng";

	const auto lang = control->GetLanguage();

	// If the language is english, then use the internal message
	if (lang.empty() || starts_with(lang, "en")) {
		LOG_MSG("LANG: Using internal English language messages");
		return;
	}

	bool result = false;
	if (ends_with(lang, ".lng"))
		result = load_message_file(GetResourcePath(subdir, lang));
	else
		// If a short-hand name was provided then add the file extension
		result = load_message_file(GetResourcePath(subdir, lang + extension));

	if (result)
		return;

	// If we got here, then the language was not found
	LOG_WARNING("LANG: The '%s' language resource file could not be loaded, using internal English messages",
	            lang.c_str());
}
