/*
This file is part of Telegram Desktop,
an unofficial desktop messaging app, see https://telegram.org

Telegram Desktop 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.

It 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.

Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE
Copyright (c) 2014 John Preston, https://tdesktop.com
*/
#include "genlang.h"

#include <QtCore/QtPlugin>

#ifdef Q_OS_WIN
Q_IMPORT_PLUGIN(QWindowsIntegrationPlugin)
#endif

#ifdef Q_OS_MAC
Q_IMPORT_PLUGIN(QCocoaIntegrationPlugin)
Q_IMPORT_PLUGIN(QDDSPlugin)
Q_IMPORT_PLUGIN(QICNSPlugin)
Q_IMPORT_PLUGIN(QICOPlugin)
Q_IMPORT_PLUGIN(QJp2Plugin)
Q_IMPORT_PLUGIN(QMngPlugin)
Q_IMPORT_PLUGIN(QTgaPlugin)
Q_IMPORT_PLUGIN(QTiffPlugin)
Q_IMPORT_PLUGIN(QWbmpPlugin)
Q_IMPORT_PLUGIN(QWebpPlugin)
#endif

typedef unsigned int uint32;

QString layoutDirection;
typedef QMap<QString, QString> LangKeys;
LangKeys keys;
typedef QVector<QString> KeysOrder;
KeysOrder keysOrder;

bool skipWhitespaces(const char *&from, const char *end) {
	while (from < end && (*from == ' ' || *from == '\n' || *from == '\t' || *from == '\r')) {
		++from;
	}
	return (from < end);
}

bool skipComment(const char *&from, const char *end) {
	if (from >= end) return false;
	if (*from == '/') {
		if (from + 1 >= end) return true;
		if (*(from + 1) == '*') {
			from += 2;
			while (from + 1 < end && (*from != '*' || *(from + 1) != '/')) {
				++from;
			}
			from += 2;
			return (from < end);
		} else if (*(from + 1) == '/') {
			from += 2;
			while (from < end && *from != '\n' && *from != '\r') {
				++from;
			}
            if (from < end) ++from;
			return true;
		} else {
			return true;
		}
	}
	return true;
}

bool skipJunk(const char *&from, const char *end) {
	const char *start;
	do {
		start = from;
		if (!skipWhitespaces(from, end)) return false;
		if (!skipComment(from, end)) throw Exception("Unexpected end of comment!");
	} while (start != from);
	return true;
}

void readKeyValue(const char *&from, const char *end) {
	if (!skipJunk(from, end)) return;

	const char *nameStart = from;
	while (from < end && ((*from >= 'a' && *from <= 'z') || (*from >= 'A' && *from <= 'Z') || *from == '_' || (*from >= '0' && *from <= '9'))) {
		++from;
	}

	QString varName = QString::fromUtf8(nameStart, int(from - nameStart));

	if (!skipJunk(from, end)) throw Exception("Unexpected end of file!");
	if (*from != ':') throw Exception(QString("':' expected after '%1'").arg(varName));

	if (!skipJunk(++from, end)) throw Exception("Unexpected end of file!");
	if (*from != '"') throw Exception(QString("Expected string after '%1:'").arg(varName));

	QByteArray varValue;
	const char *start = ++from;
	while (from < end && *from != '"') {
		if (*from == '\\') {
			if (from + 1 >= end) throw Exception("Unexpected end of file!");
			if (*(from + 1) == '"' || *(from + 1) == '\\') {
				if (from > start) varValue.append(start, int(from - start));
				start = ++from;
			}
		}
		++from;
	}
	if (from >= end) throw Exception("Unexpected end of file!");
	if (from > start) varValue.append(start, int(from - start));

	if (!skipJunk(++from, end)) throw Exception("Unexpected end of file!");
	if (*from != ';') throw Exception(QString("';' expected after '%1: \"value\"'").arg(varName));

	skipJunk(++from, end);

	if (varName == "direction") {
		if (varValue == "LTR" || varValue == "RTL") {
			layoutDirection = QString::fromUtf8(varValue);
		} else {
			throw Exception(QString("Unexpected value for 'direction' key: '%1'").arg(QString::fromUtf8(varValue)));
		}
	} else if (varName.midRef(0, 4) != "lng_") {
		throw Exception(QString("Bad key '%1'").arg(varName));
	} else if (keys.constFind(varName) != keys.cend()) {
		throw Exception(QString("Key doubled '%1'").arg(varName));
	} else {
		keys.insert(varName, QString::fromUtf8(varValue));
		keysOrder.push_back(varName);
	}
}

