/***************************************************************************
*	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 <kdirwatch.h>                  // for KDirWatch
#include <klocalizedstring.h>           // for i18n
#include <qdatetime.h>                  // for QTime, QDateTime
#include <qdebug.h>                     // for QDebug
#include <qdir.h>                       // for QDir, operator|, QDir::Files
#include <qfile.h>                      // for QFile
#include <qfileinfo.h>                  // for QFileInfo
#include <qglobal.h>                    // for foreach, qWarning, Q_UNUSED
#include <qiodevice.h>                  // for QIODevice, QIODevice::ReadOnly
#include <qlist.h>                      // for QList<>::const_iterator, QList
#include <qmap.h>                       // for QMap<>::iterator
#include <qobject.h>                    // for QObject
#include <qobjectdefs.h>                // for emit
#include <qregexp.h>                    // for QRegExp
#include <qstring.h>                    // for QString, operator+, operator>
#include <qstringlist.h>                // for QStringList
#include <qtextstream.h>                // for QTextStream
#include <threadweaver/job.h>           // for Job
#include <threadweaver/jobinterface.h>  // for JobPointer
#include <threadweaver/queue.h>         // for Queue
#include <threadweaver/queuestream.h>   // for QueueStream

#include "common.h"                     // for KurooDBSingleton, EMERGELOG
#include "emerge.h"                     // for Emerge
#include "history.h"                    // for History, EmergeTimeMap, eLog
#include "history/history.h"
#include "log.h"                        // for Log
#include "packageemergetime.h"          // for PackageEmergeTime
#include "portage.h"                    // for Portage
#include "portagedb.h"                  // for KurooDB, DbConnection (ptr only)
#include "queue.h"                      // for Queue
#include "scanhistoryjob.h"             // for EmergeTimeMap, ScanHistoryJob
#include "settings.h"                   // for KurooConfig
#include "signalist.h"                  // for Signalist
#include "statusbar.h"                  // for KurooStatusBar

namespace ThreadWeaver {
class Thread;
}  // namespace ThreadWeaver


class UpdateStatisticsJob : public ThreadWeaver::Job
{
public:
	UpdateStatisticsJob() = default;

    void run( ThreadWeaver::JobPointer /*self*/, ThreadWeaver::Thread*  /*thread*/) override {
		DbConnection* const m_db = KurooDBSingleton::Instance()->getStaticDbConnection();
		KurooDBSingleton::Instance()->singleQuery( QStringLiteral("DELETE FROM statistic;"), m_db );
		KurooDBSingleton::Instance()->singleQuery( QStringLiteral("BEGIN TRANSACTION;"), m_db );

		EmergeTimeMap emergeTimeMap( HistorySingleton::Instance()->getStatisticsMap() );
		EmergeTimeMap::iterator itMapEnd = emergeTimeMap.end();
		for ( EmergeTimeMap::iterator itMap = emergeTimeMap.begin(); itMap != itMapEnd; ++itMap ) {
			KurooDBSingleton::Instance()->insert( QStringLiteral( "INSERT INTO statistic (time, count, package) VALUES ('%1', '%2', '%3');" )
				.arg( itMap.value().emergeTime() ).arg( itMap.value().count() ).arg( itMap.key() ), m_db );
		}

		KurooDBSingleton::Instance()->singleQuery( QStringLiteral("COMMIT TRANSACTION;"), m_db );
		KurooDBSingleton::Instance()->returnStaticDbConnection( m_db );
	}

	Q_DISABLE_COPY(UpdateStatisticsJob)
};

/**
 * @class History
 * @short Object for the emerge history and statistics.
 * History watches for changes in emerge.log and parses new entries to register emerges and unmerges of packages in database.
 */
History::History( QObject *m_parent )
	: QObject( m_parent ), logWatcher( nullptr ), isEmerging( false )
{}

History::~History()
{
	m_log.close();
	delete logWatcher;
	logWatcher = nullptr;
}

void History::init( QObject *parent )
{
	m_parent = parent;

	m_log.setFileName( KurooConfig::fileEmergeLog() );
	if ( !m_log.open( QIODevice::ReadOnly ) )
		qCritical() << "Reading " << KurooConfig::fileEmergeLog();
	else
		stream.setDevice( &m_log );

	scanELog();
	loadTimeStatistics();

	connect( SignalistSingleton::Instance(), &Signalist::signalScanHistoryComplete, this, &History::slotScanHistoryCompleted );

	logWatcher = new KDirWatch( this );
	logWatcher->addFile( KurooConfig::fileEmergeLog() );
}

