#include "fvupdater.h"
#include "fvupdatewindow.h"
#include "fvupdateconfirmdialog.h"
#include "fvplatform.h"
#include "fvignoredversions.h"
#include "fvavailableupdate.h"
#include <QApplication>
#include <QtNetwork>
#include <QMessageBox>
#include <QDesktopServices>
#include <QDebug>

#ifndef FV_APP_NAME
#	error "FV_APP_NAME is undefined (must have been defined by Fervor.pri)"
#endif
#ifndef FV_APP_VERSION
#	error "FV_APP_VERSION is undefined (must have been defined by Fervor.pri)"
#endif


#ifdef FV_DEBUG
	// Unit tests
#	include "fvversioncomparatortest.h"
#endif


FvUpdater* FvUpdater::m_Instance = 0;


FvUpdater* FvUpdater::sharedUpdater()
{
	static QMutex mutex;
	if (! m_Instance) {
		mutex.lock();

		if (! m_Instance) {
			m_Instance = new FvUpdater;
		}

		mutex.unlock();
	}

	return m_Instance;
}

void FvUpdater::drop()
{
	static QMutex mutex;
	mutex.lock();
	delete m_Instance;
	m_Instance = 0;
	mutex.unlock();
}

FvUpdater::FvUpdater() : QObject(0)
{
	m_reply = 0;
	m_updaterWindow = 0;
	m_updateConfirmationDialog = 0;
	m_proposedUpdate = 0;

	// Translation mechanism
	installTranslator();

#ifdef FV_DEBUG
	// Unit tests
	FvVersionComparatorTest* test = new FvVersionComparatorTest();
	test->runAll();
	delete test;
#endif

}

FvUpdater::~FvUpdater()
{
	if (m_proposedUpdate) {
		delete m_proposedUpdate;
		m_proposedUpdate = 0;
	}

	hideUpdateConfirmationDialog();
	hideUpdaterWindow();
}

void FvUpdater::installTranslator()
{
	QTranslator translator;
	QString locale = QLocale::system().name();
	translator.load(QString("fervor_") + locale);
    //QTextCodec::setCodecForTr(QTextCodec::codecForName("utf8"));
	qApp->installTranslator(&translator);
}

void FvUpdater::showUpdaterWindowUpdatedWithCurrentUpdateProposal()
{
	// Destroy window if already exists
	hideUpdaterWindow();

	// Create a new window
	m_updaterWindow = new FvUpdateWindow();
	m_updaterWindow->UpdateWindowWithCurrentProposedUpdate();
	m_updaterWindow->show();
}

void FvUpdater::hideUpdaterWindow()
{
	if (m_updaterWindow) {
		if (! m_updaterWindow->close()) {
			qWarning() << "Update window didn't close, leaking memory from now on";
		}

		// not deleting because of Qt::WA_DeleteOnClose

		m_updaterWindow = 0;
	}
}

void FvUpdater::updaterWindowWasClosed()
{
	// (Re-)nullify a pointer to a destroyed QWidget or you're going to have a bad time.
	m_updaterWindow = 0;
}


void FvUpdater::showUpdateConfirmationDialogUpdatedWithCurrentUpdateProposal()
{
	// Destroy dialog if already exists
	hideUpdateConfirmationDialog();

	// Create a new window
	m_updateConfirmationDialog = new FvUpdateConfirmDialog();
	m_updateConfirmationDialog->UpdateWindowWithCurrentProposedUpdate();
	m_updateConfirmationDialog->show();
}

void FvUpdater::hideUpdateConfirmationDialog()
{
	if (m_updateConfirmationDialog) {
		if (! m_updateConfirmationDialog->close()) {
			qWarning() << "Update confirmation dialog didn't close, leaking memory from now on";
		}

		// not deleting because of Qt::WA_DeleteOnClose

		m_updateConfirmationDialog = 0;
	}
}

void FvUpdater::updateConfirmationDialogWasClosed()
{
	// (Re-)nullify a pointer to a destroyed QWidget or you're going to have a bad time.
	m_updateConfirmationDialog = 0;
}


void FvUpdater::SetFeedURL(QUrl feedURL)
{
	m_feedURL = feedURL;
}

void FvUpdater::SetFeedURL(QString feedURL)
{
	SetFeedURL(QUrl(feedURL));
}

QString FvUpdater::GetFeedURL()
{
	return m_feedURL.toString();
}

FvAvailableUpdate* FvUpdater::GetProposedUpdate()
{
	return m_proposedUpdate;
}


void FvUpdater::InstallUpdate()
{
	qDebug() << "Install update";

	showUpdateConfirmationDialogUpdatedWithCurrentUpdateProposal();
}

