/***************************************************************************
*	Copyright (C) 2004 by karye												*
*	karye@users.sourceforge.net												*
*																			*
*	This program 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 2 of the License, or		*
*	(at your option) any later version.										*
*																			*
*	This program 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.							*
*																			*
*	You should have received a copy of the GNU General Public License		*
*	along with this program; if not, write to the							*
*	Free Software Foundation, Inc.,											*
*	59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.				*
***************************************************************************/

#include <KAuth/Action>                    // for Action
#include <KAuth/ExecuteJob>                // for ExecuteJob
#include <qdatetime.h>                      // for QDateTime
#include <qdebug.h>                         // for QDebug
#include <qfile.h>                          // for QFile
#include <qglobal.h>                        // for qWarning, foreach, qCritical
#include <qiodevice.h>                      // for QIODevice, QIODevice::Rea...
#include <qlist.h>                          // for QList
#include <qmap.h>                           // for QMap
#include <qobject.h>                        // for QObject
#include <qobjectdefs.h>                    // for emit
#include <qstring.h>                        // for QString, operator+, opera...
#include <qstringlist.h>                    // for QStringList
#include <qtextstream.h>                    // for QTextStream
#include <threadweaver/job.h>               // for Job
#include <threadweaver/jobinterface.h>      // for JobPointer
#include <threadweaver/qobjectdecorator.h>  // for QObjectDecorator
#include <threadweaver/queue.h>             // for Queue
#include <threadweaver/queuestream.h>       // for QueueStream
#include <unistd.h>                         // for usleep

#include <utility>

#include "cacheportagejob.h"                // for CachePortageJob
#include "common.h"                         // for KurooDBSingleton, Signali...
#include "emerge.h"                         // for Emerge
#include "global.h"                         // for parsePackage
#include "portage.h"                        // for Portage
#include "portage/portage.h"
#include "portagedb.h"                      // for KurooDB, DbConnection (pt...
#include "portagefiles.h"                   // for PortageFiles
#include "queue.h"                          // for Queue
#include "scanportagejob.h"                 // for ScanPortageJob
#include "scanupdatesjob.h"                 // for ScanUpdatesJob
#include "settings.h"                       // for KurooConfig
#include "signalist.h"                      // for Signalist

namespace ThreadWeaver {
class Thread;
}  // namespace ThreadWeaver

// TODO: these really need to be in their own files, then the include portage.moc at the bottom shouldn't be needed
class AddInstalledPackageJobImpl : public ThreadWeaver::Job
{
public:
	explicit AddInstalledPackageJobImpl( QString  package ) :  m_package(std::move( package )) {}

	void run( ThreadWeaver::JobPointer /*self*/, ThreadWeaver::Thread*  /*thread*/) override {

		QStringList parts = parsePackage( m_package );
		if ( parts.isEmpty() ) {
			qWarning() << QStringLiteral("Inserting emerged package: can not match %1.").arg( m_package );
			return;
		}
		QString category = parts[0];
		QString name = parts[1];
		QString version = parts[2] % u'-' % parts[3];

		DbConnection* const m_db = KurooDBSingleton::Instance()->getStaticDbConnection();
		QString id = KurooDBSingleton::Instance()->singleQuery( QStringLiteral( "SELECT id FROM package WHERE "
			"name = '%1' AND category = '%2' LIMIT 1;").arg( name, category ), m_db );

		if ( id.isEmpty() ) {

			qWarning() << QStringLiteral("Inserting emerged package: Can not find id in database for package %1/%2.")
				.arg( category, name );

			KurooDBSingleton::Instance()->returnStaticDbConnection( m_db );
//			return;
		}
		else {
			KurooDBSingleton::Instance()->query( QStringLiteral( "UPDATE package SET status = '%1' WHERE id = '%2';" )
												.arg( PACKAGE_INSTALLED_STRING, id ), m_db );

			KurooDBSingleton::Instance()->query( QStringLiteral( "UPDATE version SET status = '%1' WHERE idPackage = '%2' AND name = '%3';" )
												.arg( PACKAGE_INSTALLED_STRING, id, version ), m_db );

			// Clean out the update string if this version was installed
			KurooDBSingleton::Instance()->query( QStringLiteral( "UPDATE package SET updateVersion = '' "
														"WHERE id = '%1' AND ( updateVersion = '%2' OR updateVersion = '%3' );" )
												.arg( id, version + QStringLiteral(" (D)"), version + QStringLiteral(" (U)") ), m_db );

			KurooDBSingleton::Instance()->returnStaticDbConnection( m_db );
//			return;
		}
// 	}
//
// 	virtual void completeJob() {
		PortageSingleton::Instance()->slotPackageChanged();
	}

private:
	const QString m_package;
	Q_DISABLE_COPY(AddInstalledPackageJobImpl)
};
/**
* @class AddInstalledPackageJob
* @short Thread for registrating packages as installed in db.
*/
class AddInstalledPackageJob : public ThreadWeaver::QObjectDecorator
{
	Q_OBJECT
public:
	explicit AddInstalledPackageJob( const QString& package )
	: ThreadWeaver::QObjectDecorator( new AddInstalledPackageJobImpl( package ) ) {}
};

