/**************************************************************************
*	Copyright (C) 2004 by karye												*
*	karye@users.sourceforge.net												*
*	From Amarok code.														*
*	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 <cmath>           // for pow
#include <qbytearray.h>     // for QByteArray
#include <qdatetime.h>      // for QDateTime
#include <qdebug.h>         // for QDebug
#include <qelapsedtimer.h>  // for QElapsedTimer
#include <qfile.h>          // for QFile
#include <qglobal.h>        // for qWarning, qDebug, QGlobalStatic, qCritical
#include <qiodevice.h>      // for QIODevice, QIODevice::WriteOnly, QIODevic...
#include <qlist.h>          // for QList<>::const_iterator, QList
#include <qobject.h>        // for QObject
#include <qrandom.h>        // for QRandomGenerator
#include <qregexp.h>        // for QRegExp
#include <qsemaphore.h>     // for QSemaphore
#include <qstring.h>        // for operator+, QString
#include <qstringlist.h>    // for QStringList
#include <qtextstream.h>    // for QTextStream
#include <sqlite3.h>        // for sqlite3_errmsg, sqlite3_finalize, sqlite3...
#include <unistd.h>         // for usleep, sleep

#include <utility>

#include "common.h"         // for PACKAGE_UPDATES_STRING, PACKAGE_ALL_STRING
#include "core/portagedb.h"
#include "global.h"         // for kurooDir, parsePackage
#include "portagedb.h"      // for KurooDB, DbConnectionPool, SqliteConnection
#include "settings.h"       // for KurooConfig

/**
 * @class KurooDB
 * @short Handle database connections and queries.
 */
KurooDB::KurooDB ( QObject *m_parent )
		: QObject ( m_parent )
{}

KurooDB::~KurooDB()
{
	delete m_dbConnPool;
}

/**
 * Check db integrity and create new db if necessary.
 * Set write permission for regular user.
 * @return database file
 */
auto KurooDB::init( QObject *parent ) -> QString
{
	m_parent = parent;

	m_dbConnPool = new DbConnectionPool();
	DbConnection *dbConn = m_dbConnPool->getDbConnection();
	m_dbConnPool->putDbConnection( dbConn );

	if ( !dbConn->isInitialized() || !isValid() )
		createTables();

	m_dbConnPool->createDbConnections();

	return kurooDir + KurooConfig::databas();
}

auto KurooDB::getStaticDbConnection() -> DbConnection *
{
// 	qDebug() << "KurooDB::getStaticDbConnection-------------";
	return m_dbConnPool->getDbConnection();
}

void KurooDB::returnStaticDbConnection( DbConnection *conn )
{
// 	qDebug() << "--------------KurooDB::returnStaticDbConnection ";
	m_dbConnPool->putDbConnection ( conn );
}

/**
 * Executes a SQL query on the already opened database
 * @param statement SQL program to execute. Only one SQL statement is allowed.
 * @return		The queried data, or QStringList() on error.
 */
auto KurooDB::query( const QString& statement, DbConnection *conn ) -> QStringList
{
	QElapsedTimer clock;
	clock.start();

	DbConnection *dbConn = nullptr;
	if ( conn != nullptr )
		dbConn = conn;
	else
		dbConn = m_dbConnPool->getDbConnection();

	QStringList values = dbConn->query ( statement );

	if ( conn == nullptr )
		m_dbConnPool->putDbConnection ( dbConn );

	if (clock.elapsed() > 2000)
		qWarning() << statement << "took too long" << clock.elapsed() / 1000;

	return values;
}

/**
 * Executes a SQL query on the already opened database
 * @param statement SQL program to execute. Only one SQL statement is allowed.
 * @return		The queried data, or QStringList() on error.
 */
auto KurooDB::singleQuery( const QString& statement, DbConnection *conn ) -> QString
{
	QElapsedTimer clock;
	clock.start();

	DbConnection *dbConn = nullptr;
	if ( conn != nullptr )
		dbConn = conn;
	else
		dbConn = m_dbConnPool->getDbConnection();

	QString value = dbConn->singleQuery ( statement );

	if ( conn == nullptr )
		m_dbConnPool->putDbConnection ( dbConn );

	if (clock.elapsed() > 2000)
		qWarning() << statement << "took too long" << clock.elapsed() / 1000;

	return value;
}

/**
 * Executes a SQL insert on the already opened database
 * @param statement SQL statement to execute. Only one SQL statement is allowed.
 * @return		The rowid of the inserted item.
 */
auto KurooDB::insert( const QString& statement, DbConnection *conn ) -> int
{
	QElapsedTimer clock;
	clock.start();

	DbConnection *dbConn = nullptr;
	if ( conn != nullptr )
		dbConn = conn;
	else
		dbConn = m_dbConnPool->getDbConnection();

	int id = dbConn->insert ( statement );

	if ( conn == nullptr )
		m_dbConnPool->putDbConnection ( dbConn );

	if (clock.elapsed() > 2000)
		qWarning() << statement << "took too long" << clock.elapsed() / 1000;

	return id;
}

auto KurooDB::isCacheEmpty() -> bool
{
	QString values = singleQuery ( QStringLiteral("SELECT COUNT(id) FROM cache LIMIT 0, 1;") );
	return values.isEmpty() ? true : values == u'0';
}

auto KurooDB::isPortageEmpty() -> bool
{
	QString values = singleQuery ( QStringLiteral("SELECT COUNT(id) FROM package LIMIT 0, 1;") );
	return values.isEmpty() ? true : values == u'0';
}

auto KurooDB::isQueueEmpty() -> bool
{
	QString values = singleQuery ( QStringLiteral("SELECT COUNT(id) FROM queue LIMIT 0, 1;") );
	return values.isEmpty() ? true : values == u'0';
}

auto KurooDB::isUpdatesEmpty() -> bool
{
	QString values = singleQuery ( QStringLiteral ( "SELECT COUNT(id) FROM package where status = '%1' LIMIT 0, 1;" )
									.arg ( PACKAGE_UPDATES_STRING ) );
	return values.isEmpty() ? true : values == u'0';
}

auto KurooDB::isHistoryEmpty() -> bool
{
	QString values = singleQuery ( QStringLiteral("SELECT COUNT(id) FROM history LIMIT 0, 1;") );
	return values.isEmpty() ? true : values == u'0';
}

