/*
 * portmgr.cc
 *
 * Copyright (c) 2002, 2003 Frerich Raabe <raabe@kde.org>
 *
 * 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. For licensing and distribution details, check the
 * accompanying file 'COPYING'.
 */
#include "databasewrapper.h"
#include "port.h"
#include "portmgr.h"
#include "process.h"

#include <kapplication.h>
#include <kconfig.h>
#include <kdebug.h>
#include <klocale.h>
#include <kmessagebox.h>
#include <knotifyclient.h>

#include <qdir.h>
#include <qregexp.h>

#include <stdlib.h> // for getenv()

// This would be nice in Qt
template <class Container>
Container qUnique( const Container &container )
{
	Container c;
	typename Container::ConstIterator it = container.begin();
	typename Container::ConstIterator end = container.end();
	for ( ; it != end; ++it )
		if ( c.find( *it ) == c.end() )
			c << *it;
	return c;
}

using namespace Barry;

PortMgr *PortMgr::m_instance = 0;

PortMgr &PortMgr::self()
{
	if ( !m_instance )
		m_instance = new PortMgr;
	return *m_instance;
}

PortMgr::PortMgr()
	: m_portDb( 0 ),
	m_pkgDb( 0 )
{
	reparseConfig();
}

PortMgr::~PortMgr()
{
	delete m_portDb;
	delete m_pkgDb;
}

void PortMgr::reparseConfig()
{
	KConfig *cfg = kapp->config();
	cfg->setGroup( "General" );

#ifdef __NetBSD__
	m_portRootDir = QString::fromLatin1( "/usr/opt" );
#else
	m_portRootDir = QString::fromLatin1( "/usr/ports" );
#endif
	QString portDirDefault = QString::fromLatin1( ::getenv( "PORTSDIR" ) );
	if ( portDirDefault.isEmpty() )
		portDirDefault = getMkVariable( QString::fromLatin1( "PORTSDIR" ) );
	if ( portDirDefault.isEmpty() )
		portDirDefault = m_portRootDir;
	 m_portRootDir = cfg->readEntry( QString::fromLatin1( "Port directory" ), portDirDefault ).stripWhiteSpace();

	QString pkgRootDirDefault = QString::fromLatin1( ::getenv( "PKG_DBDIR" ) );
	if ( pkgRootDirDefault.isEmpty() )
		pkgRootDirDefault = getMkVariable( QString::fromLatin1( "PKG_DBDIR" ) );
	if ( pkgRootDirDefault.isEmpty() )
		pkgRootDirDefault = QString::fromLatin1( "/var/db/pkg" );
	m_pkgRootDir = cfg->readEntry( QString::fromLatin1( "Package directory" ), pkgRootDirDefault).stripWhiteSpace();
}

bool PortMgr::load()
{
	checkDatabase();
	return loadPortDb() && loadPkgDb();
}

bool PortMgr::loadPortDb()
{
	QString portDbFile = m_portRootDir + QString::fromLatin1( "/INDEX.db" );
	if ( !QFile::exists( portDbFile ) ) {
		KNotifyClient::event( KNotifyClient::fatalError,
		             i18n( "<qt>The directory %1 seems to be invalid, "
		             "failed to locate the port database.<br/>"
		             "<br/>"
		             "Please make sure that you installed the 'ports' "
		             "distribution and that the directory %1 was mounted "
		             "properly, if necessary.<br/>"
		             "<br/>"
		             "Also, make sure that the file %1/INDEX.db exists; if "
		             "it doesn't, run <tt>portsdb -u</tt> as root to generate "
		             "it.</qt>")
		             .arg( m_portRootDir ).arg( m_portRootDir ).arg( m_portRootDir ) );
		return false;
	}

	delete m_portDb;
	m_portDb = new DatabaseWrapper( portDbFile );

	if ( !m_portDb->open() ) {
		KNotifyClient::event( KNotifyClient::fatalError,
			i18n( "<qt>Failed to load the port database %1!<br/>"
		          "<br/>"
			      "Did you specify an invalid port directory by any "
			      "chance?</qt" ).arg( portDbFile ) );
		return false;
	}

	return true;
}

