/**********************************************************************
 * LeechCraft - modular cross-platform feature rich internet client.
 * Copyright (C) 2006-2014  Georg Rudoy
 *
 * Boost Software License - Version 1.0 - August 17th, 2003
 *
 * Permission is hereby granted, free of charge, to any person or organization
 * obtaining a copy of the software and accompanying documentation covered by
 * this license (the "Software") to use, reproduce, display, distribute,
 * execute, and transmit the Software, and to prepare derivative works of the
 * Software, and to permit third-parties to whom the Software is furnished to
 * do so, all subject to the following:
 *
 * The copyright notices in the Software and this entire statement, including
 * the above license grant, this restriction and the following disclaimer,
 * must be included in all copies of the Software, in whole or in part, and
 * all derivative works of the Software, unless such copies or derivative
 * works are solely in the form of machine-executable object code generated by
 * a source language processor.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
 * SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
 * FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 **********************************************************************/

#include "repoinfofetcher.h"
#include <QTimer>
#include <util/sys/paths.h>
#include <util/xpc/util.h>
#include "core.h"
#include "xmlparsers.h"

namespace LeechCraft
{
namespace LackMan
{
	RepoInfoFetcher::RepoInfoFetcher (QObject *parent)
	: QObject (parent)
	{
	}

	void RepoInfoFetcher::FetchFor (QUrl url)
	{
		QString path = url.path ();
		if (!path.endsWith ("/Repo.xml.gz"))
		{
			path.append ("/Repo.xml.gz");
			url.setPath (path);
		}

		QString location = Util::GetTemporaryName ("lackman_XXXXXX.gz");

		QUrl goodUrl = url;
		goodUrl.setPath (goodUrl.path ().remove ("/Repo.xml.gz"));

		PendingRI pri =
		{
			goodUrl,
			location
		};

		Entity e = Util::MakeEntity (url,
				location,
				LeechCraft::Internal |
					LeechCraft::DoNotNotifyUser |
					LeechCraft::DoNotSaveInHistory |
					LeechCraft::NotPersistent |
					LeechCraft::DoNotAnnounceEntity);
		int id = -1;
		QObject *pr;
		emit delegateEntity (e, &id, &pr);
		if (id == -1)
		{
			emit gotEntity (Util::MakeNotification (tr ("Error fetching repository"),
					tr ("Could not find plugin to fetch repository information for %1.")
						.arg (url.toString ()),
					PCritical_));
			return;
		}

		PendingRIs_ [id] = pri;

		connect (pr,
				SIGNAL (jobFinished (int)),
				this,
				SLOT (handleRIFinished (int)),
				Qt::UniqueConnection);
		connect (pr,
				SIGNAL (jobRemoved (int)),
				this,
				SLOT (handleRIRemoved (int)),
				Qt::UniqueConnection);
		connect (pr,
				SIGNAL (jobError (int, IDownload::Error)),
				this,
				SLOT (handleRIError (int, IDownload::Error)),
				Qt::UniqueConnection);
	}

	void RepoInfoFetcher::FetchComponent (QUrl url, int repoId, const QString& component)
	{
		if (!url.path ().endsWith ("/Packages.xml.gz"))
			url.setPath (url.path () + "/Packages.xml.gz");

		QString location = Util::GetTemporaryName ("lackman_XXXXXX.gz");

		PendingComponent pc =
		{
			url,
			location,
			component,
			repoId
		};

		Entity e = Util::MakeEntity (url,
				location,
				LeechCraft::Internal |
					LeechCraft::DoNotNotifyUser |
					LeechCraft::DoNotSaveInHistory |
					LeechCraft::NotPersistent |
					LeechCraft::DoNotAnnounceEntity);
		int id = -1;
		QObject *pr;
		emit delegateEntity (e, &id, &pr);
		if (id == -1)
		{
			emit gotEntity (Util::MakeNotification (tr ("Error fetching component"),
					tr ("Could not find plugin to fetch component information at %1.")
						.arg (url.toString ()),
					PCritical_));
			return;
		}

		PendingComponents_ [id] = pc;

		connect (pr,
				SIGNAL (jobFinished (int)),
				this,
				SLOT (handleComponentFinished (int)),
				Qt::UniqueConnection);
		connect (pr,
				SIGNAL (jobRemoved (int)),
				this,
				SLOT (handleComponentRemoved (int)),
				Qt::UniqueConnection);
		connect (pr,
				SIGNAL (jobError (int, IDownload::Error)),
				this,
				SLOT (handleComponentError (int, IDownload::Error)),
				Qt::UniqueConnection);
	}