auto KurooDB::isValid() -> bool
{
	QString value1 = singleQuery ( QStringLiteral("SELECT COUNT(id) FROM category LIMIT 0, 1;") );
	QString value2 = singleQuery ( QStringLiteral("SELECT COUNT(id) FROM package LIMIT 0, 1;") );
	//A lot of times when the database gets corrupted, it has one value int it, and we know that there should
	//always be more than one.
	return ( !value1.isEmpty() && value1.toInt() != 1 && !value2.isEmpty() && value2.toInt() != 1 );
}

/**
 * Create all necessary tables.
 */
void KurooDB::createTables( DbConnection *conn )
{
	qDebug() << "Creating tables";
	query ( QStringLiteral("CREATE TABLE dbInfo ( "
			"meta VARCHAR(64), "
			"data VARCHAR(64) );")
			, conn );

	query ( QStringLiteral(" INSERT INTO dbInfo (meta, data) VALUES ('syncTimeStamp', '0');"), conn );
	query ( QStringLiteral(" INSERT INTO dbInfo (meta, data) VALUES ('packageCount', '0');"), conn );
	query ( QStringLiteral(" INSERT INTO dbInfo (meta, data) VALUES ('scanDuration', '100');"), conn );

	query ( QStringLiteral("CREATE TABLE category ( "
			"id INTEGER PRIMARY KEY AUTOINCREMENT, "
			"name VARCHAR(32) UNIQUE );")
			, conn );

	query ( QStringLiteral("CREATE TABLE subCategory ( "
			"id INTEGER PRIMARY KEY AUTOINCREMENT, "
			"name VARCHAR(32), "
			"idCategory INTEGER );")
			, conn );

	query ( QStringLiteral("CREATE TABLE package ( "
			"id INTEGER PRIMARY KEY AUTOINCREMENT, "
			"idCategory INTEGER, "
			"idSubCategory INTEGER, "
			"category VARCHAR(32), "
			"name VARCHAR(32), "
			"description VARCHAR(255), "
			"path VARCHAR(64), "
			"status INTEGER, "
			"meta VARCHAR(255), "
			"updateVersion VARCHAR(32) ); ")
			, conn );

	query ( QStringLiteral("CREATE TABLE version ( "
			"id INTEGER PRIMARY KEY AUTOINCREMENT, "
			"idPackage INTEGER, "
			"name VARCHAR(32), "
			"description VARCHAR(255), "
			"homepage VARCHAR(128), "
			"licenses VARCHAR(64), "
			"useFlags VARCHAR(255), "
			"slot VARCHAR(32), "
			"size VARCHAR(32), "
			"status INTEGER, "
			"keywords VARCHAR(32) );")
			, conn );

	query ( QStringLiteral("CREATE TABLE queue ( "
			"id INTEGER PRIMARY KEY AUTOINCREMENT, "
			"idPackage INTEGER, "
			"idDepend INTEGER, "
			"use VARCHAR(255), "
			"size VARCHAR(32), "
			"version VARCHAR(32) );")
			, conn );

	query ( QStringLiteral("CREATE TABLE history ( "
			"id INTEGER PRIMARY KEY AUTOINCREMENT, "
			"package VARCHAR(32), "
			"timestamp VARCHAR(10), "
			"time INTEGER, "
			"einfo BLOB, "
			"emerge BOOL );")
			, conn );

	query ( QStringLiteral("CREATE TABLE mergeHistory ( "
			"id INTEGER PRIMARY KEY AUTOINCREMENT, "
			"timestamp VARCHAR(10), "
			"source VARCHAR(255), "
			"destination VARCHAR(255) );")
			, conn );

	query ( QStringLiteral(" CREATE TABLE statistic ( "
			"id INTEGER PRIMARY KEY AUTOINCREMENT, "
			"package VARCHAR(32), "
			"time INTEGER, "
			"count INTEGER );")
			, conn );

	query ( QStringLiteral("CREATE TABLE cache ( "
			"id INTEGER PRIMARY KEY AUTOINCREMENT, "
			"package VARCHAR(32), "
			"size INTEGER );")
			, conn );

	query ( QStringLiteral("CREATE TABLE packageHardMask ( "
			"idPackage INTEGER, "
			"dependAtom VARCHAR(255), "
			"comment BLOB );")
			, conn );

	query ( QStringLiteral("CREATE TABLE packageUserMask ( "
			"idPackage INTEGER UNIQUE, "
			"dependAtom VARCHAR(255), "
			"comment BLOB );")
			, conn );

	query ( QStringLiteral("CREATE TABLE packageUnmask ( "
			"idPackage INTEGER UNIQUE, "
			"dependAtom VARCHAR(255), "
			"comment BLOB );")
			, conn );

	query ( QStringLiteral("CREATE TABLE packageKeywords ( "
			"idPackage INTEGER UNIQUE, "
			"keywords VARCHAR(255) );")
			, conn );

	query ( QStringLiteral("CREATE TABLE packageUse ( "
			"idPackage INTEGER UNIQUE, "
			"use VARCHAR(255) );")
			, conn );

	query ( QStringLiteral("CREATE INDEX index_name_package ON package(name);"), conn );
	query ( QStringLiteral("CREATE INDEX index_category_package ON package(category);"), conn );
	query ( QStringLiteral("CREATE INDEX index_history_timestamp ON history(timestamp);"), conn );
	query ( QStringLiteral("CREATE INDEX index_package_id ON package(id);"), conn );
	query ( QStringLiteral("CREATE INDEX index_version_packageId ON version(packageId);"), conn );
}


//////////////////////////////////////////////////////////////////////////////
// Database management
//////////////////////////////////////////////////////////////////////////////

/**
 * Backup to file data which can not be recreated, fex history.einfo and mergeHistory.source/destination
 */