void FvUpdater::SkipUpdate()
{
	qDebug() << "Skip update";

	FvAvailableUpdate* proposedUpdate = GetProposedUpdate();
	if (! proposedUpdate) {
		qWarning() << "Proposed update is NULL (shouldn't be at this point)";
		return;
	}

	// Start ignoring this particular version
	FVIgnoredVersions::IgnoreVersion(proposedUpdate->GetEnclosureVersion());

	hideUpdaterWindow();
	hideUpdateConfirmationDialog();	// if any; shouldn't be shown at this point, but who knows
}

void FvUpdater::RemindMeLater()
{
	qDebug() << "Remind me later";

	hideUpdaterWindow();
	hideUpdateConfirmationDialog();	// if any; shouldn't be shown at this point, but who knows
}

void FvUpdater::UpdateInstallationConfirmed()
{
	qDebug() << "Confirm update installation";

	FvAvailableUpdate* proposedUpdate = GetProposedUpdate();
	if (! proposedUpdate) {
		qWarning() << "Proposed update is NULL (shouldn't be at this point)";
		return;
	}

	// Open a link
	if (! QDesktopServices::openUrl(proposedUpdate->GetEnclosureUrl())) {
		showErrorDialog(tr("Unable to open this link in a browser. Please do it manually."), true);
		return;
	}

	hideUpdaterWindow();
	hideUpdateConfirmationDialog();
}

void FvUpdater::UpdateInstallationNotConfirmed()
{
	qDebug() << "Do not confirm update installation";

	hideUpdateConfirmationDialog();	// if any; shouldn't be shown at this point, but who knows
	// leave the "update proposal window" inact
}


bool FvUpdater::CheckForUpdates(bool silentAsMuchAsItCouldGet)
{
	if (m_feedURL.isEmpty()) {
		qCritical() << "Please set feed URL via setFeedURL() before calling CheckForUpdates().";
		return false;
	}

	m_silentAsMuchAsItCouldGet = silentAsMuchAsItCouldGet;

	// Check if application's organization name and domain are set, fail otherwise
	// (nowhere to store QSettings to)
	if (QApplication::organizationName().isEmpty()) {
		qCritical() << "QApplication::organizationName is not set. Please do that.";
		return false;
	}
	if (QApplication::organizationDomain().isEmpty()) {
		qCritical() << "QApplication::organizationDomain is not set. Please do that.";
		return false;
	}

	// Set application name / version is not set yet
	if (QApplication::applicationName().isEmpty()) {
		QString appName = QString::fromUtf8(FV_APP_NAME);
		qWarning() << "QApplication::applicationName is not set, setting it to '" << appName << "'";
		QApplication::setApplicationName(appName);
	}
	if (QApplication::applicationVersion().isEmpty()) {
		QString appVersion = QString::fromUtf8(FV_APP_VERSION);
		qWarning() << "QApplication::applicationVersion is not set, setting it to '" << appVersion << "'";
		QApplication::setApplicationVersion(appVersion);
	}

	cancelDownloadFeed();
	m_httpRequestAborted = false;
	startDownloadFeed(m_feedURL);

	return true;
}

bool FvUpdater::CheckForUpdatesSilent()
{
	return CheckForUpdates(true);
}

bool FvUpdater::CheckForUpdatesNotSilent()
{
	return CheckForUpdates(false);
}


void FvUpdater::startDownloadFeed(QUrl url)
{
	m_xml.clear();

	QNetworkRequest request;
 	request.setHeader(QNetworkRequest::ContentTypeHeader, "application/xml");
 	request.setHeader(QNetworkRequest::UserAgentHeader, QApplication::applicationName());
 	request.setUrl(url);

	m_reply = m_qnam.get(request);

	connect(m_reply, SIGNAL(readyRead()), this, SLOT(httpFeedReadyRead()));
	connect(m_reply, SIGNAL(downloadProgress(qint64, qint64)), this, SLOT(httpFeedUpdateDataReadProgress(qint64, qint64)));
	connect(m_reply, SIGNAL(finished()), this, SLOT(httpFeedDownloadFinished()));
}

void FvUpdater::cancelDownloadFeed()
{
	if (m_reply) {
		m_httpRequestAborted = true;
		m_reply->abort();
	}
}

void FvUpdater::httpFeedReadyRead()
{
	// this slot gets called every time the QNetworkReply has new data.
	// We read all of its new data and write it into the file.
	// That way we use less RAM than when reading it at the finished()
	// signal of the QNetworkReply
	m_xml.addData(m_reply->readAll());
}

void FvUpdater::httpFeedUpdateDataReadProgress(qint64 bytesRead,
											   qint64 totalBytes)
{
	Q_UNUSED(bytesRead);
	Q_UNUSED(totalBytes);

	if (m_httpRequestAborted) {
		return;
	}
}