	void RepoInfoFetcher::ScheduleFetchPackageInfo (const QUrl& url,
			const QString& name,
			const QList<QString>& newVers,
			int componentId)
	{
		ScheduledPackageFetch f =
		{
			url,
			name,
			newVers,
			componentId
		};

		if (ScheduledPackages_.isEmpty ())
			QTimer::singleShot (0,
					this,
					SLOT (rotatePackageFetchQueue ()));

		ScheduledPackages_ << f;
	}

	void RepoInfoFetcher::FetchPackageInfo (const QUrl& baseUrl,
			const QString& packageName,
			const QList<QString>& newVersions,
			int componentId)
	{
		QString location = Util::GetTemporaryName ("lackman_XXXXXX.gz");
		QUrl packageUrl = baseUrl;
		packageUrl.setPath (packageUrl.path () +
				Core::Instance ().NormalizePackageName (packageName) + ".xml.gz");

		PendingPackage pp =
		{
			packageUrl,
			baseUrl,
			location,
			packageName,
			newVersions,
			componentId
		};

		Entity e = Util::MakeEntity (packageUrl,
				location,
				LeechCraft::Internal |
					LeechCraft::DoNotNotifyUser |
					LeechCraft::DoNotSaveInHistory |
					LeechCraft::NotPersistent |
					LeechCraft::DoNotAnnounceEntity);
		int id = -1;
		QObject *pr;
		emit delegateEntity (e, &id, &pr);
		if (id == -1)
		{
			emit gotEntity (Util::MakeNotification (tr ("Error fetching package information"),
					tr ("Could not find plugin to fetch package information at %1.")
						.arg (packageUrl.toString ()),
					PCritical_));
			return;
		}

		PendingPackages_ [id] = pp;

		connect (pr,
				SIGNAL (jobFinished (int)),
				this,
				SLOT (handlePackageFinished (int)),
				Qt::UniqueConnection);
		connect (pr,
				SIGNAL (jobRemoved (int)),
				this,
				SLOT (handlePackageRemoved (int)),
				Qt::UniqueConnection);
		connect (pr,
				SIGNAL (jobError (int, IDownload::Error)),
				this,
				SLOT (handlePackageError (int, IDownload::Error)),
				Qt::UniqueConnection);
	}

	void RepoInfoFetcher::rotatePackageFetchQueue ()
	{
		if (ScheduledPackages_.isEmpty ())
			return;

		const auto& f = ScheduledPackages_.takeFirst ();
		FetchPackageInfo (f.BaseUrl_, f.PackageName_, f.NewVersions_, f.ComponentId_);

		if (!ScheduledPackages_.isEmpty ())
			QTimer::singleShot (50, this, SLOT (rotatePackageFetchQueue ()));
	}

	void RepoInfoFetcher::handleRIFinished (int id)
	{
		if (!PendingRIs_.contains (id))
			return;

		PendingRI pri = PendingRIs_.take (id);

		QString name = pri.Location_;
		QProcess *unarch = new QProcess (this);
		unarch->setProperty ("URL", pri.URL_);
		unarch->setProperty ("Filename", name);
		connect (unarch,
				SIGNAL (finished (int, QProcess::ExitStatus)),
				this,
				SLOT (handleRepoUnarchFinished (int, QProcess::ExitStatus)));
		connect (unarch,
				SIGNAL (error (QProcess::ProcessError)),
				this,
				SLOT (handleUnarchError (QProcess::ProcessError)));
#ifdef Q_OS_WIN32
		unarch->start ("7za", QStringList ("e") << "-so" << name);
#else
		unarch->start ("gunzip", QStringList ("-c") << name);
#endif
	}