void KurooDB::backupDb()
{
	const QStringList historyData = query ( QStringLiteral("SELECT timestamp, einfo FROM history WHERE einfo > ''; ") );
	if ( !historyData.isEmpty() )
	{
		QFile file ( kurooDir + KurooConfig::fileHistoryBackup() );
		if ( file.open ( QIODevice::WriteOnly ) )
		{
			QTextStream stream ( &file );
			QStringList::const_iterator i = historyData.begin();
			QStringList::const_iterator e = historyData.end();
			while ( i != e )
			{
				QString timestamp = *i++;
				QString einfo = *i++;
				stream << timestamp << ":" << einfo << "\n";
			}
			file.close();
		}
		else
			qCritical() << QStringLiteral ( "Creating backup of history. Writing: %1." ).arg ( KurooConfig::fileHistoryBackup() );
	}

	const QStringList mergeData = query ( QStringLiteral("SELECT timestamp, source, destination FROM mergeHistory;") );
	if ( !mergeData.isEmpty() )
	{
		QFile file ( kurooDir + KurooConfig::fileMergeBackup() );
		if ( file.open ( QIODevice::WriteOnly ) )
		{
			QTextStream stream ( &file );
			QStringList::const_iterator i = mergeData.begin();
			QStringList::const_iterator e = mergeData.end();
			//foreach ( mergeData )
			while ( i != e )
			{
				QString timestamp = *i++;
				QString source = *i++;
				QString destination = *i++;
				stream << timestamp << ":" << source << ":" << destination << "\n";
			}
			file.close();
		}
		else
			qCritical() << QStringLiteral ( "Creating backup of history. Writing: %1." ).arg ( KurooConfig::fileMergeBackup() );
	}
}

/**
 * Restore data to tables history and mergeHistory
 */
void KurooDB::restoreBackup()
{
	/*// Restore einfo into table history
	QFile file ( GlobalSingleton::Instance()->kurooDir() + KurooConfig::fileHistoryBackup() );
	QTextStream stream ( &file );
	QStringList lines;
	if ( !file.open ( QIODevice::ReadOnly ) )
		qCritical() << QString ( "Restoring backup of history. Reading: %1." ).arg ( KurooConfig::fileHistoryBackup() );
	else
	{
		while ( !stream.atEnd() )
			lines += stream.readLine();
		file.close();
	}

	QRegExp rxHistoryLine ( "(\\d+):((?:\\S|\\s)*)" );
	for ( QStringList::Iterator it = lines.begin(), end = lines.end(); it != end; ++it )
	{
		if ( ! ( *it ).isEmpty() && rxHistoryLine.exactMatch ( *it ) )
		{
			QString timestamp = rxHistoryLine.cap ( 1 );
			QString einfo = rxHistoryLine.cap ( 2 );
			singleQuery ( "UPDATE history SET einfo = '" + escapeString ( einfo ) + "' WHERE timestamp = '" + timestamp + "';" );
		}
	}

	// Restore source and destination into table mergeHistory
	file.setName ( GlobalSingleton::Instance()->kurooDir() + KurooConfig::fileMergeBackup() );
	stream.setDevice ( &file );
	lines.clear();
	if ( !file.open ( QIODevice::ReadOnly ) )
		qCritical() << QString ( "Restoring backup of history. Reading: %1." ).arg ( KurooConfig::fileMergeBackup() );
	else
	{
		while ( !stream.atEnd() )
			lines += stream.readLine();
		file.close();
	}

	QRegExp rxMergeLine ( "(\\d+):((?:\\S|\\s)*):((?:\\S|\\s)*)" );
	for ( QStringList::Iterator it = lines.begin(), end = lines.end(); it != end; ++it )
	{
		if ( ! ( *it ).isEmpty() && rxMergeLine.exactMatch ( *it ) )
		{
			QString timestamp = rxMergeLine.cap ( 1 );
			QString source = rxMergeLine.cap ( 2 );
			QString destination = rxMergeLine.cap ( 3 );
			singleQuery ( "INSERT INTO mergeHistory (timestamp, source, destination) "
						  "VALUES ('" + timestamp + "', '" + source + "', '" + destination + "');" );
		}
	}*/
}


//////////////////////////////////////////////////////////////////////////////
// Queries for kuroo
//////////////////////////////////////////////////////////////////////////////

/**
 * Return db meta data.
 */
auto KurooDB::getKurooDbMeta ( const QString& meta ) -> QString
{
	return singleQuery ( QStringLiteral("SELECT data FROM dbInfo WHERE meta = '%1' LIMIT 1;").arg( meta ) );
}

/**
 * Set db meta data.
 * @param version
 */
void KurooDB::setKurooDbMeta ( const QString& meta, const QString& data )
{
	if ( singleQuery ( QStringLiteral("SELECT COUNT(meta) FROM dbInfo WHERE meta = '%1' LIMIT 1;").arg( meta ) ) == u'0' )
		insert ( QStringLiteral("INSERT INTO dbInfo (meta, data) VALUES ('%1', '%2');").arg( meta, data ) );
	else
		singleQuery ( QStringLiteral("UPDATE dbInfo SET data = '%1' WHERE meta = '%2';").arg( data, meta ) );
}

//////////////////////////////////////////////////////////////////////////////
// Queries for portage
//////////////////////////////////////////////////////////////////////////////

/**
 * Return all categories, eg app in app-portage/kuroo.
 * "0" is appended in front of the list so as not to miss the first category when categories are entered in reverse order in listview.
 */
auto KurooDB::allCategories() -> const QStringList
{
	QStringList resultList ( QStringLiteral("0") );
	resultList += query ( QStringLiteral("SELECT name FROM category; ") );
	return resultList;
}

/**
 * Return all subcategories, eg portage in app-portage/kuroo.
 */
auto KurooDB::allSubCategories() -> const QStringList
{
	return query ( QStringLiteral("SELECT idCategory, id, name FROM subCategory ORDER BY name; ") );
}

/**
 * Return all categories which have packages matching the filter and the text.
 * @param filter	packages status as PACKAGE_AVAILABLE, PACKAGE_INSTALLED or PACKAGE_UPDATES
 * @param text		search string
 */
