#include "TranslationsModel.h"

#include <QCoreApplication>
#include <QTranslator>
#include <QLocale>
#include <QDir>
#include <QLibraryInfo>
#include <QDebug>
#include <FileSystem.h>
#include <net/NetJob.h>
#include <Env.h>
#include <net/URLConstants.h>

const static QLatin1Literal defaultLangCode("en");

struct Language
{
	QString key;
	QLocale locale;
	bool updated;
};

struct TranslationsModel::Private
{
	QDir m_dir;

	// initial state is just english
	QVector<Language> m_languages = {{defaultLangCode, QLocale(defaultLangCode), false}};
	QString m_selectedLanguage = defaultLangCode;
	std::unique_ptr<QTranslator> m_qt_translator;
	std::unique_ptr<QTranslator> m_app_translator;

	std::shared_ptr<Net::Download> m_index_task;
	QString m_downloadingTranslation;
	NetJobPtr m_dl_job;
	NetJobPtr m_index_job;
	QString m_nextDownload;
};

TranslationsModel::TranslationsModel(QString path, QObject* parent): QAbstractListModel(parent)
{
	d.reset(new Private);
	d->m_dir.setPath(path);
	loadLocalIndex();
}

TranslationsModel::~TranslationsModel()
{
}

QVariant TranslationsModel::data(const QModelIndex& index, int role) const
{
	if (!index.isValid())
		return QVariant();

	int row = index.row();

	if (row < 0 || row >= d->m_languages.size())
		return QVariant();

	switch (role)
	{
	case Qt::DisplayRole:
		return d->m_languages[row].locale.nativeLanguageName();
	case Qt::UserRole:
		return d->m_languages[row].key;
	default:
		return QVariant();
	}
}

int TranslationsModel::rowCount(const QModelIndex& parent) const
{
	return d->m_languages.size();
}

Language * TranslationsModel::findLanguage(const QString& key)
{
	auto found = std::find_if(d->m_languages.begin(), d->m_languages.end(), [&](Language & lang)
	{
		return lang.key == key;
	});
	if(found == d->m_languages.end())
	{
		return nullptr;
	}
	else
	{
		return found;
	}
}

bool TranslationsModel::selectLanguage(QString key)
{
	QString &langCode = key;
	auto langPtr = findLanguage(key);
	if(!langPtr)
	{
		qWarning() << "Selected invalid language" << key << ", defaulting to" << defaultLangCode;
		langCode = defaultLangCode;
	}
	else
	{
		langCode = langPtr->key;
	}

	// uninstall existing translators if there are any
	if (d->m_app_translator)
	{
		QCoreApplication::removeTranslator(d->m_app_translator.get());
		d->m_app_translator.reset();
	}
	if (d->m_qt_translator)
	{
		QCoreApplication::removeTranslator(d->m_qt_translator.get());
		d->m_qt_translator.reset();
	}

	/*
	 * FIXME: potential source of crashes:
	 * In a multithreaded application, the default locale should be set at application startup, before any non-GUI threads are created.
	 * This function is not reentrant.
	 */
	QLocale locale(langCode);
	QLocale::setDefault(locale);

	// if it's the default UI language, finish
	if(langCode == defaultLangCode)
	{
		d->m_selectedLanguage = langCode;
		return true;
	}

	// otherwise install new translations
	bool successful = false;
	// FIXME: this is likely never present. FIX IT.
	d->m_qt_translator.reset(new QTranslator());
	if (d->m_qt_translator->load("qt_" + langCode, QLibraryInfo::location(QLibraryInfo::TranslationsPath)))
	{
		qDebug() << "Loading Qt Language File for" << langCode.toLocal8Bit().constData() << "...";
		if (!QCoreApplication::installTranslator(d->m_qt_translator.get()))
		{
			qCritical() << "Loading Qt Language File failed.";
			d->m_qt_translator.reset();
		}
		else
		{
			successful = true;
		}
	}
	else
	{
		d->m_qt_translator.reset();
	}

	d->m_app_translator.reset(new QTranslator());
	if (d->m_app_translator->load("mmc_" + langCode, d->m_dir.path()))
	{
		qDebug() << "Loading Application Language File for" << langCode.toLocal8Bit().constData() << "...";
		if (!QCoreApplication::installTranslator(d->m_app_translator.get()))
		{
			qCritical() << "Loading Application Language File failed.";
			d->m_app_translator.reset();
		}
		else
		{
			successful = true;
		}
	}
	else
	{
		d->m_app_translator.reset();
	}
	d->m_selectedLanguage = langCode;
	return successful;
}