	void RepoInfoFetcher::handleRIRemoved (int id)
	{
		if (!PendingRIs_.contains (id))
			return;

		PendingRIs_.remove (id);
	}

	void RepoInfoFetcher::handleRIError (int id, IDownload::Error)
	{
		if (!PendingRIs_.contains (id))
			return;

		PendingRI pri = PendingRIs_.take (id);

		QFile::remove (pri.Location_);

		emit gotEntity (Util::MakeNotification (tr ("Error fetching repository"),
				tr ("Error downloading file from %1.")
					.arg (pri.URL_.toString ()),
				PCritical_));
	}

	void RepoInfoFetcher::handleComponentFinished (int id)
	{
		if (!PendingComponents_.contains (id))
			return;

		PendingComponent pc = PendingComponents_.take (id);

		QProcess *unarch = new QProcess (this);
		unarch->setProperty ("Component", pc.Component_);
		unarch->setProperty ("Filename", pc.Location_);
		unarch->setProperty ("URL", pc.URL_);
		unarch->setProperty ("RepoID", pc.RepoID_);
		connect (unarch,
				SIGNAL (finished (int, QProcess::ExitStatus)),
				this,
				SLOT (handleComponentUnarchFinished (int, QProcess::ExitStatus)));
		connect (unarch,
				SIGNAL (error (QProcess::ProcessError)),
				this,
				SLOT (handleUnarchError (QProcess::ProcessError)));
#ifdef Q_OS_WIN32
		unarch->start ("7za", QStringList ("e") << "-so" << pc.Location_);
#else
		unarch->start ("gunzip", QStringList ("-c") << pc.Location_);
#endif
	}

	void RepoInfoFetcher::handleComponentRemoved (int id)
	{
		if (!PendingComponents_.contains (id))
			return;

		PendingComponents_.remove (id);
	}

	void RepoInfoFetcher::handleComponentError (int id, IDownload::Error)
	{
		if (!PendingComponents_.contains (id))
			return;

		PendingComponent pc = PendingComponents_.take (id);

		QFile::remove (pc.Location_);

		emit gotEntity (Util::MakeNotification (tr ("Error fetching component"),
				tr ("Error downloading file from %1.")
					.arg (pc.URL_.toString ()),
				PCritical_));
	}

	void RepoInfoFetcher::handlePackageFinished (int id)
	{
		if (!PendingPackages_.contains (id))
			return;

		PendingPackage pp = PendingPackages_ [id];

		QProcess *unarch = new QProcess (this);
		unarch->setProperty ("Filename", pp.Location_);
		unarch->setProperty ("URL", pp.URL_);
		unarch->setProperty ("TaskID", id);
		connect (unarch,
				SIGNAL (finished (int, QProcess::ExitStatus)),
				this,
				SLOT (handlePackageUnarchFinished (int, QProcess::ExitStatus)));
		connect (unarch,
				SIGNAL (error (QProcess::ProcessError)),
				this,
				SLOT (handleUnarchError (QProcess::ProcessError)));
#ifdef Q_OS_WIN32
		unarch->start ("7za", QStringList ("e") << "-so" << pp.Location_);
#else
		unarch->start ("gunzip", QStringList ("-c") << pp.Location_);
#endif
	}

	void RepoInfoFetcher::handlePackageRemoved (int id)
	{
		if (!PendingPackages_.contains (id))
			return;

		PendingPackages_.remove (id);
	}

	void RepoInfoFetcher::handlePackageError (int id, IDownload::Error)
	{
		if (!PendingPackages_.contains (id))
			return;

		PendingPackage pp = PendingPackages_.take (id);

		QFile::remove (pp.Location_);

		emit gotEntity (Util::MakeNotification (tr ("Error fetching package"),
				tr ("Error fetching package from %1.")
					.arg (pp.URL_.toString ()),
				PCritical_));
	}