auto KurooDB::portageCategories( int filter, const QString& text ) -> const QStringList
{
	QString filterQuery, textQuery;
//	int len;

	// Allow for multiple words match
	QString textString = escapeString ( text.simplified() );
	QStringList textStringList = textString.split(u' ');

	// Concatenate all search words
	if ( !textStringList.isEmpty() )
	{
		while ( !textStringList.isEmpty() )
		{
			if (textStringList.first().length() > 0)
			{
				textQuery += QStringLiteral(" AND (meta LIKE '%%1%')").arg( textStringList.takeFirst() );
			}
			else
				textStringList.takeFirst();
		}
		/*len = textQuery.length();
		if (len > 0)
			textQuery = "( " + textQuery.right ( len - 5 ) + " ) ";
		else
			textQuery = "";*/
	}

	switch ( filter )
	{
		case PACKAGE_AVAILABLE:
			filterQuery = QStringLiteral(" WHERE package.status & ") + PACKAGE_ALL_STRING;
			break;

		case PACKAGE_INSTALLED:
			filterQuery = QStringLiteral(" WHERE package.status & ") + PACKAGE_INSTALLED_UPDATES_OLD_STRING;
			break;

		case PACKAGE_UPDATES:
			filterQuery = QStringLiteral(" WHERE package.status & ") + PACKAGE_UPDATES_STRING;
			break;

		default:
			qDebug() << LINE_INFO << "This switch needs an extra case!";
			break;
	}

	return query ( QStringLiteral("SELECT DISTINCT idCategory FROM package %1%2 ;").arg( filterQuery, textQuery ) );
}

/**
 * Return all subcategories which have packages matching the filter and the text in this category.
 * @param categoryId 	category id
 * @param filter		packages status as PACKAGE_AVAILABLE, PACKAGE_INSTALLED or PACKAGE_UPDATES
 * @param text			search string
 */
auto KurooDB::portageSubCategories( const QString& categoryId, int filter, const QString& text ) -> const QStringList
{
	QString filterQuery, textQuery;
	QStringList resultList ( categoryId );
	int len = 0;

	// Allow for multiple words match
	QString textString = escapeString ( text.simplified() );
	QStringList textStringList = textString.split(u' ');

	// Concatenate all search words
	if ( !textStringList.isEmpty() )
	{
		while ( !textStringList.isEmpty() )
		{
			textQuery += QStringLiteral(" AND meta LIKE '%%1%' ").arg( textStringList.first() );
			textStringList.pop_front();
		}
		len = textQuery.length();
		textQuery = QStringLiteral(" AND ( %1 ) ").arg( textQuery.right ( len - 5 ) );
	}

	if ( categoryId != u'0' )
	{

		switch ( filter )
		{
			case PACKAGE_AVAILABLE:
				filterQuery = QStringLiteral(" AND package.status & ") + PACKAGE_ALL_STRING;
				break;

			case PACKAGE_INSTALLED:
				filterQuery = QStringLiteral(" AND package.status & ") + PACKAGE_INSTALLED_UPDATES_OLD_STRING;
				break;

			case PACKAGE_UPDATES:
				filterQuery = QStringLiteral(" AND package.status & ") + PACKAGE_UPDATES_STRING;
				break;

			default:
				qDebug() << LINE_INFO << "This switch needs an extra case!";
				break;
		}

		resultList += query ( QStringLiteral(" SELECT DISTINCT idSubCategory FROM package WHERE idCategory = '%1'%2%3 ;")
							.arg( categoryId, filterQuery, textQuery ) );
	}

	// Add meta-subcategory when query is successful
	if ( resultList.size() > 1 )
		resultList += QStringLiteral("0");

	return resultList;
}

/**
 * Return all packages which are matching the filter and the text in this category-subcategory.
 * @param categoryId	category id
 * @param subCategoryId	subcategory id
 * @param filter		packages status as PACKAGE_AVAILABLE, PACKAGE_INSTALLED or PACKAGE_UPDATES
 * @param text			search string
 */
auto KurooDB::portagePackagesBySubCategory( const QString& categoryId, const QString& subCategoryId, int filter, const QString& text ) -> const QStringList
{
	QString filterQuery, textQuery;
//	int len;

	// Allow for multiple words match
	QString textString = escapeString( text.simplified() );
	QStringList textStringList = textString.split(u' ');

	// Concatenate all search words
	if ( !textStringList.isEmpty() )
	{
		while ( !textStringList.isEmpty() )
		{
			if (textStringList.first().length() > 0)
				textQuery += QStringLiteral(" AND (meta LIKE '%%1%')").arg( textStringList.first() );
			textStringList.pop_front();
		}
		/*len = textQuery.length();
		if (len > 0)
			textQuery = " AND ( " + textQuery.right( len - 5 ) + " ) ";
		else
			textQuery = "";*/
	}

	switch ( filter )
	{
		case PACKAGE_AVAILABLE:
			filterQuery = QStringLiteral(" AND package.status & ") + PACKAGE_ALL_STRING;
			break;

		case PACKAGE_INSTALLED:
			filterQuery = QStringLiteral(" AND package.status & ") + PACKAGE_INSTALLED_UPDATES_OLD_STRING;
			break;

		case PACKAGE_UPDATES:
			filterQuery = QStringLiteral(" AND package.status & ") + PACKAGE_UPDATES_STRING;
			break;

		default:
			qDebug() << LINE_INFO << "This switch needs an extra case!";
			break;
	}

	if ( categoryId == u'0' )
	{

		if ( subCategoryId == u'0' )
		{
			switch ( filter )
			{
				case PACKAGE_AVAILABLE:
					filterQuery = QStringLiteral(" WHERE package.status & ") + PACKAGE_ALL_STRING;
					break;

				case PACKAGE_INSTALLED:
					filterQuery = QStringLiteral(" WHERE package.status & ") + PACKAGE_INSTALLED_UPDATES_OLD_STRING;
					break;

				case PACKAGE_UPDATES:
					filterQuery = QStringLiteral(" WHERE package.status & ") + PACKAGE_UPDATES_STRING;
					break;

				default:
					qDebug() << LINE_INFO << "This switch needs an extra case!";
					break;
			}

			return query( QStringLiteral(" SELECT id, name, category, description, status, updateVersion "
							" FROM package %1%2 ORDER BY name DESC;").arg( filterQuery, textQuery ) );
		}

		return query( QStringLiteral(" SELECT id, name, category, description, status, updateVersion "
						" FROM package "
						" WHERE idSubCategory = '%1'%2%3 ORDER BY name DESC;")
						.arg( subCategoryId, filterQuery, textQuery ) );

	}

	if ( subCategoryId == u'0' )
	{

		return query( QStringLiteral(" SELECT id, name, category, description, status, updateVersion "
						" FROM package "
						" WHERE idCategory = '%1'%2%3 ORDER BY name DESC;")
						.arg( categoryId, filterQuery, textQuery ) );
	}

	return query( QStringLiteral(" SELECT id, name, category, description, status, updateVersion "
					" FROM package "
					" WHERE idCategory = '%1' AND idSubCategory = '%2'%3%4 ORDER BY name DESC;")
					.arg( categoryId, subCategoryId, filterQuery, textQuery ) );
}