QModelIndex TranslationsModel::selectedIndex()
{
	auto found = findLanguage(d->m_selectedLanguage);
	if(found)
	{
		// QVector iterator freely converts to pointer to contained type
		return index(found - d->m_languages.begin(), 0, QModelIndex());
	}
	return QModelIndex();
}

QString TranslationsModel::selectedLanguage()
{
	return d->m_selectedLanguage;
}

void TranslationsModel::downloadIndex()
{
	if(d->m_index_job || d->m_dl_job)
	{
		return;
	}
	qDebug() << "Downloading Translations Index...";
	d->m_index_job.reset(new NetJob("Translations Index"));
	MetaEntryPtr entry = ENV.metacache()->resolveEntry("translations", "index");
	d->m_index_task = Net::Download::makeCached(QUrl("http://files.multimc.org/translations/index"), entry);
	d->m_index_job->addNetAction(d->m_index_task);
	connect(d->m_index_job.get(), &NetJob::failed, this, &TranslationsModel::indexFailed);
	connect(d->m_index_job.get(), &NetJob::succeeded, this, &TranslationsModel::indexRecieved);
	d->m_index_job->start();
}

void TranslationsModel::indexRecieved()
{
	qDebug() << "Got translations index!";
	d->m_index_job.reset();
	loadLocalIndex();
	if(d->m_selectedLanguage != defaultLangCode)
	{
		downloadTranslation(d->m_selectedLanguage);
	}
}

void TranslationsModel::loadLocalIndex()
{
	QByteArray data;
	try
	{
		data = FS::read(d->m_dir.absoluteFilePath("index"));
	}
	catch (Exception &e)
	{
		qCritical() << "Translations Download Failed: index file not readable";
		return;
	}
	QVector<Language> languages;
	QList<QByteArray> lines = data.split('\n');
	// add the default english.
	languages.append({defaultLangCode, QLocale(defaultLangCode), true});
	for (const auto line : lines)
	{
		if(!line.isEmpty())
		{
			auto str = QString::fromLatin1(line);
			str.remove(".qm");
			languages.append({str, QLocale(str), false});
		}
	}
	beginResetModel();
	d->m_languages.swap(languages);
	endResetModel();
}

void TranslationsModel::updateLanguage(QString key)
{
	if(key == defaultLangCode)
	{
		qWarning() << "Cannot update builtin language" << key;
		return;
	}
	auto found = findLanguage(key);
	if(!found)
	{
		qWarning() << "Cannot update invalid language" << key;
		return;
	}
	if(!found->updated)
	{
		downloadTranslation(key);
	}
}

void TranslationsModel::downloadTranslation(QString key)
{
	if(d->m_dl_job)
	{
		d->m_nextDownload = key;
		return;
	}
	d->m_downloadingTranslation = key;
	MetaEntryPtr entry = ENV.metacache()->resolveEntry("translations", "mmc_" + key + ".qm");
	entry->setStale(true);
	d->m_dl_job.reset(new NetJob("Translation for " + key));
	d->m_dl_job->addNetAction(Net::Download::makeCached(QUrl(URLConstants::TRANSLATIONS_BASE_URL + key + ".qm"), entry));
	connect(d->m_dl_job.get(), &NetJob::succeeded, this, &TranslationsModel::dlGood);
	connect(d->m_dl_job.get(), &NetJob::failed, this, &TranslationsModel::dlFailed);
	d->m_dl_job->start();
}

void TranslationsModel::downloadNext()
{
	if(!d->m_nextDownload.isEmpty())
	{
		downloadTranslation(d->m_nextDownload);
		d->m_nextDownload.clear();
	}
}

void TranslationsModel::dlFailed(QString reason)
{
	qCritical() << "Translations Download Failed:" << reason;
	d->m_dl_job.reset();
	downloadNext();
}

void TranslationsModel::dlGood()
{
	qDebug() << "Got translation:" << d->m_downloadingTranslation;

	if(d->m_downloadingTranslation == d->m_selectedLanguage)
	{
		selectLanguage(d->m_selectedLanguage);
	}
	d->m_dl_job.reset();
	downloadNext();
}

void TranslationsModel::indexFailed(QString reason)
{
	qCritical() << "Translations Index Download Failed:" << reason;
	d->m_index_job.reset();
}