/* SPDX-FileCopyrightText: 2026 Project Tick
 * SPDX-FileContributor: Project Tick
 * SPDX-License-Identifier: GPL-3.0-or-later WITH LicenseRef-MeshMC-MMCO-Module-Exception-1.0
 *
 *   MeshMC - A Custom Launcher for Minecraft
 *   Copyright (C) 2026 Project Tick
 *
 *   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 3 of the License, or
 *   (at your option) any later version, with the additional permission
 *   described in the MeshMC MMCO Module Exception 1.0.
 *
 *   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, see <https://www.gnu.org/licenses/>.
 *
 *   You should have received a copy of the MeshMC MMCO Module Exception 1.0
 *   along with this program.  If not, see <https://projecttick.org/licenses/>.
 */

#include "POTranslator.h"

#include <QDebug>
#include "FileSystem.h"

struct POEntry {
	QString text;
	bool fuzzy;
};

struct POTranslatorPrivate {
	QString filename;
	QHash<QByteArray, POEntry> mapping;
	QHash<QByteArray, POEntry> mapping_disambiguatrion;
	bool loaded = false;

	void reload();
};

class ParserArray : public QByteArray
{
  public:
	ParserArray(const QByteArray& in) : QByteArray(in) {}
	bool chomp(const char* data, int length)
	{
		if (startsWith(data)) {
			remove(0, length);
			return true;
		}
		return false;
	}
	bool chompString(QByteArray& appendHere)
	{
		QByteArray msg;
		bool escape = false;
		if (size() < 2) {
			qDebug() << "String fragment is too short";
			return false;
		}
		if (!startsWith('"')) {
			qDebug() << "String fragment does not start with \"";
			return false;
		}
		if (!endsWith('"')) {
			qDebug()
				<< "String fragment does not end with \", instead, there is"
				<< at(size() - 1);
			return false;
		}
		for (int i = 1; i < size() - 1; i++) {
			char c = operator[](i);
			if (escape) {
				switch (c) {
					case 'r':
						msg += '\r';
						break;
					case 'n':
						msg += '\n';
						break;
					case 't':
						msg += '\t';
						break;
					case 'v':
						msg += '\v';
						break;
					case 'a':
						msg += '\a';
						break;
					case 'b':
						msg += '\b';
						break;
					case 'f':
						msg += '\f';
						break;
					case '"':
						msg += '"';
						break;
					case '\\':
						msg.append('\\');
						break;
					case '0':
					case '1':
					case '2':
					case '3':
					case '4':
					case '5':
					case '6':
					case '7': {
						int octal_start = i;
						while ((c = operator[](i)) >= '0' && c <= '7') {
							i++;
							if (i == length() - 1) {
								qDebug() << "Something went bad while parsing "
											"an octal escape string...";
								return false;
							}
						}
						msg += mid(octal_start, i - octal_start).toUInt(0, 8);
						break;
					}
					case 'x': {
						// chomp the 'x'
						i++;
						int hex_start = i;
						while (isxdigit(operator[](i))) {
							i++;
							if (i == length() - 1) {
								qDebug() << "Something went bad while parsing "
											"a hex escape string...";
								return false;
							}
						}
						msg += mid(hex_start, i - hex_start).toUInt(0, 16);
						break;
					}
					default: {
						qDebug() << "Invalid escape sequence character:" << c;
						return false;
					}
				}
				escape = false;
			} else if (c == '\\') {
				escape = true;
			} else {
				msg += c;
			}
		}
		if (escape) {
			qDebug() << "Unterminated escape sequence...";
			return false;
		}
		appendHere += msg;
		return true;
	}
};

