/*
 * orphanedfilesdialog.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 "orphanedfilesdialog.h"
#include "portmgr.h"

#include <kapplication.h>
#include <kfiledetailview.h>
#include <kiconloader.h>
#include <klocale.h>
#include <kmessagebox.h>
#include <kopenwith.h>
#include <kpropertiesdialog.h>
#include <ksqueezedtextlabel.h>

#include <qheader.h>
#include <qpopupmenu.h>
#include <qthread.h>
#include <qvbox.h>

#include <sys/types.h>
#include <dirent.h>
#include <math.h>
#include <unistd.h>

namespace
{
	bool isPrime( int number )
	{
		if ( number <= 1 )
			return false;

		int divisor = int( sqrt( number ) );
		while ( number % divisor != 0 )
			--divisor;

		return divisor == 1;
	}

	int nextPrime( int number )
	{
		int i = number;
		while ( !isPrime( i ) )
			++i;
		return i;
	}
};

using namespace Barry;

class DirChangeEvent : public QCustomEvent
{
	public:
		static const int Id = 10001;

		DirChangeEvent( const QString &dir ) : QCustomEvent( Id ), m_dir( dir ) { }

		QString dir() const { return m_dir; }

	private:
		QString m_dir;
};

class FoundFileEvent : public QCustomEvent
{
	public:
		static const int Id = 10002;

		FoundFileEvent( const QString &name ) : QCustomEvent( Id ), m_name( name ) { }

		QString name() const { return m_name; }

	private:
		QString m_name;
};

class TraverserDoneEvent : public QCustomEvent
{
	public:
		static const int Id = 10003;

		TraverserDoneEvent() : QCustomEvent( Id ) { }
};

class TraverserListEvent : public QCustomEvent
{
	public:
		static const int Id = 10004;

		TraverserListEvent( const QPtrList<QCustomEvent> &eventList )
			: QCustomEvent( Id ), m_eventList( eventList ) { }

		const QPtrList<QCustomEvent> &eventList() const { return m_eventList; }

	private:
		QPtrList<QCustomEvent> m_eventList;
};

class DirTraverserThread : public QThread
{
	public:
		DirTraverserThread( QWidget *parent, const QStringList &startDirs,
		                    const QDict<bool> &ignoredFiles );

		void abort() { m_abort = true; }

	protected:
		virtual void run();

	private:
		void scanDir( const QString &path );
	
	QWidget *m_parent;
	QStringList m_startDirs;
	QDict<bool> m_ignoredFiles;
	bool m_abort;
	QPtrList<QCustomEvent> m_eventList;
	QTime m_timeSnapshot;
};

DirTraverserThread::DirTraverserThread( QWidget *parent,
                                        const QStringList &startDirs,
                                        const QDict<bool> &ignoredFiles )
	: m_parent( parent ),
	m_startDirs( startDirs ),
	m_ignoredFiles( ignoredFiles ),
	m_abort( false )
{
}

void DirTraverserThread::run()
{
	m_timeSnapshot = QTime::currentTime();
	QStringList::ConstIterator it = m_startDirs.begin();
	QStringList::ConstIterator end = m_startDirs.end();
	for ( ; it != end; ++it )
		scanDir( *it );
	kapp->postEvent( m_parent, new TraverserDoneEvent );
}

void DirTraverserThread::scanDir( const QString &path )
{
	if ( m_abort )
		return;

	if ( m_ignoredFiles.find( path ) != 0 )
		return;

	DIR *dirp = opendir( path.latin1() );
	if ( !dirp )
		return;

	m_eventList.append( new DirChangeEvent( path ) );

	struct dirent *dp;
	while ( ( dp = readdir( dirp ) ) != 0 ) {
		if ( dp->d_type == DT_DIR && strcmp( dp->d_name, "." ) != 0 && strcmp( dp->d_name, ".." ) != 0 ) {
			const QString absPath = path + QString::fromLatin1( "/" ) + QString::fromLatin1( dp->d_name, dp->d_namlen );
			if ( access( absPath.latin1(), R_OK | X_OK ) == 0 )
				scanDir( absPath );
		} else if ( dp->d_type == DT_REG ) {
			QDir d( path );
			const QString canonPath = d.canonicalPath();
			const QString absName = canonPath + QString::fromLatin1( "/" ) + QString::fromLatin1( dp->d_name, dp->d_namlen );
			if ( m_ignoredFiles.find( absName ) == 0 )
				m_eventList.append( new FoundFileEvent( absName ) );
		}
		if ( m_timeSnapshot.msecsTo( QTime::currentTime() ) > 100 ) {
			kapp->postEvent( m_parent, new TraverserListEvent( m_eventList ) );
			m_eventList.clear();
			m_timeSnapshot = QTime::currentTime();
		}
		if ( m_abort )
			break;
	}
	closedir( dirp );
}

OrphanedFilesDialog::OrphanedFilesDialog( QWidget *parent, const char *name )
	: KDialogBase( parent, name, true, i18n( "Searching for orphaned files..." ),
	               Cancel, Cancel, true )
{
	resize( 500, 300 );

	QVBox *mainWidget = makeVBoxMainWidget();

	m_statusText = new KSqueezedTextLabel(
	                     i18n( "Scanning: %1" ).arg( QString::null ),
	                     mainWidget, "m_statusText"
	                   );

	m_foundFiles = new KFileDetailView( mainWidget, "m_foundFiles" );
	m_foundFiles->setSorting( QDir::Unsorted );
	m_foundFiles->hide();

	// Disable clicking to work around a crash in KFileDetailView
	m_foundFiles->header()->setClickEnabled( false );
	connect( m_foundFiles, SIGNAL( contextMenu( KListView *, QListViewItem *, const QPoint & ) ),
	         this, SLOT( showContextMenu( KListView *, QListViewItem *, const QPoint & ) ) );
}

void OrphanedFilesDialog::show()
{
	KDialogBase::show();
	search();
}

QStringList OrphanedFilesDialog::determineRegisteredFiles()
{
	QStringList registeredFiles;

	/* We know that all the pkgdb can map a filename to a package, so we
	 * get all keys and strip those which don't start with a slash, that
	 * should be the list of registered files.
	 */
	registeredFiles = PortMgr::self().pkgDb().getAllKeys();

	/* Since ~95% of the keys in the pkgdb are filenames, and they come first,
	 * we start searching from the end and just strip all non-filename
	 * keys.
	 */
	QStringList::Iterator it = registeredFiles.end();
	--it;
	while ( ( *it )[ 0 ] != '/' && it != registeredFiles.begin() )
		--it;
	registeredFiles.erase( ++it, registeredFiles.end() );

	return registeredFiles;
}