//////////////////////////////////////////////////////////////////////////////
// Query for packages
//////////////////////////////////////////////////////////////////////////////

/**
 * Return package name, eg kuroo in app-portage/kuroo.
 * @param id
 */
auto KurooDB::package( const QString& id ) -> const QString
{
	QString name = singleQuery( QStringLiteral("SELECT name FROM package WHERE id = '%1' LIMIT 1;").arg( id ) );

	if ( !name.isEmpty() )
		return name;
	qWarning() << QStringLiteral ( "Can not find package in database for id %1." ).arg ( id );

	return QString();
}

/**
 * Return category name, eg app-portage in app-portage/kuroo.
 * @param id
 */
auto KurooDB::category( const QString& id ) -> const QString
{
	QString category = singleQuery( QStringLiteral("SELECT category FROM package WHERE id = '%1' LIMIT 1;").arg( id ) );

	if ( !category.isEmpty() )
		return category;
	qWarning() << QStringLiteral ( "Can not find category in database for id %1." ).arg ( id );

	return QString();
}

/**
 * Return package when searching by category-subcategory and name.
 * @param category		category-subcategory
 * @param name
 */
auto KurooDB::packageId( const QString& package ) -> const QString
{
	QStringList parts = parsePackage( package );
	if ( !parts.isEmpty() )
	{
		QString category = parts[0];
		QString name = parts[1];
		QString id = singleQuery( QStringLiteral("SELECT id FROM package WHERE name = '%1' AND category = '%2' LIMIT 1;")
								.arg( name, category ) );

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

	}
	else
		qWarning() << "Querying for package id. Can not parse: " << package;

	return QString();
}

/**
 * Return all versions for this package.
 * @param id
 */
auto KurooDB::packageVersionsInstalled( const QString& idPackage ) -> const QStringList
{
	return query( QStringLiteral(" SELECT name FROM version WHERE idPackage = '%1' AND status = '%2' ORDER BY version.name;")
						.arg( idPackage, PACKAGE_INSTALLED_STRING ) );
}

/**
 * Return all versions and their info for this package.
 * @param id
 */
auto KurooDB::packageVersionsInfo( const QString& idPackage ) -> const QStringList
{
	return query( QStringLiteral(" SELECT name, description, homepage, status, licenses, useFlags, slot, keywords, size "
					" FROM version WHERE idPackage = '%1' ORDER BY version.name;").arg( idPackage ) );
}

/**
 * Return the size of this package version.
 * @param idPackage
 * @param version
 */
auto KurooDB::versionSize( const QString& idPackage, const QString& version ) -> const QString
{
	return singleQuery( QStringLiteral(" SELECT size, status FROM version WHERE idPackage = '%1'"
						 " AND name = '%2' LIMIT 1;").arg( idPackage, version ) );
}

/**
 * Return hardmask dependAtom and the gentoo dev comment.
 * @param id
 */
auto KurooDB::packageHardMaskInfo( const QString& id ) -> const QStringList
{
	return query( QStringLiteral("SELECT dependAtom, comment FROM packageHardMask WHERE idPackage = '%1' LIMIT 1;").arg( id ) );
}

/**
 * Return path, eg where to find the package: in Portage or in any overlay.
 * @param idPackage
 * @param version
 */
auto KurooDB::packagePath( const QString& id ) -> const QString
{
	return singleQuery( QStringLiteral("SELECT path FROM package WHERE id = '%1' LIMIT 1;").arg( id ) );
}

/**
 * Return package hardmask depend atom.
 * @param id
 */
auto KurooDB::packageHardMaskAtom( const QString& id ) -> const QStringList
{
	return query( QStringLiteral("SELECT dependAtom FROM packageHardMask WHERE idPackage = '%1';").arg( id ) );
}

/**
 * Return package user-mask depend atom.
 * @param id
 */
auto KurooDB::packageUserMaskAtom( const QString& id ) -> const QStringList
{
	return query( QStringLiteral("SELECT dependAtom FROM packageUserMask WHERE idPackage = '%1';").arg( id ) );
}

/**
 * Return package unmask depend atom.
 * @param id
 */
auto KurooDB::packageUnMaskAtom( const QString& id ) -> const QStringList
{
	return query( QStringLiteral("SELECT dependAtom FROM packageUnmask WHERE idPackage = '%1';").arg( id ) );
}

/**
 * Return package keyword atom.
 * @param id
 */
auto KurooDB::packageKeywordsAtom( const QString& id ) -> const QString
{
	return singleQuery( QStringLiteral("SELECT keywords FROM packageKeywords WHERE idPackage = '%1' LIMIT 1;").arg( id ) );
}

auto KurooDB::packageUse( const QString& id ) -> const QString
{
	return singleQuery( QStringLiteral("SELECT use FROM packageUse where idPackage = '%1' LIMIT 1;").arg( id ) );
}

/**
 * Is the package in package.keywords?
 * @param id
 */
auto KurooDB::isPackageUnTesting( const QString& id ) -> bool
{
	QString keywords = singleQuery( QStringLiteral("SELECT keywords FROM packageKeywords where idPackage = '%1' LIMIT 1;").arg( id ) );
	static const QRegularExpression unstableStarOrArch( QStringLiteral("(~\\*)|(~%1)").arg( KurooConfig::arch() ) );
	return keywords.contains( unstableStarOrArch );
}

/**
 * Is the package available in package.keywords?
 * @param id
 */
auto KurooDB::isPackageAvailable( const QString& id ) -> bool
{
	QString keywords = singleQuery( QStringLiteral("SELECT keywords FROM packageKeywords where idPackage = '%1' LIMIT 1;").arg( id ) );
	static const QRegularExpression maskedStarOrArch( QStringLiteral("(\\-\\*)|(\\-%1)").arg( KurooConfig::arch() ) );
	return keywords.contains( maskedStarOrArch );
}