class RemoveInstalledPackageJobImpl : public ThreadWeaver::Job
{
public:
	explicit RemoveInstalledPackageJobImpl( QString  package ) :  m_package(std::move( package )) {}

	void run( ThreadWeaver::JobPointer /*self*/, ThreadWeaver::Thread*  /*thread*/) override {
		QStringList parts = parsePackage( m_package );
		if ( parts.isEmpty() ) {
			qWarning() << QStringLiteral("Removing unmerged package: can not match %1.").arg( m_package );
			return;
		}
		QString category = parts[0];
		QString name = parts[1];
		QString version = parts[2] % u'-' % parts[3];

		DbConnection* const m_db = KurooDBSingleton::Instance()->getStaticDbConnection();

		QString id = KurooDBSingleton::Instance()->singleQuery( QStringLiteral( "SELECT id FROM package WHERE "
			"name = '%1' AND category = '%2' LIMIT 1;").arg( name, category ), m_db );

		if ( id.isEmpty() ) {
			qWarning() << QStringLiteral("Removing unmerged package: Can not find id in database for package %1/%2.")
				.arg( category, name );

			KurooDBSingleton::Instance()->returnStaticDbConnection( m_db );
//			return;
		}
		else {

			// Check how many version are installed
			QString installedVersionCount = KurooDBSingleton::Instance()->singleQuery(
				QStringLiteral( "SELECT COUNT(id) FROM version WHERE idPackage = '%1' AND status = '%2' LIMIT 1;")
				.arg( id, PACKAGE_INSTALLED_STRING ), m_db );

			// Mark package as uninstalled only when one version is found
			if ( installedVersionCount == u'1' ) {

				// Mark package as uninstalled
				KurooDBSingleton::Instance()->singleQuery( QStringLiteral( "UPDATE package SET status = '%1' WHERE id = '%2'")
															.arg( PACKAGE_AVAILABLE_STRING, id ), m_db );

				// Delete package if "old" = not in official Portage anymore
				KurooDBSingleton::Instance()->singleQuery( QStringLiteral( "UPDATE package SET status = '%1' WHERE status = '%2' AND id = '%3';" )
															.arg( PACKAGE_DELETED_STRING, PACKAGE_OLD_STRING, id ), m_db );
			}

			// And now mark this specific version as not installed
			KurooDBSingleton::Instance()->singleQuery( QStringLiteral( "UPDATE version SET status = '%1' WHERE idPackage = '%2' AND name = '%3';" )
														.arg( PACKAGE_AVAILABLE_STRING, id, version ), m_db );

			KurooDBSingleton::Instance()->returnStaticDbConnection( m_db );
//			return;
		}
// 	}
//
// 	virtual void completeJob() {
		PortageSingleton::Instance()->slotPackageChanged();
	}

private:
	const QString m_package;
	Q_DISABLE_COPY(RemoveInstalledPackageJobImpl)
};