void FvUpdater::httpFeedDownloadFinished()
{
	if (m_httpRequestAborted) {
		m_reply->deleteLater();
		return;
	}

	QVariant redirectionTarget = m_reply->attribute(QNetworkRequest::RedirectionTargetAttribute);
	if (m_reply->error()) {

		// Error.
		showErrorDialog(tr("Feed download failed: %1.").arg(m_reply->errorString()), false);

	} else if (! redirectionTarget.isNull()) {
		QUrl newUrl = m_feedURL.resolved(redirectionTarget.toUrl());

		m_feedURL = newUrl;
		m_reply->deleteLater();

		startDownloadFeed(m_feedURL);
		return;

	} else {

		// Done.
		xmlParseFeed();

	}

	m_reply->deleteLater();
	m_reply = 0;
}

bool FvUpdater::xmlParseFeed()
{
	QString currentTag, currentQualifiedTag;

	QString xmlTitle, xmlLink, xmlReleaseNotesLink, xmlPubDate, xmlEnclosureUrl,
			xmlEnclosureVersion, xmlEnclosurePlatform, xmlEnclosureType;
	unsigned long xmlEnclosureLength = 0;

	// Parse
	while (! m_xml.atEnd()) {

		m_xml.readNext();

		if (m_xml.isStartElement()) {

			currentTag = m_xml.name().toString();
			currentQualifiedTag = m_xml.qualifiedName().toString();

			if (m_xml.name() == "item") {

				xmlTitle.clear();
				xmlLink.clear();
				xmlReleaseNotesLink.clear();
				xmlPubDate.clear();
				xmlEnclosureUrl.clear();
				xmlEnclosureVersion.clear();
				xmlEnclosurePlatform.clear();
				xmlEnclosureLength = 0;
				xmlEnclosureType.clear();

			} else if (m_xml.name() == "enclosure") {

				QXmlStreamAttributes attribs = m_xml.attributes();

				if (attribs.hasAttribute("fervor:platform")) {

					if (FvPlatform::CurrentlyRunningOnPlatform(attribs.value("fervor:platform").toString().trimmed())) {

						xmlEnclosurePlatform = attribs.value("fervor:platform").toString().trimmed();

						if (attribs.hasAttribute("url")) {
							xmlEnclosureUrl = attribs.value("url").toString().trimmed();
						} else {
							xmlEnclosureUrl = "";
						}

                        // First check for Sparkle's version, then overwrite with Fervor's version (if any)
                        if (attribs.hasAttribute("sparkle:version")) {
                            QString candidateVersion = attribs.value("sparkle:version").toString().trimmed();
                            if (! candidateVersion.isEmpty()) {
                                xmlEnclosureVersion = candidateVersion;
                            }
                        }
                        if (attribs.hasAttribute("fervor:version")) {
                            QString candidateVersion = attribs.value("fervor:version").toString().trimmed();
                            if (! candidateVersion.isEmpty()) {
                                xmlEnclosureVersion = candidateVersion;
                            }
                        }

						if (attribs.hasAttribute("length")) {
							xmlEnclosureLength = attribs.value("length").toString().toLong();
						} else {
							xmlEnclosureLength = 0;
						}
						if (attribs.hasAttribute("type")) {
							xmlEnclosureType = attribs.value("type").toString().trimmed();
						} else {
							xmlEnclosureType = "";
						}

					}

				}

			}

		} else if (m_xml.isEndElement()) {

			if (m_xml.name() == "item") {

				// That's it - we have analyzed a single <item> and we'll stop
				// here (because the topmost is the most recent one, and thus
				// the newest version.

				return searchDownloadedFeedForUpdates(xmlTitle,
													  xmlLink,
													  xmlReleaseNotesLink,
													  xmlPubDate,
													  xmlEnclosureUrl,
													  xmlEnclosureVersion,
													  xmlEnclosurePlatform,
													  xmlEnclosureLength,
													  xmlEnclosureType);

			}

		} else if (m_xml.isCharacters() && ! m_xml.isWhitespace()) {

			if (currentTag == "title") {
				xmlTitle += m_xml.text().toString().trimmed();

			} else if (currentTag == "link") {
				xmlLink += m_xml.text().toString().trimmed();

			} else if (currentQualifiedTag == "sparkle:releaseNotesLink") {
				xmlReleaseNotesLink += m_xml.text().toString().trimmed();

			} else if (currentTag == "pubDate") {
				xmlPubDate += m_xml.text().toString().trimmed();

			}

		}

		if (m_xml.error() && m_xml.error() != QXmlStreamReader::PrematureEndOfDocumentError) {

			showErrorDialog(tr("Feed parsing failed: %1 %2.").arg(QString::number(m_xml.lineNumber()), m_xml.errorString()), false);
			return false;

		}
	}

    // No updates were found if we're at this point
    // (not a single <item> element found)
    showInformationDialog(tr("No updates were found."), false);

	return false;
}