/**
 * Load emerge statistics if any.
 * And start a watch on the emerge.log for changes.
 */
void History::slotInit()
{
// 	connect( logWatcher, SIGNAL( dirty( const QString& ) ), this, SLOT( slotParse() ) );
}

void History::slotScanHistoryCompleted()
{
	Q_EMIT signalScanHistoryCompleted();
	connect(logWatcher, &KDirWatch::dirty, this, &History::slotParse);
}

/**
 * Check for new entries in emerge.log similar to a "tail".
 * @return false if emerge log shows changes.
 */
auto History::slotRefresh() -> bool
{
	DEBUG_LINE_INFO;
	QString lastDate = KurooDBSingleton::Instance()->getKurooDbMeta( QStringLiteral("scanTimeStamp") );
	if ( lastDate.isEmpty() )
		lastDate = QStringLiteral("0");

	// Collect all recent entries in emerge log
	QStringList emergeLines;
	QRegExp rx( QStringLiteral("\\d+") );
	while ( !stream.atEnd() ) {
		QString line = stream.readLine();
		static const QRegularExpression emergeSyncUnmergeComplete(QStringLiteral("(>>> emerge)|(=== Sync completed)|(::: completed emerge)|(>>> unmerge success)"));
		if ( rx.indexIn(line) > -1 )
			if ( rx.cap(0) > lastDate )
				if ( line.contains( emergeSyncUnmergeComplete ) )
					emergeLines += line + u' ';
	}

	// Check only for successful emerge/unmerges or sync outside kuroo
	static const QRegularExpression syncCompleteCompletedEmergeUnmergeSuccess(QStringLiteral("(=== Sync completed)|(::: completed emerge)|(>>> unmerge success)"));
	if ( !emergeLines.filter(syncCompleteCompletedEmergeUnmergeSuccess).isEmpty() ) {
		slotScanHistory( emergeLines );
		return false;
	}

	slotScanHistoryCompleted();
	return true;
}

/**
 * Launch scan to load into db.
 * @param emergeLines
 */
void History::slotScanHistory( const QStringList& lines ) const
{
	SignalistSingleton::Instance()->scanStarted();
	auto *job = new ScanHistoryJob( lines );
	connect(job, &ScanHistoryJob::done, this, &History::slotWeaverDone);
	ThreadWeaver::Queue::instance()->stream() << job;
}

/**
 * Parse emerge.log when it has been changed, eg after, emerge, unmerge, sync...
 * Allow for translation of the output.
 */