/**
* @class RemoveInstalledPackageJob
* @short Thread for removing packages as installed in db.
*/
class RemoveInstalledPackageJob : public ThreadWeaver::QObjectDecorator
{
	Q_OBJECT
public:
	explicit RemoveInstalledPackageJob( const QString& package )
	: ThreadWeaver::QObjectDecorator( new RemoveInstalledPackageJobImpl( package ) ) {}
};

class CheckUpdatesPackageJobImpl : public ThreadWeaver::Job
{
public:
	CheckUpdatesPackageJobImpl( QString  id, QString  updateVersion, int hasUpdate )
	: 
		m_id(std::move( id )), m_updateVersion(std::move( updateVersion )), m_hasUpdate( hasUpdate ) {}

	void run( ThreadWeaver::JobPointer /*self*/, ThreadWeaver::Thread*  /*thread*/) override {
		DbConnection* const m_db = KurooDBSingleton::Instance()->getStaticDbConnection();

		if ( m_hasUpdate == 0 ) {
			KurooDBSingleton::Instance()->singleQuery( QStringLiteral("UPDATE package SET updateVersion = '', status = '%1' WHERE id = '%2';")
														.arg( PACKAGE_INSTALLED_STRING, m_id ), m_db );
		}
		else {
			QString updateString;
			if ( m_hasUpdate > 0 )
				updateString = m_updateVersion + QStringLiteral(" (U)");
			else
				updateString = m_updateVersion + QStringLiteral(" (D)");

			KurooDBSingleton::Instance()->singleQuery( QStringLiteral( "UPDATE package SET updateVersion = '%1', status = '%2' WHERE id = '%3';" )
														.arg( updateString, PACKAGE_UPDATES_STRING, m_id ), m_db );
		}

		KurooDBSingleton::Instance()->returnStaticDbConnection( m_db );
// 		return;
// 	}
//
// 	virtual void completeJob() {
		PortageSingleton::Instance()->slotChanged();
	}

private:
	const 	QString m_id, m_updateVersion;
	int 	m_hasUpdate;
	Q_DISABLE_COPY(CheckUpdatesPackageJobImpl)
};
/**
* @class CheckUpdatesPackageJob
* @short Thread for marking packages as updates or downgrades.
*/
class CheckUpdatesPackageJob : public ThreadWeaver::QObjectDecorator
{
	Q_OBJECT
public:
	CheckUpdatesPackageJob( const QString& id, const QString& updateVersion, int hasUpdate )
		: ThreadWeaver::QObjectDecorator( new CheckUpdatesPackageJobImpl( id, updateVersion, hasUpdate ) ) {}
};


///////////////////////////////////////////////////////////////////////////////////////////////////
//
///////////////////////////////////////////////////////////////////////////////////////////////////

/**
* @class Portage
* @short Object handling the Portage tree.
*
* @todo: Register @world packages in db.
*/
Portage::Portage( QObject *m_parent )
	: QObject( m_parent )
{
	// When cache scan is done go one scanning portage for all packages
	connect( SignalistSingleton::Instance(), &Signalist::signalCachePortageComplete, this, &Portage::slotScan );
	// Then portage scan is completed
	connect( SignalistSingleton::Instance(), &Signalist::signalScanPortageComplete, this, &Portage::slotScanCompleted );

	connect( SignalistSingleton::Instance(), &Signalist::signalScanUpdatesComplete, this, &Portage::slotLoadUpdates );
	connect( SignalistSingleton::Instance(), &Signalist::signalLoadUpdatesComplete, this, &Portage::slotChanged );

	// Start refresh directly after emerge sync
	connect( SignalistSingleton::Instance(), &Signalist::signalSyncDone, this, &Portage::slotSyncCompleted );
}

Portage::~Portage()
= default;

void Portage::init( QObject *parent )
{
	m_parent = parent;
	loadWorld();
}

/**
* Emit signal after changes in Portage.
*/
void Portage::slotChanged()
{
	// Register in db so we can check at next start if user has emerged any packages outside kuroo
	KurooDBSingleton::Instance()->setKurooDbMeta( QStringLiteral("scanTimeStamp"), QString::number( QDateTime::currentSecsSinceEpoch() ) );

	Q_EMIT signalPortageChanged();
}