void POTranslatorPrivate::reload()
{
	QFile file(filename);
	if (!file.open(QFile::OpenMode::enum_type::ReadOnly |
				   QFile::OpenMode::enum_type::Text)) {
		qDebug() << "Failed to open PO file:" << filename;
		return;
	}

	QByteArray context;
	QByteArray disambiguation;
	QByteArray id;
	QByteArray str;
	bool fuzzy = false;
	bool nextFuzzy = false;

	enum class Mode {
		First,
		MessageContext,
		MessageId,
		MessageString
	} mode = Mode::First;

	int lineNumber = 0;
	QHash<QByteArray, POEntry> newMapping;
	QHash<QByteArray, POEntry> newMapping_disambiguation;
	auto endEntry = [&]() {
		auto strStr = QString::fromUtf8(str);
		// NOTE: PO header has empty id. We skip it.
		if (!id.isEmpty()) {
			auto normalKey = context + "|" + id;
			newMapping.insert(normalKey, {strStr, fuzzy});
			if (!disambiguation.isEmpty()) {
				auto disambiguationKey =
					context + "|" + id + "@" + disambiguation;
				newMapping_disambiguation.insert(disambiguationKey,
												 {strStr, fuzzy});
			}
		}
		context.clear();
		disambiguation.clear();
		id.clear();
		str.clear();
		fuzzy = nextFuzzy;
		nextFuzzy = false;
	};
	while (!file.atEnd()) {
		ParserArray line = file.readLine();
		if (line.endsWith('\n')) {
			line.resize(line.size() - 1);
		}
		if (line.endsWith('\r')) {
			line.resize(line.size() - 1);
		}

		if (!line.size()) {
			// NIL
		} else if (line[0] == '#') {
			if (line.contains(", fuzzy")) {
				nextFuzzy = true;
			}
		} else if (line.startsWith('"')) {
			QByteArray temp;
			QByteArray* out = &temp;

			switch (mode) {
				case Mode::First:
					qDebug() << "Unexpected escaped string during initial "
								"state... line:"
							 << lineNumber;
					return;
				case Mode::MessageString:
					out = &str;
					break;
				case Mode::MessageContext:
					out = &context;
					break;
				case Mode::MessageId:
					out = &id;
					break;
			}
			if (!line.chompString(*out)) {
				qDebug() << "Badly formatted string on line:" << lineNumber;
				return;
			}
		} else if (line.chomp("msgctxt ", 8)) {
			switch (mode) {
				case Mode::First:
					break;
				case Mode::MessageString:
					endEntry();
					break;
				case Mode::MessageContext:
				case Mode::MessageId:
					qDebug() << "Unexpected msgctxt line:" << lineNumber;
					return;
			}
			if (line.chompString(context)) {
				auto parts = context.split('|');
				context = parts[0];
				if (parts.size() > 1 && !parts[1].isEmpty()) {
					disambiguation = parts[1];
				}
				mode = Mode::MessageContext;
			}
		} else if (line.chomp("msgid ", 6)) {
			switch (mode) {
				case Mode::MessageContext:
				case Mode::First:
					break;
				case Mode::MessageString:
					endEntry();
					break;
				case Mode::MessageId:
					qDebug() << "Unexpected msgid line:" << lineNumber;
					return;
			}
			if (line.chompString(id)) {
				mode = Mode::MessageId;
			}
		} else if (line.chomp("msgstr ", 7)) {
			switch (mode) {
				case Mode::First:
				case Mode::MessageString:
				case Mode::MessageContext:
					qDebug() << "Unexpected msgstr line:" << lineNumber;
					return;
				case Mode::MessageId:
					break;
			}
			if (line.chompString(str)) {
				mode = Mode::MessageString;
			}
		} else {
			qDebug() << "I did not understand line: " << lineNumber << ":"
					 << QString::fromUtf8(line);
		}
		lineNumber++;
	}
	endEntry();
	mapping = std::move(newMapping);
	mapping_disambiguatrion = std::move(newMapping_disambiguation);
	loaded = true;
}

POTranslator::POTranslator(const QString& filename, QObject* parent)
	: QTranslator(parent)
{
	d = new POTranslatorPrivate;
	d->filename = filename;
	d->reload();
}

QString POTranslator::translate(const char* context, const char* sourceText,
								const char* disambiguation, int n) const
{
	if (disambiguation) {
		auto disambiguationKey = QByteArray(context) + "|" +
								 QByteArray(sourceText) + "@" +
								 QByteArray(disambiguation);
		auto iter = d->mapping_disambiguatrion.find(disambiguationKey);
		if (iter != d->mapping_disambiguatrion.end()) {
			auto& entry = *iter;
			if (entry.text.isEmpty()) {
				qDebug() << "Translation entry has no content:"
						 << disambiguationKey;
			}
			if (entry.fuzzy) {
				qDebug() << "Translation entry is fuzzy:" << disambiguationKey
						 << "->" << entry.text;
			}
			return entry.text;
		}
	}
	auto key = QByteArray(context) + "|" + QByteArray(sourceText);
	auto iter = d->mapping.find(key);
	if (iter != d->mapping.end()) {
		auto& entry = *iter;
		if (entry.text.isEmpty()) {
			qDebug() << "Translation entry has no content:" << key;
		}
		if (entry.fuzzy) {
			qDebug() << "Translation entry is fuzzy:" << key << "->"
					 << entry.text;
		}
		return entry.text;
	}
	return QString();
}

bool POTranslator::isEmpty() const
{
	return !d->loaded;
}
