/**********************************************************************
 * 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 <interfaces/core/ientitymanager.h>
#include "core.h"
#include "xmlparsers.h"
#include "lackmanutil.h"

namespace LeechCraft
{
namespace LackMan
{
	RepoInfoFetcher::RepoInfoFetcher (const ICoreProxy_ptr& proxy, QObject *parent)
	: QObject { parent }
	, Proxy_ { proxy }
	{
	}

	namespace
	{
		template<typename PendingF>
		void FetchImpl (QHash<int, std::result_of_t<PendingF (QString)>>& map,
				PendingF&& factory,
				const QUrl& url,
				const ICoreProxy_ptr& proxy,
				QObject *object,
				const char *finished,
				const char *removed,
				const char *error)
		{
			const auto& location = Util::GetTemporaryName ("lackman_XXXXXX.gz");

			const auto iem = proxy->GetEntityManager ();

			const auto& e = Util::MakeEntity (url,
					location,
					Internal |
						DoNotNotifyUser |
						DoNotSaveInHistory |
						NotPersistent |
						DoNotAnnounceEntity);
			const auto& result = iem->DelegateEntity (e);
			if (!result)
			{
				iem->HandleEntity (Util::MakeNotification (RepoInfoFetcher::tr ("Error fetching repository"),
						RepoInfoFetcher::tr ("Could not find any plugins to fetch %1.")
							.arg ("<em>" + url.toString () + "</em>"),
						Priority::Critical));
				return;
			}

			map [result.ID_] = factory (location);

			QObject::connect (result.Handler_,
					SIGNAL (jobFinished (int)),
					object,
					finished,
					Qt::UniqueConnection);
			QObject::connect (result.Handler_,
					SIGNAL (jobRemoved (int)),
					object,
					removed,
					Qt::UniqueConnection);
			QObject::connect (result.Handler_,
					SIGNAL (jobError (int, IDownload::Error)),
					object,
					error,
					Qt::UniqueConnection);
		}
	}

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

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

		FetchImpl (PendingRIs_,
				[&] (const QString& loc) { return PendingRI { goodUrl, loc }; },
				url,
				Proxy_,
				this,
				SLOT (handleRIFinished (int)),
				SLOT (handleRIRemoved (int)),
				SLOT (handleRIError (int, IDownload::Error)));
	}

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

		FetchImpl (PendingComponents_,
				[&] (const QString& loc) { return PendingComponent { url, loc, component, repoId }; },
				url,
				Proxy_,
				this,
				SLOT (handleComponentFinished (int)),
				SLOT (handleComponentRemoved (int)),
				SLOT (handleComponentError (int, IDownload::Error)));
	}

	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)
	{
		auto packageUrl = baseUrl;
		packageUrl.setPath (packageUrl.path () +
				LackManUtil::NormalizePackageName (packageName) + ".xml.gz");

		FetchImpl (PendingPackages_,
				[&] (const QString& loc) { return PendingPackage { packageUrl, baseUrl, loc, packageName, newVersions, componentId }; },
				packageUrl,
				Proxy_,
				this,
				SLOT (handlePackageFinished (int)),
				SLOT (handlePackageRemoved (int)),
				SLOT (handlePackageError (int, IDownload::Error)));
	}

	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", { "e", "-so", name });
#else
		unarch->start ("gunzip", { "-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_);

		Proxy_->GetEntityManager ()->HandleEntity (Util::MakeNotification (tr ("Error fetching repository"),
				tr ("Error downloading file from %1.")
					.arg (pri.URL_.toString ()),
				Priority::Critical));
	}

	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", { "e", "-so", pc.Location_ });
#else
		unarch->start ("gunzip", { "-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_);

		Proxy_->GetEntityManager ()->HandleEntity (Util::MakeNotification (tr ("Error fetching component"),
				tr ("Error downloading file from %1.")
					.arg (pc.URL_.toString ()),
				Priority::Critical));
	}

	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", { "e", "-so", pp.Location_ });
#else
		unarch->start ("gunzip", { "-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_);

		Proxy_->GetEntityManager ()->HandleEntity (Util::MakeNotification (tr ("Error fetching package"),
				tr ("Error fetching package from %1.")
					.arg (pp.URL_.toString ()),
				Priority::Critical));
	}

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

		if (exitCode)
		{
			Proxy_->GetEntityManager ()->HandleEntity (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 ()),
					Priority::Critical));
			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;
			Proxy_->GetEntityManager ()->HandleEntity (Util::MakeNotification (tr ("Repository parse error"),
					tr ("Unable to parse repository description: %1.")
						.arg (error),
					Priority::Critical));
			return;
		}

		emit infoFetched (info);
	}

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

		if (exitCode)
		{
			Proxy_->GetEntityManager ()->HandleEntity (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 ()),
					Priority::Critical));
			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 ();
			Proxy_->GetEntityManager ()->HandleEntity (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 ()),
					Priority::Critical));
			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)
		{
			Proxy_->GetEntityManager ()->HandleEntity (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 ()),
					Priority::Critical));
			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 ();
			Proxy_->GetEntityManager ()->HandleEntity (Util::MakeNotification (tr ("Package parse error"),
					tr ("Unable to parse package description file. "
						"More information is available in logs."),
					Priority::Critical));
			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 ();
		Proxy_->GetEntityManager ()->HandleEntity (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 ()),
					Priority::Critical));
	}
}
}