bool PortMgr::loadPkgDb()
{
	QString pkgDbFile = m_pkgRootDir + QString::fromLatin1( "/pkgdb.db" );
	if ( !QFile::exists( pkgDbFile ) ) {
		KNotifyClient::event( KNotifyClient::fatalError,
			i18n( "<qt>The directory %1 seems to be invalid, "
			      "failed to locate the package database.<br/>"
			      "<br/>"
			      "Please make sure that the directory %1 was mounted "
			      "properly, if necessary.</qt>"
			      "<br/>"
			      "Also, make sure that the file %1/pkgdb.db exists; if "
			      "it doesn't, run <tt>pkgdb -u</tt> as root to generate "
			      "it.</qt>")
			      .arg( m_pkgRootDir ).arg( m_pkgRootDir ).arg( m_pkgRootDir ) );
		return false;
	}

	delete m_pkgDb;
	m_pkgDb = new DatabaseWrapper( pkgDbFile );

	if ( !m_pkgDb->open() ) {
		KNotifyClient::event( KNotifyClient::fatalError,
			i18n( "<qt>Failed to load the package database %1!<br/>"
		          "<br/>"
			      "Did you specify an invalid package directory by any "
			      "chance?</qt" ).arg( pkgDbFile ) );
		return false;
	}

	return true;
}

QStringList PortMgr::prefixes() const
{
	QStringList keys;
	keys << QString::fromLatin1( "LOCALBASE" );
	keys << QString::fromLatin1( "X11BASE" );
	keys << QString::fromLatin1( "LINUXBASE" );
	return getMkVariables( keys );
}

QStringList PortMgr::categories() const
{
	if ( !m_portDb )
		return QStringList();

	QString cat = m_portDb->getEntry( QString::fromLatin1( ":categories" ) );
	cat += QString::fromLatin1( " " );
	cat += m_portDb->getEntry( QString::fromLatin1( ":virtual_categories" ) );

	return QStringList::split( ' ', cat );
}

PortList PortMgr::ports( const QString &category ) const
{
	if ( !m_portDb )
		return PortList();

	PortList ports;

	const QString origins = m_portDb->getEntry( QString::fromLatin1( "?" ) + category );
	const QStringList tokens = QStringList::split( ' ', origins );
	QStringList::ConstIterator it = tokens.begin();
	QStringList::ConstIterator end = tokens.end();	
	for ( ; it != end; ++it )
		ports += Port( *it );

	// Sort out duplicate entries; there is a bug in the way the INDEX file
	// gets built :-(
	return qUnique( ports );
}

PortList PortMgr::allPorts() const
{
	if ( m_allPorts.isEmpty() )
		determineAllPorts();
	return m_allPorts;
}

void PortMgr::determineAllPorts() const
{
	if ( !m_portDb )
		return;

	const QString origins = m_portDb->getEntry( QString::fromLatin1( ":origins" ) );
	const QStringList tokens = QStringList::split( ' ', origins );
	QStringList::ConstIterator it = tokens.begin();
	QStringList::ConstIterator end = tokens.end();
	for ( ; it != end; ++it )
		m_allPorts += Port( *it );
}

PortList PortMgr::installedPorts() const
{
	if ( m_installedPorts.isEmpty() )
		determineInstalledPorts();
	return m_installedPorts;
}

void PortMgr::determineInstalledPorts() const
{
	if ( !m_pkgDb )
		return;

	const QString origins = m_pkgDb->getEntry( QString::fromLatin1( ":origins" ) );
	const QStringList entries = QStringList::split( ' ', origins );
	QStringList::ConstIterator it = entries.begin();
	QStringList::ConstIterator end = entries.end();
	for ( ; it != end; ++it ) {
		const QString pkgName = m_pkgDb->getEntry( QString::fromLatin1( "?" ) + *it );
		m_installedPorts += Port::fromPkgName( *it, pkgName );
	}
}