/**
 * Is the package in package.unmask? @fixme: better way to check for existens.
 * @param id
 */
auto KurooDB::isPackageUnMasked( const QString& id ) -> bool
{
	return !query( QStringLiteral("SELECT dependAtom FROM packageUnmask where idPackage = '%1';").arg( id ) ).isEmpty();
}

/**
 * Add use flags for this package.
 * @param id
 */
void KurooDB::setPackageUse( const QString& id, const QString& useFlags )
{
	singleQuery( QStringLiteral("REPLACE INTO packageUse (idPackage, use) VALUES ('%1', '%2');").arg( id, useFlags ) );
}

void KurooDB::setPackageUnMasked( const QString& id )
{
	singleQuery( QStringLiteral("REPLACE INTO packageUnmask (idPackage, dependAtom) VALUES ('%1', '%2/%3');")
				.arg( id, category( id ), package( id ) ) );
}

/**
 * Add package in package.unmask. @fixme: check category and package?
 * @param id
 */
void KurooDB::setPackageUnMasked( const QString& id, const QString& version )
{
	singleQuery( QStringLiteral("REPLACE INTO packageUnmask (idPackage, dependAtom) VALUES ('%1', "
				  "'=%2/%3-%4');").arg( id, category( id ), package( id ), version ) );
}

/**
 * Add package in package.mask. @fixme: check category and package?
 * @param id
 */
void KurooDB::setPackageUserMasked( const QString& id )
{
	singleQuery( QStringLiteral("REPLACE INTO packageUserMask (idPackage, dependAtom) VALUES ('%1', "
				  "'%2/%3');").arg( id,category( id ), package( id ) ) );
}

/**
 * Set package as testing, eg add keyword ~*.
 * @param id
 */
void KurooDB::setPackageUnTesting( const QString& id )
{
	QString keywords = packageKeywordsAtom( id );

	// Aready testing skip!
	static const QRegularExpression unstableStarOrArch( QStringLiteral("(~\\*)|(~%1)").arg( KurooConfig::arch() ) );
	if ( keywords.contains( unstableStarOrArch ) )
		return;

	if ( keywords.isEmpty() )
		insert( QStringLiteral("INSERT INTO packageKeywords (idPackage, keywords) VALUES ('%1', '~*');").arg( id ) );
	else
		singleQuery( QStringLiteral("UPDATE packageKeywords SET keywords = '%1 ~*' WHERE idPackage = '%2';").arg( keywords, id ) );
}

/**
 * Set package as available, eg add keywords '-* -arch'
 */
void KurooDB::setPackageAvailable( const QString& id )
{
	QString keywords = packageKeywordsAtom( id );

	// Already available skip!
	static const QRegularExpression maskedStarOrArch( QStringLiteral("(\\-\\*)|(\\-%1)").arg( KurooConfig::arch() ) );
	if ( keywords.contains( maskedStarOrArch ) )
		return;

	if ( keywords.isEmpty() )
		insert( QStringLiteral("INSERT INTO packageKeywords (idPackage, keywords) VALUES ('%1', '-* -%2');").arg( id , KurooConfig::arch() ) );
	else
		singleQuery( QStringLiteral("UPDATE packageKeywords SET keywords = '%1 -* -%2' WHERE idPackage = '%3';").arg( keywords, KurooConfig::arch(), id ) );
}

/**
 * Clear testing keyword from package.
 * @param id
 */
void KurooDB::clearPackageUnTesting( const QString& id )
{
	QString keywords = packageKeywordsAtom( id );

	// If only testing keywords - remove it, else set only available keywords
	static const QRegularExpression maskedStarOrArch( QStringLiteral("(\\-\\*)|(\\-%1)").arg( KurooConfig::arch() ) );
	if ( !keywords.contains( maskedStarOrArch ) )
		singleQuery( QStringLiteral("DELETE FROM packageKeywords WHERE idPackage = '%1';").arg( id ) );
	else
		singleQuery( QStringLiteral("UPDATE packageKeywords SET keywords = '-* -%1' WHERE idPackage = '%2';").arg( KurooConfig::arch(), id ) );
}

/**
 * Removing available keywords for package.
 * @param id
 */
void KurooDB::clearPackageAvailable( const QString& id )
{
	QString keywords = packageKeywordsAtom( id );

	// If only available keywords - remove it, else set only testing keyword
	static const QRegularExpression unstableStarOrArch( QStringLiteral("(~\\*)|(~%1)").arg( KurooConfig::arch() ) );
	if ( !keywords.contains( unstableStarOrArch ) )
		singleQuery( QStringLiteral("DELETE FROM packageKeywords WHERE idPackage = '%1';").arg( id ) );
	else
		singleQuery( QStringLiteral("UPDATE packageKeywords SET keywords = '~*' WHERE idPackage = '%1';").arg( id ) );
}

/**
 * Clear package from package.unmask.
 * @param id
 */
void KurooDB::clearPackageUnMasked( const QString& id )
{
	singleQuery( QStringLiteral("DELETE FROM packageUnmask WHERE idPackage = '%1';").arg( id ) );
}

/**
 * Clear package from package.mask.
 * @param id
 */
void KurooDB::clearPackageUserMasked( const QString& id )
{
	singleQuery( QStringLiteral("DELETE FROM packageUserMask WHERE idPackage = '%1';").arg( id ) );
}


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

/**
 * Return all packages in the queue.
 */
auto KurooDB::allQueuePackages() -> const QStringList
{
	return query( QStringLiteral(" SELECT package.id, package.category, package.name, "
					" package.status, queue.idDepend, queue.size, queue.version "
					" FROM queue, package "
					" WHERE queue.idPackage = package.id "
					" ORDER BY queue.idDepend;") );
}

/**
 * Return all packages in the queue.
 */
auto KurooDB::allQueueId() -> const QStringList
{
	return query( QStringLiteral("SELECT idPackage FROM queue;") );
}

/**
 * Return all history.
 */
auto KurooDB::allHistory() -> const QStringList
{
	return query( QStringLiteral("SELECT timestamp, package, time, einfo FROM history ORDER BY id DESC;") );
}

/**
 * Return all etc-update history.
 */