	void RepoInfoFetcher::handleRepoUnarchFinished (int exitCode,
			QProcess::ExitStatus)
	{
		sender ()->deleteLater ();

		if (exitCode)
		{
			emit gotEntity (Util::MakeNotification (tr ("Repository unpack error"),
					tr ("Unable to unpack the repository file. gunzip error: %1. "
						"Problematic file is at %2.")
						.arg (exitCode)
						.arg (sender ()->property ("Filename").toString ()),
					PCritical_));
			return;
		}

		QByteArray data = qobject_cast<QProcess*> (sender ())->readAllStandardOutput ();
		QFile::remove (sender ()->property ("Filename").toString ());

		RepoInfo info;
		try
		{
			info = ParseRepoInfo (sender ()->property ("URL").toUrl (), QString (data));
		}
		catch (const QString& error)
		{
			qWarning () << Q_FUNC_INFO
					<< error;
			emit gotEntity (Util::MakeNotification (tr ("Repository parse error"),
					tr ("Unable to parse repository description: %1.")
						.arg (error),
					PCritical_));
			return;
		}

		emit infoFetched (info);
	}

	void RepoInfoFetcher::handleComponentUnarchFinished (int exitCode,
			QProcess::ExitStatus)
	{
		sender ()->deleteLater ();

		if (exitCode)
		{
			emit gotEntity (Util::MakeNotification (tr ("Component unpack error"),
					tr ("Unable to unpack the component file. gunzip error: %1. "
						"Problematic file is at %2.")
						.arg (exitCode)
						.arg (sender ()->property ("Filename").toString ()),
					PCritical_));
			return;
		}

		QByteArray data = qobject_cast<QProcess*> (sender ())->readAllStandardOutput ();
		QFile::remove (sender ()->property ("Filename").toString ());

		PackageShortInfoList infos;
		try
		{
			infos = ParseComponent (data);
		}
		catch (const std::exception& e)
		{
			qWarning () << Q_FUNC_INFO
					<< e.what ();
			emit gotEntity (Util::MakeNotification (tr ("Component parse error"),
					tr ("Unable to parse component %1 description file. "
						"More information is available in logs.")
						.arg (sender ()->property ("Component").toString ()),
					PCritical_));
			return;
		}

		emit componentFetched (infos,
				sender ()->property ("Component").toString (),
				sender ()->property ("RepoID").toInt ());
	}

	void RepoInfoFetcher::handlePackageUnarchFinished (int exitCode,
			QProcess::ExitStatus)
	{
		sender ()->deleteLater ();

		int id = sender ()->property ("TaskID").toInt ();
		PendingPackage pp = PendingPackages_.take (id);

		if (exitCode)
		{
			emit gotEntity (Util::MakeNotification (tr ("Component unpack error"),
					tr ("Unable to unpack the component file. gunzip error: %1. "
						"Problematic file is at %2.")
						.arg (exitCode)
						.arg (sender ()->property ("Filename").toString ()),
					PCritical_));
			return;
		}

		QByteArray data = qobject_cast<QProcess*> (sender ())->readAllStandardOutput ();
		QFile::remove (sender ()->property ("Filename").toString ());

		PackageInfo packageInfo;
		try
		{
			packageInfo = ParsePackage (data, pp.BaseURL_, pp.PackageName_, pp.NewVersions_);
		}
		catch (const std::exception& e)
		{
			qWarning () << Q_FUNC_INFO
					<< e.what ();
			emit gotEntity (Util::MakeNotification (tr ("Package parse error"),
					tr ("Unable to parse package description file. "
						"More information is available in logs."),
					PCritical_));
			return;
		}

		emit packageFetched (packageInfo, pp.ComponentId_);
	}

	void RepoInfoFetcher::handleUnarchError (QProcess::ProcessError error)
	{
		sender ()->deleteLater ();

		qWarning () << Q_FUNC_INFO
				<< "unable to unpack for"
				<< sender ()->property ("URL").toUrl ()
				<< sender ()->property ("Filename").toString ()
				<< "with"
				<< error
				<< qobject_cast<QProcess*> (sender ())->readAllStandardError ();
		emit gotEntity (Util::MakeNotification (tr ("Component unpack error"),
					tr ("Unable to unpack file. Exit code: %1. "
						"Problematic file is at %2.")
						.arg (error)
						.arg (sender ()->property ("Filename").toString ()),
					PCritical_));
	}
}
}