/**
* Reload @world when new package is installed/removed in case user not using --oneshot.
*/
void Portage::slotPackageChanged()
{
	loadWorld();
	slotChanged();
}


////////////////////////////////////////////////////////////////////////////////////////
// Portage handling...
////////////////////////////////////////////////////////////////////////////////////////

/**
* Start scan of portage packages.
* @return bool
*/
auto Portage::slotRefresh() -> bool
{
	DEBUG_LINE_INFO;
	// Update cache if empty
	if ( KurooDBSingleton::Instance()->isCacheEmpty() ) {
		SignalistSingleton::Instance()->scanStarted();
		//This seems important, not sure why it was commented out
		auto *job = new ThreadWeaver::QObjectDecorator( new CachePortageJob() );
		connect(job, &ThreadWeaver::QObjectDecorator::done, this, &Portage::slotWeaverDone);
		ThreadWeaver::Queue::instance()->stream() << job;
	}
	else
		slotScan();

	return true;
}

/**
* Start emerge sync.
* @return bool
*/
void Portage::slotSync()
{
	EmergeSingleton::Instance()->sync();
}

/**
* Stop progressbar.
*/
void Portage::slotSyncCompleted()
{
	slotRefresh();
}

/**
* Continue with scan of portage packages.
* @return bool
*/
auto Portage::slotScan() -> bool
{
	DEBUG_LINE_INFO;

	// Wait for cache job to finish before launching the scan.
	int maxLoops( 99 );
	while ( true ) {
		if ( KurooDBSingleton::Instance()->isCacheEmpty() )
			::usleep( 100000 ); // Sleep 100 msec
		else
			break;

		if ( maxLoops-- == 0 ) {
			qWarning() << "Scanning Portage. Wait-counter has reached maximum. Attempting to scan Portage.";
			break;
		}
	}

	auto *job = new ThreadWeaver::QObjectDecorator( new ScanPortageJob() );
	connect(job, &ThreadWeaver::QObjectDecorator::done, this, &Portage::slotWeaverDone);
	ThreadWeaver::Queue::instance()->stream() << job;

	return true;
}

/**
* After portage has completed scanning for all packages, check for updates.
*/
void Portage::slotScanCompleted()
{
	// Reset Queue with it's own cache
	QueueSingleton::Instance()->reset();

	// Now all Portage files
	PortageFilesSingleton::Instance()->loadPackageFiles();

	// Ready to roll!
	SignalistSingleton::Instance()->setKurooReady( true );
	slotChanged();

	// Go on with checking for updates
	if ( KurooDBSingleton::Instance()->isUpdatesEmpty() )
		slotRefreshUpdates();
}


////////////////////////////////////////////////////////////////////////////////////////
// World handling...
////////////////////////////////////////////////////////////////////////////////////////

/**
* Load packages in @world file. @fixme: optimize this...
*/
void Portage::loadWorld()
{
	m_mapWorld.clear();

	QFile file( KurooConfig::fileWorld() );
	if ( file.open( QIODevice::ReadOnly ) ) {
		QTextStream stream( &file );
		while ( !stream.atEnd() ) {
			QString package = stream.readLine();
			m_mapWorld[ package.trimmed() ] = QString();
		}
		Q_EMIT signalWorldChanged();
	}
	else
		qCritical() << "Loading packages in @world. Reading: " << KurooConfig::fileWorld();
}

/**
* Check if this package in is @world file.
* @param package
* @return true/false
*/
auto Portage::isInWorld( const QString& package ) -> bool
{
	return m_mapWorld.contains( package );
}

/**
* Add package to @world file.
* @param package
*/
void Portage::appendWorld( const QStringList& packageList )
{
	// Add/update package into @world map
	for ( const QString& pkg : packageList )
		m_mapWorld.insert( pkg, QString() );

	writeWorld();
}