auto KurooDB::allMergeHistory() -> const QStringList
{
	return query( QStringLiteral("SELECT timestamp, source, destination FROM mergeHistory ORDER BY id DESC;") );
}

/**
 * Return all package statistics.
 */
auto KurooDB::allStatistic() -> const QStringList
{
	return query( QStringLiteral("SELECT package, time, count FROM statistic ORDER BY id ASC;") );
}

/**
 * Clear all updates.
 */
void KurooDB::resetUpdates()
{
	singleQuery( QStringLiteral("UPDATE package SET updateVersion = '' WHERE updateVersion != '';") );
}

/**
 * Clear all installed.
 */
void KurooDB::resetInstalled()
{
	singleQuery( QStringLiteral("UPDATE package set installed = '%1';").arg( PACKAGE_AVAILABLE_STRING ) );
}

void KurooDB::resetQueue()
{
	singleQuery( QStringLiteral("DELETE FROM queue;") );
}

void KurooDB::addEmergeInfo( const QString& einfo )
{
	singleQuery( QStringLiteral ( "UPDATE history SET einfo = '%1' WHERE id = (SELECT MAX(id) FROM history);" ).arg ( escapeString ( einfo ) ) );
}

/**
 * Insert etc-update file names.
 * @param source
 * @param destination
 */
void KurooDB::addBackup( const QString& source, const QString& destination )
{
	insert( QStringLiteral ( "INSERT INTO mergeHistory ( timestamp, source, destination ) VALUES ('%1', '%2', '%3');" )
			 .arg( QString::number( QDateTime::currentSecsSinceEpoch() ), source, destination ) );
}

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

/**
 */
DbConnection::DbConnection( DbConfig* config )
		: m_config ( config )
{}

DbConnection::~DbConnection()
= default;

/**
 * Sqlite methods
 */
SqliteConnection::SqliteConnection( SqliteConfig* config )
		: DbConnection( config )
{
	const QString path = kurooDir + KurooConfig::databas();

	// Open database file and check for correctness
	m_initialized = false;
	QFile file( path );
	if ( file.open( QIODevice::ReadOnly ) ) {
		QString format = QString::fromUtf8( file.readLine( 50 ) );

		if ( !format.startsWith( QStringLiteral("SQLite format 3") ) )
			qWarning() << "Database versions incompatible. Removing and rebuilding database.";
		else
		{
			if ( sqlite3_open( path.toUtf8().data(), &m_db ) != SQLITE_OK )
			{
				qWarning() << "Database file corrupt. Removing and rebuilding database.";
				sqlite3_close( m_db );
			}
			else
				m_initialized = true;
		}
	}

	if ( !m_initialized )
	{
		// Remove old db file; create new
		QFile::remove( path );
		if ( sqlite3_open( path.toUtf8().data(), &m_db ) == SQLITE_OK )
			m_initialized = true;
	}
	else
	{
		if ( sqlite3_create_function( m_db, "rand", 0, SQLITE_UTF8, nullptr, sqlite_rand, nullptr, nullptr ) != SQLITE_OK )
			m_initialized = false;
		if ( sqlite3_create_function( m_db, "power", 2, SQLITE_UTF8, nullptr, sqlite_power, nullptr, nullptr ) != SQLITE_OK )
			m_initialized = false;
	}

	//optimization for speeding up SQLite
	query( QStringLiteral("PRAGMA default_synchronous = OFF;") );
}

SqliteConnection::~SqliteConnection()
{
	if ( m_db )
		sqlite3_close( m_db );
}

auto SqliteConnection::query( const QString& statement ) -> QStringList
{
	QStringList values;
	int error = 0;
	const char* tail = nullptr;
	sqlite3_stmt* stmt = nullptr;

	int busyCnt( 0 );
	while (true) {
		//compile SQL program to virtual machine
		error = sqlite3_prepare( m_db, statement.toUtf8().data(), statement.length(), &stmt, &tail );
		if ( SQLITE_LOCKED == error || SQLITE_BUSY == error ) {
			if ( ++busyCnt > 99 ) {
				qWarning() << "Busy-counter reached" << busyCnt << "on prepare, aborting" << statement;
				break;
			}
			qDebug() << "sqlite3_prepare: BUSY counter: " << busyCnt << " on query: " << statement;
			::sleep( 1 );
		} else
			break;
	}

	if ( error != SQLITE_OK )
	{
		qWarning() << "sqlite3_compile error: " << error << sqlite3_errmsg( m_db ) << " on query: " << statement;
		values = QStringList();
	}
	else
	{
		int busyCnt( 0 );
		int number = sqlite3_column_count( stmt );

		//execute virtual machine by iterating over rows
		while ( true )
		{
			error = sqlite3_step( stmt );

			if ( error == SQLITE_BUSY )
			{
				if ( busyCnt++ > 99 )
				{
					qWarning() << "Busy-counter has reached maximum. Aborting this sql statement!";
					break;
				}
				::usleep( 1000000 ); // Sleep 1000 msec
				qDebug() << "sqlite3_step: BUSY counter: " << busyCnt << " on query: " << statement;
				continue;
			}

			if ( error == SQLITE_MISUSE ) {
				qWarning() << "sqlite3_step: MISUSE on query: " << statement;
				break;
			}

			if ( error == SQLITE_DONE || error == SQLITE_ERROR )
				break;

			//iterate over columns
			for ( int i = 0; i < number; i++ )
				values << QString::fromUtf8( reinterpret_cast< const char*>(sqlite3_column_text( stmt, i )) );
		}

		//deallocate vm ressources
		sqlite3_finalize( stmt );

		if ( error != SQLITE_DONE )
		{
			qWarning() << "sqlite_step error: " << sqlite3_errmsg( m_db ) << " on query: " << statement;
			values = QStringList();
		}
	}

	return values;
}