QDateTime PortMgr::portDatabaseTimestamp() const
{
	const QFileInfo fi( m_portRootDir + QString::fromLatin1( "/INDEX.db" ) );
	return fi.lastModified();
}

QDateTime PortMgr::packageDatabaseTimestamp() const
{
	const QFileInfo fi( m_pkgRootDir + QString::fromLatin1( "/pkgdb.db" ) );
	return fi.lastModified();
}

QString PortMgr::getMkVariable( const QString &key ) const
{
	const QString cmd = QString::fromLatin1( "make -f%1/Mk/bsd.port.mk -V%2" )
	                    .arg( m_portRootDir ).arg( key );
	return Process::execute( cmd, Process::Stdout );
}

QStringList PortMgr::getMkVariables( const QStringList &keys ) const
{
	QString cmd = QString::fromLatin1( "make -f%1/Mk/bsd.port.mk" )
	              .arg( m_portRootDir );

	QStringList::ConstIterator it = keys.begin();
	QStringList::ConstIterator end = keys.end();
	for ( ; it != end; ++it )
		cmd += QString::fromLatin1( " -V%1" ).arg( *it );

	const QString output = Process::execute( cmd, Process::Stdout );
	return QStringList::split( '\n', output );
}

void PortMgr::checkDatabase() const
{
	KConfig *cfg = kapp->config();
	cfg->setGroup( QString::fromLatin1( "General" ) );

	const QString supfileName = cfg->readEntry( QString::fromLatin1( "CVSup supfile" ) );
	QFile supfile( supfileName );
	if ( !supfile.open( IO_ReadOnly ) ) {
		kdWarning() << "Couldn't open supfile " << supfileName << endl;
		return;
	}

	const QRegExp rx = QString::fromLatin1( "^\\*default\\s+base=" );
	QString line;
	QTextStream stream( &supfile );
	while ( !stream.atEnd() && line.find( rx ) == -1 )
		line = stream.readLine();

	if ( line.find( rx ) == -1 )
		line = QString::fromLatin1( "*default base=/usr/local/etc/cvsup" );

	line = line.mid( line.find( '=' ) + 1 ).stripWhiteSpace();

	const QString checkoutsFileName = line + QString::fromLatin1( "/sup/ports-all/checkouts.cvs:." );
	if ( !QFile::exists( checkoutsFileName ) ) {
		kdWarning() << "Could not check checkouts file " << checkoutsFileName << endl;
		return;
	}

	const bool portDbOutdated = PortMgr::self().portDatabaseTimestamp() < QFileInfo( checkoutsFileName ).lastModified();
	if ( portDbOutdated ) {
		const QString errorMsg =
			i18n( "Your port database is not up to date, this means that "
			      "Barry cannot function properly. It is strongly advised "
			      "that you quit Barry and update your port database." );
		const QString detailedErrorMsg =
			i18n( "<qt>Barry uses the database file %1 for a lot of features "
			      "it provides and detected that this file is not up to date, "
			      "which means that you updated your ports tree in %2 "
			      "without running <tt>portsdb -u</tt> afterwards.<br/>"
			      "<br />"
			      "You can resolve this by exiting Barry and running (as "
			      "root) the command <tt>portsdb -u</tt> now. After that, "
			      "re-start Barry.<br />"
			      "<br />"
			      "If you do not update your database, the data shown by "
			      "Barry (such as the list of available ports, the version "
			      "numbers, and much more) does not reflect the actual "
			      "situation on your system!" )
			      .arg( m_portRootDir + QString::fromLatin1( "/INDEX.db" ) )
			      .arg( m_portRootDir );
		KMessageBox::detailedError( 0, errorMsg, detailedErrorMsg,
		                            i18n( "Database outdated" ) );
	}
}

#include "portmgr.moc"
// vim:ts=4:sw=4:noet:list