void History::slotParse()
{
	static bool syncDone( false );
	QStringList emergeLines;
	static const QRegularExpression rxTimeStamp( QStringLiteral("\\d+:\\s") );
	QRegExp rxPackage( QStringLiteral("(\\()(\\d+)(\\s+of\\s+)(\\d+)(\\)\\s+)(\\S+/\\S+)") );

	while ( !stream.atEnd() )
		emergeLines += stream.readLine() + u' ';

	for ( QString line : std::as_const(emergeLines) ) {

		if ( !line.isEmpty() ) {
			QString emergeLine = line.section( rxTimeStamp, 1, 1 );
			static const QRegularExpression rxPrefixes( QStringLiteral("(!!! )|(>>> )|(=== )|(\\*\\*\\* )|(::: )") );
			emergeLine = emergeLine.section( rxPrefixes, 1, 1 );

			// For translation
			emergeLine.replace( QStringLiteral(" to "), i18n(" to ") );
			emergeLine.replace( QStringLiteral(" of "), i18n(" of ") );

			// Is it a regular emerge or something else
			if ( QRegExp(QStringLiteral("(\\s|\\S)*(\\*\\*\\* emerge)(\\s|\\S)*") ).exactMatch( line ) ) {
				isEmerging = true;

				// Not emerging - just downloading the packages
				if ( line.contains( QStringLiteral("fetch-all-uri") ) )
					isEmerging = false;
			}

			// Parse out nice statusbar text
			static const QRegularExpression rxStatusBarText(QStringLiteral("(\\) )(Cleaning)|(Compiling/Merging)|(Post-Build Cleaning)"));
			if ( line.contains( rxStatusBarText ) ) {
				QString logLine = u'(' + emergeLine.section( QStringLiteral("::"), 0, 0 ).remove( u'(' );

				logLine.replace( QStringLiteral("Compiling/Merging"), i18n( "Compiling/Merging" ) );
				logLine.replace( QStringLiteral("Post-Build Cleaning"), i18n( "Post-Build Cleaning" ) );
				logLine.replace( QStringLiteral("Cleaning"), i18n( "Cleaning" ) );

				KurooStatusBar::instance()->setProgressStatus( QString(), logLine );
				LogSingleton::Instance()->writeLog( logLine, EMERGELOG );
			}
			else

			// Catch emerge session start
			if ( line.contains( QStringLiteral("Started emerge on") ) ) {
				DEBUG_LINE_INFO;
				line.replace( QStringLiteral("Started emerge on"), i18n( "Started emerge on" ) );
				LogSingleton::Instance()->writeLog( line.section( rxTimeStamp, 1, 1 ), EMERGELOG );
			}
			else

			// Emerge has started, signal queue to launch progressbar for this package
			if ( line.contains( QStringLiteral(">>> emerge") ) && isEmerging ) {
				DEBUG_LINE_INFO;
				if ( rxPackage.indexIn( line ) > -1 ) {
					/*int order = rxPackage.cap(2).toInt();
					int total = rxPackage.cap(4).toInt();*/
					QString package = rxPackage.cap(6);
					QueueSingleton::Instance()->emergePackageStart( package/*, order, total */);
				}
				else
					qWarning() << QStringLiteral("Can not parse package emerge start in %1: %2")
					.arg( KurooConfig::fileEmergeLog(), line );
			}
			else

			// Emerge has completed, signal queue to mark package as installed
			if ( line.contains( QStringLiteral("::: completed emerge ") ) && isEmerging ) {
				scanELog();

				if ( rxPackage.indexIn( line ) > -1 ) {
					/*int order = rxPackage.cap(2).toInt();
					int total = rxPackage.cap(4).toInt();*/
					QString package = rxPackage.cap(6);
					QueueSingleton::Instance()->emergePackageComplete( package/*, order, total */);
					//TODO: This causes a full reload of PackageListView
					PortageSingleton::Instance()->addInstalledPackage( package );
					Q_EMIT signalHistoryChanged();
				}
				else
					qWarning() << QStringLiteral("Can not parse package emerge complete in %1: %2")
									.arg( KurooConfig::fileEmergeLog(), line );

				emergeLine.replace( QStringLiteral("completed emerge"), i18n( "completed emerge" ) );
				LogSingleton::Instance()->writeLog( emergeLine, EMERGELOG );
			}
			else

			// Catch package unmerge completion
			if ( emergeLine.contains(QStringLiteral("unmerge success")) ) {
				QString package = emergeLine.section( QStringLiteral("unmerge success: "), 1, 1 );
				PortageSingleton::Instance()->removeInstalledPackage( package );
				emergeLine.replace( QStringLiteral("unmerge success"), i18n( "unmerge success" ) );
				LogSingleton::Instance()->writeLog( emergeLine, EMERGELOG );
				Q_EMIT signalHistoryChanged();
			}
			else

			// Catch sync session start
			if ( emergeLine.contains( QStringLiteral("Starting rsync") ) ) {
				KurooStatusBar::instance()->setProgressStatus( QString(), i18n( "Synchronizing Portage..." ) );
				LogSingleton::Instance()->writeLog( i18n( "Synchronizing Portage..." ), EMERGELOG );
				m_syncTime = QTime::currentTime();
			}
			else

			// Catch sync session complete
			if ( emergeLine.contains( QStringLiteral("Sync completed") ) ) {
				syncDone = true;
				KurooStatusBar::instance()->setProgressStatus( QString(), i18n( "Sync completed." ) );
				LogSingleton::Instance()->writeLog( i18n( "Sync completed." ), EMERGELOG );
			}
			else

			// Catch emerge termination
			if ( emergeLine.contains( QStringLiteral("terminating.") ) ) {
				KurooStatusBar::instance()->setProgressStatus( QString(), i18n( "Done." ) );
				LogSingleton::Instance()->writeLog( i18n( "Done." ), EMERGELOG );

				if ( syncDone ) {
					syncDone = false;
					SignalistSingleton::Instance()->syncDone();

					// Store this sync duration for progressbar estimate
					KurooDBSingleton::Instance()->setKurooDbMeta( QStringLiteral("syncDuration"), QString::number( m_syncTime.secsTo( QTime::currentTime() ) ) );
				}
			}
			else {
				emergeLine.replace( QStringLiteral("AUTOCLEAN"), i18n( "AUTOCLEAN" ) );
				emergeLine.replace( QStringLiteral("Unmerging"), i18n( "Unmerging" ) );
				emergeLine.replace( QStringLiteral("Finished. Cleaning up"), i18n( "Finished. Cleaning up" ) );
				emergeLine.replace( QStringLiteral("exiting successfully"), i18n( "Exiting successfully" ) );
				emergeLine.replace( QStringLiteral("terminating"), i18n( "Terminating" ) );

				KurooStatusBar::instance()->setProgressStatus( QString(), emergeLine );
				LogSingleton::Instance()->writeLog( emergeLine, EMERGELOG );
			}
		}
	}

	// Update history
	if ( !emergeLines.isEmpty() )
		slotScanHistory( emergeLines );
}