QStringList OrphanedFilesDialog::determineIgnoredFiles()
{
	KConfig *cfg = kapp->config();
	cfg->setGroup( QString::fromLatin1( "Orphaned files" ) );
	return cfg->readListEntry( "Ignored files" );
}

void OrphanedFilesDialog::search()
{
	/* Merge the lists of registered and ignored files into a QDict so that
	 * the directory traverser thread wastes less time with lookups.
	 */
	const QStringList allFiles = determineRegisteredFiles() +
	                             determineIgnoredFiles();
	QDict<bool> ignoredDict( nextPrime( allFiles.count() ) );
	ignoredDict.setAutoDelete( true );

	QStringList::ConstIterator it = allFiles.begin();
	QStringList::ConstIterator end = allFiles.end();
	for ( ; it != end; ++it ) {
		QString s = *it;
		if ( s.at( s.length() - 1 ) == '/' )
			s.remove( s.length() - 1, 1 );
		ignoredDict.insert( s, new bool( true ) );
	}

	m_traverserThread = new DirTraverserThread( this,
	                                           PortMgr::self().prefixes(),
	                                           ignoredDict );
	m_traverserThread->start();
}

void OrphanedFilesDialog::slotCancel()
{
	if ( m_traverserThread ) {
		m_statusText->setText( i18n( "Aborting..." ) );
		m_statusText->update();
		m_traverserThread->abort();
		m_traverserThread->wait( 5 );
		delete m_traverserThread;
	}
	reject();
}

void OrphanedFilesDialog::showContextMenu( KListView *, QListViewItem *, const QPoint &p )
{
	QPopupMenu *menu = new QPopupMenu( this );
	menu->insertItem( SmallIcon( QString::fromLatin1( "misc" ) ),
	                  i18n( "Properties..." ), this, SLOT( showProperties() ) );
	menu->insertItem( SmallIcon( QString::fromLatin1( "fileopen" ) ),
	                  i18n( "Open..." ), this, SLOT( openFile() ) );
	menu->insertSeparator();
	menu->insertItem( SmallIcon( QString::fromLatin1( "editdelete" ) ),
	                  i18n( "Delete..." ),
	                  this, SLOT( deleteFile() ) );
	menu->insertSeparator();
	menu->insertItem( i18n( "Add file to ignore list" ),
	                  this, SLOT( addFileToIgnoreList() ) );
	menu->insertItem( i18n( "Add directory to ignore list" ),
	                  this, SLOT( addDirectoryToIgnoreList() ) );

	menu->exec( p );
}