QString escapeCpp(const QString &key, QString value, bool wideChar) {
	if (value.isEmpty()) return "QString()";
	value = value.replace('\\', "\\\\").replace('\n', "\\n").replace('\r', "").replace('"', "\\\"");
	QString res;
	res.reserve(value.size() * 10);
	bool instr = false;
	for (const QChar *ch = value.constData(), *e = value.constData() + value.size(); ch != e; ++ch) {
		if (ch->unicode() < 32) {
			throw Exception(QString("Bad value for key '%1'").arg(key));
		} else if (ch->unicode() > 127) {
			if (instr) {
				res.append('"');
				instr = false;
			}
			res.append(' ');
			if (wideChar) {
				res.append('L').append('"').append('\\').append('x').append(QString("%1").arg(ch->unicode(), 4, 16, QChar('0'))).append('"');
			} else {
				res.append('"');
				QByteArray utf(QString(*ch).toUtf8());
				for (const unsigned char *uch = (const unsigned char *)utf.constData(), *ue = (const unsigned char *)utf.constData() + utf.size(); uch != ue; ++uch) {
					res.append('\\').append('x').append(QString("%1").arg(ushort(*uch), 2, 16, QChar('0')));
				}
				res.append('"');
			}
		} else {
			if (!instr) {
				res.append(' ');
				if (wideChar) res.append('L');
				res.append('"');
				instr = true;
			}
			res.append(*ch);
		}
	}
	if (instr) res.append('"');
	return (wideChar ? "qsl(" : "QString::fromUtf8(") + res.mid(wideChar ? 2 : 1) + ")";
}

void writeCppKey(QTextStream &tcpp, const QString &key, const QString &val) {
	QString wide = escapeCpp(key, val, true), utf = escapeCpp(key, val, false);
	if (wide.indexOf(" L\"") < 0) {
		tcpp << "\t\t\tset(" << key << ", " << wide << ");\n";
	} else {
		tcpp << "#ifdef Q_OS_WIN\n";
		tcpp << "\t\t\tset(" << key << ", " << wide << ");\n";
		tcpp << "#else\n";
		tcpp << "\t\t\tset(" << key << ", " << utf << ");\n";
		tcpp << "#endif\n";
	}
}