/**
 * Update emerge duration statistic.
 */
void History::updateStatistics()
{
	DEBUG_LINE_INFO;
    ThreadWeaver::Queue::instance()->stream() << new UpdateStatisticsJob();
}

/**
 * Register einfo in db for package.
 */
void History::appendEmergeInfo()
{
	DEBUG_LINE_INFO;

	QString einfo = EmergeSingleton::Instance()->packageMessage(); //.utf8();
	if ( !einfo.isEmpty() )
		KurooDBSingleton::Instance()->addEmergeInfo( einfo );
}

/**
 * Return all etc-file merge history.
 * @return QStringList
 */
auto History::allMergeHistory() -> const QStringList
{
	return KurooDBSingleton::Instance()->allMergeHistory();
}

/**
 * Load the history map with emerge times statistics from database.
 */
void History::loadTimeStatistics()
{
	DEBUG_LINE_INFO;

	m_statisticsMap.clear();
	const QStringList timePackageList = KurooDBSingleton::Instance()->allStatistic();

	for( QStringList::const_iterator it = timePackageList.constBegin(); it != timePackageList.constEnd(); ++it) {
		QString package = *it++;
		QString time = *it++;
		QString count = *it;
		PackageEmergeTime p( time.toInt(), count.toInt() );
		m_statisticsMap.insert( package, p );
	}
}

/**
 * Return statistics map.
 * @return m_statisticsMap
 */
auto History::getStatisticsMap() -> const EmergeTimeMap
{
	return m_statisticsMap;
}

/**
 * Set statistics map.
 * @param timeMap
 */
void History::setStatisticsMap( const EmergeTimeMap& statisticsMap )
{
	m_statisticsMap = statisticsMap;
}

/**
 * Get emerge time for this package.
 * @param package
 * @return emergeTime		time or na
 */
auto History::packageTime( const QString& packageNoversion ) -> const QString
{
	EmergeTimeMap::iterator itMap = m_statisticsMap.find( packageNoversion );
	if ( itMap != m_statisticsMap.end() )
		return QString::number( itMap.value().emergeTime() / itMap.value().count() );
	return QString();
}

/**
 * Collect all eLogs files.
 */
void History::scanELog()
{
	QDir eLogDir( KurooConfig::dirELog() );
	eLogDir.setFilter( QDir::Files | QDir::NoSymLinks );
	eLogDir.setSorting( QDir::Time );

	m_eLogs.clear();
	// declaring a local const container prevents detaching and the attendant clazy warning
	// In C++20 the range for can do this in the loop declaration https://www.kdab.com/blog-qasconst-and-stdas_const/
	const QFileInfoList entryInfo = eLogDir.entryInfoList();
	for( const QFileInfo& elogInfo : entryInfo ) {
		eLog elog;
		elog.timestamp = elogInfo.lastModified().toSecsSinceEpoch();
		elog.package = elogInfo.fileName();
		m_eLogs.push_back( elog );
	}
	DEBUG_LINE_INFO;
}

/**
 * Return vector with all eLog files.
 */
auto History::getELogs() -> eLogVector
{
	return m_eLogs;
}

void History::slotWeaverDone(const ThreadWeaver::JobPointer& job)
{
	Q_UNUSED(job);
	//TODO: hope that QSharedPointer cleans this up and it doesn't leak memory
	//delete (QSharedPointer<ThreadWeaver::JobInterface>) job;
}