void OrphanedFilesDialog::showProperties()
{
	new KPropertiesDialog( m_foundFiles->currentFileItem()->url() );
}

void OrphanedFilesDialog::openFile()
{
	const KURL fileURL = m_foundFiles->currentFileItem()->url();
	const KMimeType::Ptr mime = KMimeType::findByURL( fileURL.url(), 0, true );

	/* application shouldn't get passed to KRun since that will execute the
	 * respective file. Instead, we open a "Open with" dialog to let the user
	 * decide (most likely, an editor will be chosen). */
	if ( mime->name().startsWith( QString::fromLatin1( "application/" ) ) ) {
		KFileOpenWithHandler *openWithHandler = new KFileOpenWithHandler;
		openWithHandler->displayOpenWithDialog( fileURL );
	} else {
		new KRun( fileURL );
	}
}

void OrphanedFilesDialog::deleteFile()
{
	const QString fileName = m_foundFiles->currentFileItem()->url().prettyURL( -1, KURL::StripFileProtocol );

	if ( KMessageBox::warningYesNo( this, i18n( "Are you sure you want to erase the "
		" file %1?" ).arg( fileName ),
		i18n( "Confirmation" ), KStdGuiItem::yes(), KStdGuiItem::no(),
		QString::fromLatin1( "showDeleteConfirmationBox" ) ) == KMessageBox::No )
		return;

	const QFileInfo fileInfo( fileName );
	if ( !fileInfo.isWritable() ) {
		KMessageBox::error( this, i18n( "Cannot delete file %1, no write "
		"access." ).arg( fileName ), i18n( "Cannot delete file" ) );
		return;
	}

	if ( !QFile::remove( fileName ) )
		KMessageBox::error( this, i18n( "Unknown error occured while trying "
		"to delete file %1!" ).arg( fileName ), i18n( "Couldn't delete file" ) );
		return;
}

void OrphanedFilesDialog::addFileToIgnoreList()
{
	addItemToIgnoreList( m_foundFiles->currentFileItem()->url().prettyURL( -1, KURL::StripFileProtocol ) );
}

void OrphanedFilesDialog::addDirectoryToIgnoreList()
{
	addItemToIgnoreList( m_foundFiles->currentFileItem()->url().directory() );
}

void OrphanedFilesDialog::addItemToIgnoreList( const QString &item )
{
	QStringList ignoredFiles = determineIgnoredFiles();
	ignoredFiles << item;

	KConfig *cfg = kapp->config();
	cfg->setGroup( QString::fromLatin1( "Orphaned files" ) );
	cfg->writeEntry( "Ignored files", ignoredFiles );
	cfg->sync();
}

void OrphanedFilesDialog::customEvent( QCustomEvent *e )
{
	if ( e->type() == DirChangeEvent::Id ) {
		DirChangeEvent *de = static_cast<DirChangeEvent *>( e );
		m_statusText->setText( i18n( "Scanning: %1" ).arg( de->dir() ) );
	} else if ( e->type() == FoundFileEvent::Id ) {
		FoundFileEvent *fe = static_cast<FoundFileEvent *>( e );
		KFileItem *fi = new KFileItem( KFileItem::Unknown, KFileItem::Unknown,
		                    KURL( fe->name() ) );
		KFileListViewItem *item = new KFileListViewItem( m_foundFiles, fi );
		item->setText( 0, fe->name() );
	} else if ( e->type() == TraverserDoneEvent::Id ) {
		m_foundFiles->show();
		m_statusText->setText( i18n( "Search finished." ) );
	} else if ( e->type() == TraverserListEvent::Id ) {
		TraverserListEvent *le = static_cast<TraverserListEvent *>( e );
		QPtrList<QCustomEvent> events = le->eventList();
		for ( QCustomEvent *event = events.first(); event != 0; event = events.next() ) {
			customEvent( event );
			delete event;
		}
	}
}

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