bool genLang(const QString &lang_in, const QString &lang_out) {
	QString lang_cpp = lang_out + ".cpp", lang_h = lang_out + ".h";
	QFile f(lang_in);
	if (!f.open(QIODevice::ReadOnly)) {
		cout << "Could not open styles input file '" << lang_in.toUtf8().constData() << "'!\n";
		QCoreApplication::exit(1);
		return false;
	}

	QByteArray blob = f.readAll();
	const char *text = blob.constData(), *end = blob.constData() + blob.size();
	f.close();

	try {
		while (text != end) {
			readKeyValue(text, end);
		}

		QByteArray cppText, hText;
		{
			QTextStream tcpp(&cppText), th(&hText);
			tcpp.setCodec("ISO 8859-1");
			th.setCodec("ISO 8859-1");
			th << "\
/*\n\
Created from \'/Resources/lang.txt\' by \'/MetaLang\' project\n\
\n\
WARNING! All changes made in this file will be lost!\n\
\n\
This file is part of Telegram Desktop,\n\
an unofficial desktop messaging app, see https://telegram.org\n\
\n\
Telegram Desktop is free software: you can redistribute it and/or modify\n\
it under the terms of the GNU General Public License as published by\n\
the Free Software Foundation, either version 3 of the License, or\n\
(at your option) any later version.\n\
\n\
It is distributed in the hope that it will be useful,\n\
but WITHOUT ANY WARRANTY; without even the implied warranty of\n\
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n\
GNU General Public License for more details.\n\
\n\
Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE\n\
Copyright (c) 2014 John Preston, https://tdesktop.com\n\
*/\n";
			th << "#pragma once\n\n";
			th << "enum LangKey {\n";
			for (int i = 0, l = keysOrder.size(); i < l; ++i) {
				th << "\t" << keysOrder[i] << (i ? "" : " = 0") << ",\n";
			}
			th << "\n\tlng_keys_cnt\n";
			th << "};\n\n";
			th << "QString lang(LangKey key);\n";
			th << "inline QString langDayOfMonth(const QDate &date) {\n";
			th << "\tint32 month = date.month(), day = date.day();\n";
			th << "\treturn (month > 0 && month <= 12) ? lang(lng_month_day).replace(qsl(\"{month}\"), lang(LangKey(lng_month1 + month - 1))).replace(qsl(\"{day}\"), QString::number(day)) : qsl(\"{err}\");\n";
			th << "}\n\n";
			th << "inline QString langDayOfWeek(const QDate &date) {\n";
			th << "\tint32 day = date.dayOfWeek();\n";
			th << "\treturn (day > 0 && day <= 7) ? lang(LangKey(lng_weekday1 + day - 1)) : qsl(\"{err}\");\n";
			th << "}\n\n";
			th << "Qt::LayoutDirection langDir();\n\n";
			th << "class LangLoader {\n";
			th << "public:\n";
			th << "\tconst QString &errors() const;\n";
			th << "\tconst QString &warnings() const;\n\n";
			th << "protected:\n";
			th << "\tLangLoader() : _checked(false) {\n";
			th << "\t\tmemset(_found, 0, sizeof(_found));\n";
			th << "\t}\n\n";
			th << "\tbool feedKeyValue(const QString &key, const QString &value);\n\n";
			th << "\tvoid error(const QString &text) {\n";
			th << "\t\t_err.push_back(text);\n";
			th << "\t}\n";
			th << "\tvoid warning(const QString &text) {\n";
			th << "\t\t_warn.push_back(text);\n";
			th << "\t}\n\n";
			th << "private:\n";
			th << "\tmutable QStringList _err, _warn;\n";
			th << "\tmutable QString _errors, _warnings;\n";
			th << "\tmutable bool _checked;\n";
			th << "\tbool _found[lng_keys_cnt];\n\n";
			th << "\tLangLoader(const LangLoader &);\n";
			th << "\tLangLoader &operator=(const LangLoader &);\n";
			th << "};\n";

			tcpp << "\
/*\n\
Created from \'/Resources/lang.txt\' by \'/MetaLang\' project\n\
\n\
WARNING! All changes made in this file will be lost!\n\
\n\
This file is part of Telegram Desktop,\n\
an unofficial desktop messaging app, see https://telegram.org\n\
\n\
Telegram Desktop is free software: you can redistribute it and/or modify\n\
it under the terms of the GNU General Public License as published by\n\
the Free Software Foundation, either version 3 of the License, or\n\
(at your option) any later version.\n\
\n\
It is distributed in the hope that it will be useful,\n\
but WITHOUT ANY WARRANTY; without even the implied warranty of\n\
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n\
GNU General Public License for more details.\n\
\n\
Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE\n\
Copyright (c) 2014 John Preston, https://tdesktop.com\n\
*/\n";
			tcpp << "#include \"stdafx.h\"\n#include \"lang.h\"\n\n";
			tcpp << "namespace {\n";
			tcpp << "\tQt::LayoutDirection _langDir = Qt::" << (layoutDirection == "LTR" ? "LeftToRight" : "RightToLeft") << ";\n";
			tcpp << "\tconst char *_langKeyNames[lng_keys_cnt + 1] = {\n";
			for (int i = 0, l = keysOrder.size(); i < l; ++i) {
				tcpp << "\t\t\"" << keysOrder[i] << "\",\n";
			}
			tcpp << "\t\t\"lng_keys_cnt\"\n";
			tcpp << "\t};\n\n";
			tcpp << "\tQString _langValues[lng_keys_cnt + 1];\n\n";
			tcpp << "\tvoid set(LangKey key, const QString &val) {\n";
			tcpp << "\t\t_langValues[key] = val;\n";
			tcpp << "\t}\n\n";
			tcpp << "\tclass LangInit {\n";
			tcpp << "\tpublic:\n";
			tcpp << "\t\tLangInit() {\n";
			for (int i = 0, l = keysOrder.size(); i < l; ++i) {
				writeCppKey(tcpp, keysOrder[i], keys[keysOrder[i]]);
			}
			tcpp << "\t\t\tset(lng_keys_cnt, QString());\n";
			tcpp << "\t\t}\n";
			tcpp << "\t};\n\n";
			tcpp << "\tLangInit _langInit;\n";
			tcpp << "}\n\n";

			tcpp << "QString lang(LangKey key) {\n";
			tcpp << "\treturn _langValues[(key < 0 || key > lng_keys_cnt) ? lng_keys_cnt : key];\n";
			tcpp << "}\n\n";

			tcpp << "Qt::LayoutDirection langDir() {\n";
			tcpp << "\treturn _langDir;\n";
			tcpp << "}\n\n";

			tcpp << "bool LangLoader::feedKeyValue(const QString &key, const QString &value) {\n";
			tcpp << "\tif (key == qsl(\"direction\")) {\n";
			tcpp << "\t\tif (value == qsl(\"LTR\")) {\n";
			tcpp << "\t\t\t_langDir = Qt::LeftToRight;\n";
			tcpp << "\t\t\treturn true;\n";
			tcpp << "\t\t} else if (value == qsl(\"RTL\")) {\n";
			tcpp << "\t\t\t_langDir = Qt::RightToLeft;\n";
			tcpp << "\t\t\treturn true;\n";
			tcpp << "\t\t} else {\n";
			tcpp << "\t\t\t_err.push_back(qsl(\"Bad value for 'direction' key.\"));\n";
			tcpp << "\t\t\treturn false;\n";
			tcpp << "\t\t}\n";
			tcpp << "\t}\n";
			tcpp << "\tif (key.size() < 5 || key.midRef(0, 4) != qsl(\"lng_\")) {\n";
			tcpp << "\t\t_err.push_back(qsl(\"Bad key name '%1'\").arg(key));\n";
			tcpp << "\t\treturn false;\n";
			tcpp << "\t}\n\n";
			if (!keys.isEmpty()) {
				QString tab("\t");
				tcpp << "\tLangKey keyIndex = lng_keys_cnt;\n";
				tcpp << "\tconst QChar *ch = key.constData(), *e = key.constData() + key.size();\n";
				QString current("lng_");
				int depth = current.size();
				tcpp << "\tswitch ((ch + " << depth << ")->unicode()) {\n";
				for (LangKeys::const_iterator i = keys.cbegin(), j = i + 1, e = keys.cend(); i != e; ++i) {
					QString key = i.key();
					while (key.midRef(0, depth) != current) {
						tcpp << tab.repeated(depth - 3) << "}\n";
						current.chop(1);
						--depth;
						tcpp << tab.repeated(depth - 3) << "break;\n";
					}
					do {
						if (key == current) break;
							
						QChar ich = i.key().at(current.size());
						tcpp << tab.repeated(current.size() - 3) << "case '" << ich << "':\n";
						if (j == e || ich != ((j.key().size() > depth) ? j.key().at(depth) : 0)) {
							if (key == current + ich) {
								tcpp << tab.repeated(depth - 3) << "\tif (ch + " << (depth + 1) << " == e) keyIndex = " << key << ";\n";
							} else {
								tcpp << tab.repeated(depth - 3) << "\tif (key.midRef(" << (depth + 1) << ") == qsl(\"" << i.key().mid(depth + 1) << "\")) keyIndex = " << key << ";\n";
							}
							tcpp << tab.repeated(depth - 3) << "break;\n";
							break;
						}

						++depth;
						current += ich;

						if (key == current) {
							tcpp << tab.repeated(depth - 3) << "if (ch + " << depth << " == e) {\n";
							tcpp << tab.repeated(depth - 3) << "\tkeyIndex = " << key << ";\n";
							tcpp << tab.repeated(depth - 3) << "}\n";
						}

						tcpp << tab.repeated(depth - 3) << "if (ch + " << depth << " < e) switch ((ch + " << depth << ")->unicode()) {\n";
					} while (true);
					++j;
				}
				while (QString("lng_") != current) {
					tcpp << tab.repeated(depth - 3) << "}\n";
					current.chop(1);
					--depth;
					tcpp << tab.repeated(depth - 3) << "break;\n";
				}
				tcpp << "\t}\n\n";
				tcpp << "\tif (keyIndex < lng_keys_cnt) {\n";
				tcpp << "\t\t_found[keyIndex] = 1;\n";
				tcpp << "\t\t_langValues[keyIndex] = value;\n";
				tcpp << "\t\treturn true;\n";
				tcpp << "\t}\n\n";
			}
			tcpp << "\t_err.push_back(qsl(\"Unknown key name '%1'\").arg(key));\n";
			tcpp << "\treturn false;\n";
			tcpp << "}\n\n";

			tcpp << "const QString &LangLoader::errors() const {\n";
			tcpp << "\tif (_errors.isEmpty() && !_err.isEmpty()) {\n";
			tcpp << "\t\t_errors = _err.join('\\n');\n";
			tcpp << "\t}\n";
			tcpp << "\treturn _errors;\n";
			tcpp << "}\n\n";

			tcpp << "const QString &LangLoader::warnings() const {\n";
			tcpp << "\tif (!_checked) {\n";
			tcpp << "\t\tfor (int32 i = 0; i < lng_keys_cnt; ++i) {\n";
			tcpp << "\t\t\tif (!_found[i]) {\n";
			tcpp << "\t\t\t\t_warn.push_back(qsl(\"No value found for key '%1'\").arg(_langKeyNames[i]));\n";
			tcpp << "\t\t\t}\n";
			tcpp << "\t\t}\n";
			tcpp << "\t\t_checked = true;\n";
			tcpp << "\t}\n";
			tcpp << "\tif (_warnings.isEmpty() && !_warn.isEmpty()) {\n";
			tcpp << "\t\t_warnings = _warn.join('\\n');\n";
			tcpp << "\t}\n";
			tcpp << "\treturn _warnings;\n";
			tcpp << "}\n";
		}

		QFile cpp(lang_cpp), h(lang_h);
		bool write_cpp = true, write_h = true;
		if (cpp.open(QIODevice::ReadOnly)) {
			QByteArray wasCpp = cpp.readAll();
			if (wasCpp.size() == cppText.size()) {
				if (!memcmp(wasCpp.constData(), cppText.constData(), cppText.size())) {
					write_cpp = false;
				}
			}
			cpp.close();
		}
		if (write_cpp) {
			cout << "lang.cpp updated, writing " << keysOrder.size() << " rows.\n";
			if (!cpp.open(QIODevice::WriteOnly)) throw Exception("Could not open lang.cpp for writing!");
			if (cpp.write(cppText) != cppText.size()) throw Exception("Could not open lang.cpp for writing!");
		}
		if (h.open(QIODevice::ReadOnly)) {
			QByteArray wasH = h.readAll();
			if (wasH.size() == hText.size()) {
				if (!memcmp(wasH.constData(), hText.constData(), hText.size())) {
					write_h = false;
				}
			}
			h.close();
		}
		if (write_h) {
			cout << "lang.h updated, writing " << keysOrder.size() << " rows.\n";
			if (!h.open(QIODevice::WriteOnly)) throw Exception("Could not open lang.h for writing!");
			if (h.write(hText) != hText.size()) throw Exception("Could not open lang.h for writing!");
		}
	} catch (exception &e) {
		cout << e.what() << "\n";
		QCoreApplication::exit(1);
		return false;
	}
	return true;
}