/**
* Remove package from @world file.
* @param package
*/
void Portage::removeFromWorld( const QStringList& packageList )
{
	// Make a copy of @world map
	for( const QString& pkg : packageList )
		m_mapWorld.remove( pkg );
	
	writeWorld();
}

void Portage::writeWorld()
{
	KAuth::Action writeworld( QStringLiteral("org.gentoo.kuroo.writeworld") );
	writeworld.setHelperId( QStringLiteral("org.gentoo.kuroo") );
	//writeworld.setTimeout( 60*60*24*1000 );		// Should finish within default timeout
	
	writeworld.addArgument( QStringLiteral("worldfile"), KurooConfig::fileWorld() );
	writeworld.addArgument( QStringLiteral("worldmap"), QStringList( m_mapWorld.keys() ) );
	
	KAuth::ExecuteJob *job = writeworld.execute();
	if ( job->exec() ) {
		Q_EMIT signalWorldChanged();
	} else {
		qCritical() << "Error writing world file:" << KurooConfig::fileWorld();
	}
}

////////////////////////////////////////////////////////////////////////////////////////
// Package handlling...
////////////////////////////////////////////////////////////////////////////////////////

/**
* Launch emerge pretend of packages.
* @param category
* @param packageList
*/
void Portage::pretendPackageList( const QStringList& packageIdList )
{
	QStringList packageList;
	for ( const QString& id : packageIdList )
		packageList += KurooDBSingleton::Instance()->category( id ) % u'/' % KurooDBSingleton::Instance()->package( id );

	EmergeSingleton::Instance()->pretend( packageList );
}

/**
* Launch unmerge of packages
* @param category
* @param packageList
*/
void Portage::uninstallInstalledPackageList( const QStringList& packageIdList )
{
	QStringList packageList;
	for ( const QString& id : packageIdList )
		packageList += KurooDBSingleton::Instance()->category( id ) % u'/' % KurooDBSingleton::Instance()->package( id );

	EmergeSingleton::Instance()->unmerge( packageList );
}

/**
* @fixme: Check for failure.
* Add package as installed in db.
* @param package
*/
void Portage::addInstalledPackage( const QString& package ) const
{
	auto *job = new AddInstalledPackageJob( package );
	connect(job, &AddInstalledPackageJob::done, this, &Portage::slotWeaverDone);
	ThreadWeaver::Queue::instance()->stream() << job;
}

/**
* Remove packages from db. @todo: Check for failure.
* @param packageIdList
*/
void Portage::removeInstalledPackage( const QString& package ) const
{
	auto *job = new RemoveInstalledPackageJob( package );
	connect(job, &RemoveInstalledPackageJob::done, this, &Portage::slotWeaverDone);
	ThreadWeaver::Queue::instance()->stream() << job;
}

/**
* Start scan of update packages.
* @return bool
*/
auto Portage::slotRefreshUpdates() -> bool
{
	EmergeSingleton::Instance()->checkUpdates();
	return true;
}

/**
* Start scan of update packages.
* @return bool
*/
auto Portage::slotLoadUpdates() -> bool
{
	SignalistSingleton::Instance()->scanStarted();
	auto *job = new ScanUpdatesJob( EmergeSingleton::Instance()->packageList() );
	connect(job, &ScanUpdatesJob::done, this, &Portage::slotWeaverDone);
	ThreadWeaver::Queue::instance()->stream() << job;
	return true;
}

/**
* Update packages when user changes package stability.
*/
void Portage::checkUpdates( const QString& id, const QString& emergeVersion, int hasUpdate ) const
{
	auto *job = new CheckUpdatesPackageJob( id, emergeVersion, hasUpdate );
	connect(job, &CheckUpdatesPackageJob::done, this, &Portage::slotWeaverDone);
	ThreadWeaver::Queue::instance()->stream() << job;
}

void Portage::slotWeaverDone(const ThreadWeaver::JobPointer& job)
{
	Q_UNUSED(job);
	DEBUG_LINE_INFO;
	//WARN: Hope that QSharedPointer takes care of this so it doesn't leak memory
	//delete job;
}


#include "portage.moc"