auto SqliteConnection::singleQuery( const QString& statement ) -> QString
{
	QString value;
	int error = 0;
	const char* tail = nullptr;
	sqlite3_stmt* stmt = nullptr;

	int busyCnt( 0 );
	while (true) {
		//compile SQL program to virtual machine
		error = sqlite3_prepare( m_db, statement.toUtf8().data(), statement.length(), &stmt, &tail );
		if ( SQLITE_LOCKED == error || SQLITE_BUSY == error ) {
			if ( ++busyCnt > 99 ) {
				qWarning() << "Busy-counter reached" << busyCnt << "on prepare, aborting" << statement;
				break;
			}
			qDebug() << "sqlite3_prepare: BUSY counter: " << busyCnt << " on singleQuery: " << statement;
			::sleep( 1 );
		} else
			break;
	}

	if ( error != SQLITE_OK )
		qWarning() << "sqlite3_compile error: " << error << sqlite3_errmsg( m_db ) << " on query: " << statement;
	else
	{
		int busyCnt( 0 );

		//execute virtual machine
		while ( true )
		{
			error = sqlite3_step( stmt );

			if ( error == SQLITE_BUSY )
			{
				if ( busyCnt++ > 99 )
				{
					qWarning() << "Busy-counter has reached maximum. Aborting this sql statement!";
					break;
				}
				::usleep( 1000000 ); // Sleep 1000 msec
				qDebug() << "sqlite3_step: BUSY counter: " << busyCnt << " on query: " << statement;
				continue;
			}

			if ( error == SQLITE_MISUSE ) {
				qWarning() << "sqlite3_step: MISUSE on query: " << statement;
				break;
			}

			if ( error == SQLITE_DONE || error == SQLITE_ERROR )
				break;

			value = QString::fromUtf8( reinterpret_cast< const char*>(sqlite3_column_text ( stmt, 0 )) );
		}

		//deallocate vm ressources
		sqlite3_finalize( stmt );

		if ( error != SQLITE_DONE )
		{
			qWarning() << "sqlite_step error: " << sqlite3_errmsg( m_db ) << " on query: " << statement;
			value = QString();
		}
	}

	return value;
}

auto SqliteConnection::insert( const QString& statement ) -> int
{
	int error = 0;
	const char* tail = nullptr;
	sqlite3_stmt* stmt = nullptr;

	//compile SQL program to virtual machine
	error = sqlite3_prepare( m_db, statement.toUtf8().data(), statement.toUtf8().length(), &stmt, &tail );

	if ( error != SQLITE_OK )
		qWarning() << "sqlite3_compile error: " << sqlite3_errmsg( m_db ) << " on insert: " << statement;
	else
	{
		int busyCnt( 0 );

		//execute virtual machine by iterating over rows
		while ( true )
		{
			error = sqlite3_step( stmt );

			if ( error == SQLITE_BUSY )
			{
				if ( busyCnt++ > 99 )
				{
					qWarning() << "Busy-counter has reached maximum. Aborting this sql statement!";
					break;
				}
				::usleep( 1000000 ); // Sleep 1000 msec
				qDebug() << "sqlite3_step: BUSY counter: " << busyCnt << " on insert: " << statement;
				continue;
			}

			if ( error == SQLITE_MISUSE ) {
				qWarning() << "sqlite3_step: MISUSE on insert: " << statement;
				break;
			}

			if ( error == SQLITE_DONE || error == SQLITE_ERROR )
				break;
		}

		//deallocate vm ressources
		sqlite3_finalize( stmt );

		if ( error != SQLITE_DONE )
		{
			qWarning() << "sqlite_step error: " << sqlite3_errmsg( m_db ) << " on insert: " << statement;
			return 0;
		}
	}
	return sqlite3_last_insert_rowid( m_db );
}

// this implements a RAND() function compatible with the MySQL RAND() (0-param-form without seed)
void SqliteConnection::sqlite_rand( sqlite3_context *context, int /*argc*/, sqlite3_value ** /*argv*/ )
{
	sqlite3_result_double( context, QRandomGenerator::global()->generateDouble() );
}

// this implements a POWER() function compatible with the MySQL POWER()
void SqliteConnection::sqlite_power( sqlite3_context *context, int argc, sqlite3_value **argv )
{
	Q_ASSERT ( argc==2 );
	if ( sqlite3_value_type( argv[0] ) == SQLITE_NULL || sqlite3_value_type( argv[1] ) == SQLITE_NULL )
	{
		sqlite3_result_null( context );
		return;
	}
	double a = sqlite3_value_double( argv[0] );
	double b = sqlite3_value_double( argv[1] );
	sqlite3_result_double( context, pow( a,b ) );
}

SqliteConfig::SqliteConfig( QString  dbfile )
		: m_dbfile(std::move( dbfile ))
{}

/**
 * Connections pool with thread support
 */
DbConnectionPool::DbConnectionPool() : m_semaphore( POOL_SIZE )
{
	m_semaphore.acquire( POOL_SIZE ); //+=
	DbConnection *dbConn = nullptr;
	m_dbConfig = new SqliteConfig( KurooConfig::databas() );
	dbConn = new SqliteConnection( static_cast<SqliteConfig*>( m_dbConfig ) );

	enqueue( dbConn );
	m_semaphore.release(1); //--
}

DbConnectionPool::~DbConnectionPool()
{
	m_semaphore.acquire( POOL_SIZE );
	bool vacuum = true;

	while( !isEmpty() )
	{
		DbConnection *conn = dequeue();
		if ( vacuum )
		{
			vacuum = false;
			qDebug() << "Running VACUUM";
			conn->query( QStringLiteral("VACUUM;") );
		}
		delete conn;
	}
	delete m_dbConfig;
}

void DbConnectionPool::createDbConnections()
{
	for ( int i = 0; i < POOL_SIZE - 1; i++ )
	{
		DbConnection *dbConn = nullptr;
		dbConn = new SqliteConnection( static_cast<SqliteConfig*>( m_dbConfig ) );
		enqueue( dbConn );
		m_semaphore.release(1);
	}
	qDebug() << "Create. Available db connections: " << m_semaphore.available();
}

auto DbConnectionPool::getDbConnection() -> DbConnection *
{
//	qDebug() << "Get a DB connection from" << m_semaphore.available() << "available.";
	m_semaphore.acquire(1);
	return dequeue();
}

void DbConnectionPool::putDbConnection( DbConnection *conn )
{
//	qDebug() << "Put a DB connection in" << m_semaphore.available() << "available.";
	enqueue( conn );
	m_semaphore.release(1);
}

auto DbConnectionPool::escapeString( const QString& str ) -> QString
{
	QString result=str;
	return result.replace( u'\'', QStringLiteral("''") );
}