bool FvUpdater::searchDownloadedFeedForUpdates(QString xmlTitle,
											   QString xmlLink,
											   QString xmlReleaseNotesLink,
											   QString xmlPubDate,
											   QString xmlEnclosureUrl,
											   QString xmlEnclosureVersion,
											   QString xmlEnclosurePlatform,
											   unsigned long xmlEnclosureLength,
											   QString xmlEnclosureType)
{
    qDebug() << "Title:" << xmlTitle;
    qDebug() << "Link:" << xmlLink;
    qDebug() << "Release notes link:" << xmlReleaseNotesLink;
    qDebug() << "Pub. date:" << xmlPubDate;
    qDebug() << "Enclosure URL:" << xmlEnclosureUrl;
    qDebug() << "Enclosure version:" << xmlEnclosureVersion;
    qDebug() << "Enclosure platform:" << xmlEnclosurePlatform;
    qDebug() << "Enclosure length:" << xmlEnclosureLength;
    qDebug() << "Enclosure type:" << xmlEnclosureType;

	// Validate
	if (xmlReleaseNotesLink.isEmpty()) {
		if (xmlLink.isEmpty()) {
			showErrorDialog(tr("Feed error: \"release notes\" link is empty"), false);
			return false;
		} else {
			xmlReleaseNotesLink = xmlLink;
		}
	} else {
		xmlLink = xmlReleaseNotesLink;
	}
	if (! (xmlLink.startsWith("http://") || xmlLink.startsWith("https://"))) {
		showErrorDialog(tr("Feed error: invalid \"release notes\" link"), false);
		return false;
	}
	if (xmlEnclosureUrl.isEmpty() || xmlEnclosureVersion.isEmpty() || xmlEnclosurePlatform.isEmpty()) {
        showErrorDialog(tr("Feed error: invalid \"enclosure\" with the download link"), false);
		return false;
	}

	// Relevant version?
	if (FVIgnoredVersions::VersionIsIgnored(xmlEnclosureVersion)) {
		qDebug() << "Version '" << xmlEnclosureVersion << "' is ignored, too old or something like that.";

		showInformationDialog(tr("No updates were found."), false);

		return true;	// Things have succeeded when you think of it.
	}


	//
	// Success! At this point, we have found an update that can be proposed
	// to the user.
	//

	if (m_proposedUpdate) {
		delete m_proposedUpdate; m_proposedUpdate = 0;
	}
	m_proposedUpdate = new FvAvailableUpdate();
	m_proposedUpdate->SetTitle(xmlTitle);
	m_proposedUpdate->SetReleaseNotesLink(xmlReleaseNotesLink);
	m_proposedUpdate->SetPubDate(xmlPubDate);
	m_proposedUpdate->SetEnclosureUrl(xmlEnclosureUrl);
	m_proposedUpdate->SetEnclosureVersion(xmlEnclosureVersion);
	m_proposedUpdate->SetEnclosurePlatform(xmlEnclosurePlatform);
	m_proposedUpdate->SetEnclosureLength(xmlEnclosureLength);
	m_proposedUpdate->SetEnclosureType(xmlEnclosureType);

	// Show "look, there's an update" window
	showUpdaterWindowUpdatedWithCurrentUpdateProposal();

	return true;
}


void FvUpdater::showErrorDialog(QString message, bool showEvenInSilentMode)
{
	if (m_silentAsMuchAsItCouldGet) {
		if (! showEvenInSilentMode) {
			// Don't show errors in the silent mode
			return;
		}
	}

	QMessageBox dlFailedMsgBox;
	dlFailedMsgBox.setIcon(QMessageBox::Critical);
	dlFailedMsgBox.setText(tr("Error"));
	dlFailedMsgBox.setInformativeText(message);
	dlFailedMsgBox.exec();
}

void FvUpdater::showInformationDialog(QString message, bool showEvenInSilentMode)
{
	if (m_silentAsMuchAsItCouldGet) {
		if (! showEvenInSilentMode) {
			// Don't show information dialogs in the silent mode
			return;
		}
	}

	QMessageBox dlInformationMsgBox;
	dlInformationMsgBox.setIcon(QMessageBox::Information);
	dlInformationMsgBox.setText(tr("Information"));
	dlInformationMsgBox.setInformativeText(message);
	dlInformationMsgBox.exec();
}