/*
 * Copyright (C) 2003 Fredrik H�lund <fredrik@kde.org>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public
 * License version 2 as published by the Free Software Foundation.
 *
 * 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; see the file COPYING.  If not, write to
 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 */

#ifdef HAVE_CONFIG_H
#  include <config.h>
#endif

#include <klocale.h>
#include <kaboutdata.h>
#include <kstandarddirs.h>
#include <klistview.h>
#include <ksimpleconfig.h>
#include <kglobalsettings.h>
#include <kdialog.h>
#include <kmessagebox.h>
#include <kurlrequesterdlg.h>
#include <kio/job.h>
#include <kio/netaccess.h>
#include <ktar.h>

#include <qlayout.h>
#include <qdir.h>
#include <qpixmap.h>
#include <qimage.h>
#include <qlabel.h>
#include <qhbox.h>
#include <qpainter.h>
#include <qfileinfo.h>
#include <qpushbutton.h>

#include <cstdlib> // for getenv()

#include "themepage.h"
#include "themepage.moc"

#include "previewwidget.h"

#include <X11/Xlib.h>
#include <X11/Xcursor/Xcursor.h>

// Check for older version
#if !defined(XCURSOR_LIB_MAJOR) && defined(XCURSOR_MAJOR)
# define XCURSOR_LIB_MAJOR	XCURSOR_MAJOR
# define XCURSOR_LIB_MINOR	XCURSOR_MINOR
#endif

namespace {
	// Listview icon size
	const int iconSize = 24;

	// Listview columns
	enum Columns { NameColumn = 0, DescColumn, /* hidden */ DirColumn };
}

struct ThemeInfo {
	QString path;           // Path to the cursor theme
	bool writable;          // Theme directory is writable
};


ThemePage::ThemePage( QWidget* parent, const char* name )
	: QWidget( parent, name ), selectedTheme( NULL ), currentTheme( NULL )
{
	QBoxLayout *layout = new QVBoxLayout( this );
	layout->setAutoAdd( true );
	layout->setMargin( KDialog::marginHint() );
	layout->setSpacing( KDialog::spacingHint() );

	new QLabel( i18n("Select the cursor theme you want to use (hover preview to test cursor):"), this );

	// Create the preview widget
	preview = new PreviewWidget( new QHBox( this ) );

	// Create the theme list view
	listview = new KListView( this );
	listview->setFullWidth( true );
	listview->setAllColumnsShowFocus( true );
	listview->addColumn( i18n("Name") );
	listview->addColumn( i18n("Description") );

	connect( listview, SIGNAL(selectionChanged(QListViewItem*)),
			SLOT(selectionChanged(QListViewItem*)) );

	themeDirs = getThemeBaseDirs();
	insertThemes();

	QHBox *hbox = new QHBox( this );
	hbox->setSpacing( KDialog::spacingHint() );
	installButton = new QPushButton( i18n("Install New Theme..."), hbox );
	removeButton = new QPushButton( i18n("Remove Theme"), hbox );

	connect( installButton, SIGNAL( clicked() ), SLOT( installClicked() ) );
	connect( removeButton, SIGNAL( clicked() ), SLOT( removeClicked() ) );

	// Disable the install button if ~/.icons isn't writable
	QString path = QDir::homeDirPath() + "/.icons";
	QFileInfo icons = QFileInfo( path );

	if ( ( icons.exists() && !icons.isWritable() ) ||
		( !icons.exists() && !QFileInfo( QDir::homeDirPath() ).isWritable() ) )
		installButton->setEnabled( false );

	if ( !themeDirs.contains( path ) )
		installButton->setEnabled( false );

	selectionChanged( listview->currentItem() );
}


ThemePage::~ThemePage()
{
}


void ThemePage::save()
{
	if ( currentTheme == selectedTheme )
		return;

	KConfig c( "kcminputrc" );
	c.setGroup( "Mouse" );
	c.writeEntry( "cursorTheme", selectedTheme != "system" ? selectedTheme : QString::null );

	KMessageBox::information( this, i18n("You have to restart KDE for these "
				"changes to take effect."), i18n("Cursor Settings Changed"),
				"CursorSettingsChanged" );

	currentTheme = selectedTheme;
}

void ThemePage::load()
{
	load( false );
}

void ThemePage::load( bool useDefaults )
{
	// Get the name of the theme libXcursor currently uses
	const char *theme = XcursorGetTheme( x11Display() );
	currentTheme = theme;

	// Get the name of the theme KDE is configured to use
	KConfig c( "kcminputrc" );
	c.setReadDefaults( useDefaults );
	c.setGroup( "Mouse" );
	currentTheme = c.readEntry( "cursorTheme", currentTheme );
        if( currentTheme.isEmpty())
            currentTheme = "system";

	// Find the theme in the listview and select it
	QListViewItem *item = listview->findItem( currentTheme, DirColumn );
        if( !item )
                item = listview->findItem( "system", DirColumn );
	selectedTheme = item->text( DirColumn );
	listview->setSelected( item, true );
	listview->ensureItemVisible( item );

	// Update the preview widget as well
	if ( preview )
		preview->setTheme( selectedTheme );

	// Disable the listview if we're in kiosk mode
	if ( c.entryIsImmutable( "cursorTheme" ) )
		listview->setEnabled( false );
}


void ThemePage::defaults()
{
	load( true );
}


void ThemePage::selectionChanged( QListViewItem *item )
{
	if ( !item )
	{
		removeButton->setEnabled( false );
		return;
	}

	selectedTheme = item->text( DirColumn );

	// Update the preview widget
	if ( preview )
		preview->setTheme( selectedTheme );

	removeButton->setEnabled( themeInfo[ selectedTheme ] && themeInfo[ selectedTheme ]->writable );

	emit changed( currentTheme != selectedTheme );
}


void ThemePage::installClicked()
{
	// Get the URL for the theme we're going to install
	KURL url = KURLRequesterDlg::getURL( QString::null, this, i18n( "Drag or Type Theme URL" ) );
	if ( url.isEmpty() )
		return;

	QString tmpFile;
	if ( !KIO::NetAccess::download( url, tmpFile, this ) ) {
		QString text;

		if ( url.isLocalFile() )
			text = i18n( "Unable to find the cursor theme archive %1." );
		else
			text = i18n( "Unable to download the cursor theme archive; "
			             "please check that the address %1 is correct." );

		KMessageBox::sorry( this, text.arg( url.prettyURL() ) );
		return;
	}

	if ( !installThemes( tmpFile ) )
		KMessageBox::error( this, i18n( "The file %1 does not appear to be a valid "
				"cursor theme archive.").arg( url.fileName() ) );

	KIO::NetAccess::removeTempFile( tmpFile );
}


void ThemePage::removeClicked()
{
	QString question = i18n( "<qt>Are you sure you want to remove the "
		"<strong>%1</strong> cursor theme?<br>"
		"This will delete all the files installed by this theme.</qt>")
		.arg( listview->currentItem()->text( NameColumn ) );

	// Get confirmation from the user
	int answer = KMessageBox::warningContinueCancel( this, question, i18n( "Confirmation" ), KStdGuiItem::del() );
	if ( answer != KMessageBox::Continue )
		return;

	// Delete the theme from the harddrive
        KURL u;
        u.setPath( themeInfo[  selectedTheme ]->path );
	KIO::del( u );

	// Remove the theme from the listview and from the themeinfo dict
	delete listview->findItem( selectedTheme, DirColumn );
	themeInfo.remove( selectedTheme );
	listview->setSelected( listview->currentItem(), true );

	// TODO:
	//  Since it's possible to substitute cursors in a system theme by adding a local
	//  theme with the same name, we shouldn't remove the theme from the list if it's
	//  still available elsewhere. This could be solved by calling insertThemes() here,
	//  but since KIO::del() is an asynchronos operation, the theme we're deleting will
	//  be readded to the list again before KIO has removed it.
}


bool ThemePage::installThemes( const QString &file )
{
	KTar archive( file );

	if ( !archive.open( IO_ReadOnly ) )
		return false;

	const KArchiveDirectory *archiveDir = archive.directory();
	QStringList themeDirs;

	const QStringList entries = archiveDir->entries();
	for ( QStringList::ConstIterator it = entries.begin(); it != entries.end(); ++it )
	{
		const KArchiveEntry *entry = archiveDir->entry( *it );
		if ( entry->isDirectory() && entry->name().lower() != "default" ) {
			const KArchiveDirectory *dir = static_cast< const KArchiveDirectory* >( entry );
			if ( dir->entry( "index.theme" ) && dir->entry( "cursors" ) )
				themeDirs << dir->name();
		}
	}

	if ( themeDirs.count() < 1 )
		return false;

	const QString destDir = QDir::homeDirPath() + "/.icons/";
	KStandardDirs::makeDir( destDir ); // Make sure the directory exists

	for ( QStringList::ConstIterator it = themeDirs.begin(); it != themeDirs.end(); ++it )
	{
		// Check if a theme with that name already exists
		if ( QDir( destDir ).exists( *it ) ) {
			const QString question = i18n( "A theme named %1 already exists in your icon "
					"theme folder. Do you want replace it with this one?" ).arg( *it );
			int answer = KMessageBox::warningContinueCancel( this, question, i18n( "Overwrite Theme?"), i18n("Replace") );
			if ( answer != KMessageBox::Continue )
				continue;

			// ### If the theme that's being replaced is the current theme, it
			//     will cause cursor inconsistencies in newly started apps.
		}

		// ### Should we check if a theme with the same name exists in a global theme dir?
		//     If that's the case it will effectively replace it, even though the global theme
		//     won't be deleted. Checking for this situation is easy, since the global theme
		//     will be in the listview. Maybe this should never be allowed since it might
		//     result in strange side effects (from the average users point of view). OTOH
		//     a user might want to do this 'upgrade' a global theme.

		const QString dest = destDir + *it;
		const KArchiveDirectory *dir = static_cast< const KArchiveDirectory* >( archiveDir->entry( *it ) );
		dir->copyTo( dest );
		insertTheme( dest );
	}

	listview->sort();

	archive.close();
	return true;
}


void ThemePage::insertTheme( const QString &path )
{
	QString dirName = QDir( path ).dirName();

	// Defaults in case there's no name or comment field.
	QString name   = dirName;
	QString desc   = i18n( "No description available" );
	QString sample = "left_ptr";

	KSimpleConfig c( path + "/index.theme", true ); // Open read-only
	c.setGroup( "Icon Theme" );

	// Don't insert the theme if it's hidden.
	if ( c.readBoolEntry( "Hidden", false ) )
		return;

	// ### If the theme is hidden, the user will probably find it strange that it
	//     doesn't appear in the list view. There also won't be a way for the user
	//     to delete the theme using the KCM. Perhaps a warning about this should
	//     be issued, and the user given a chance to undo the installation.

	// Read the name, description and sample cursor
	name   = c.readEntry( "Name", name );
	desc   = c.readEntry( "Comment", desc );
	sample = c.readEntry( "Example", sample );

	// Create a ThemeInfo object if one doesn't already exist, and fill in the members
	ThemeInfo *info = themeInfo[ dirName ];
	if ( !info ) {
		info = new ThemeInfo;
		themeInfo.insert( dirName, info );
	}

	info->path     = path;
	info->writable = true;

	// If an item with the same name already exists, remove it
	delete listview->findItem( dirName, DirColumn );

	// Create the listview item and insert it into the list.
	KListViewItem *item = new KListViewItem( listview, name, desc, /*hidden*/ dirName );
	item->setPixmap( NameColumn, createIcon( dirName, sample ) );
	listview->insertItem( item );
}


const QStringList ThemePage::getThemeBaseDirs() const
{
#if XCURSOR_LIB_MAJOR == 1 && XCURSOR_LIB_MINOR < 1
	// These are the default paths Xcursor will scan for cursor themes
	QString path( "~/.icons:/usr/share/icons:/usr/share/pixmaps:/usr/X11R6/lib/X11/icons" );

	// If XCURSOR_PATH is set, use that instead of the default path
	char *xcursorPath = std::getenv( "XCURSOR_PATH" );
	if ( xcursorPath )
		path = xcursorPath;
#else
	// Get the search patch from Xcursor
	QString path = XcursorLibraryPath();
#endif
	// Expand all occurences of ~ to the home dir
	path.replace( "~/", QDir::homeDirPath() + '/' );
	return QStringList::split( ':', path );
}


bool ThemePage::isCursorTheme( const QString &theme, const int depth ) const
{
	// Prevent infinate recursion
	if ( depth > 10 )
		return false;

	// Search each icon theme directory for 'theme'
	for ( QStringList::ConstIterator it = themeDirs.begin(); it != themeDirs.end(); ++it )
	{
		QDir dir( *it  );
		if ( !dir.exists() )
			continue;

		const QStringList subdirs( dir.entryList( QDir::Dirs ) );
		if ( subdirs.contains( theme ) )
		{
			const QString path       = *it + '/' + theme;
			const QString indexfile  = path + "/index.theme";
			const bool haveIndexFile = dir.exists( indexfile );
			const bool haveCursors   = dir.exists( path + "/cursors" );
			QStringList inherit;

			// Return true if we have a cursors subdirectory
			if ( haveCursors )
				return true;

			// Parse the index.theme file if one exists
			if ( haveIndexFile )
			{
				KSimpleConfig c( indexfile, true ); // Open read-only
				c.setGroup( "Icon Theme" );
				inherit = c.readListEntry( "Inherits" );
			}

			// Recurse through the list of inherited themes
			for ( QStringList::ConstIterator it2 = inherit.begin(); it2 != inherit.end(); ++it2 )
			{
				if ( *it2 == theme ) // Avoid possible DoS
					continue;

				if ( isCursorTheme( *it2, depth + 1 ) )
					return true;
			}
		}
	}

	return false;
}


void ThemePage::insertThemes()
{
	// Scan each base dir for cursor themes and add them to the listview.
	// An icon theme is considered to be a cursor theme if it contains
	// a cursors subdirectory or if it inherits a cursor theme.
	for ( QStringList::ConstIterator it = themeDirs.begin(); it != themeDirs.end(); ++it )
	{
		QDir dir( *it );
		if ( !dir.exists() )
			continue;

		QStringList subdirs( dir.entryList( QDir::Dirs ) );
		subdirs.remove( "." );
		subdirs.remove( ".." );

		for ( QStringList::ConstIterator it = subdirs.begin(); it != subdirs.end(); ++it )
		{
			// Only add the theme if we don't already have a theme with that name
			// in the list. Xcursor will use the first theme it finds in that
			// case, and since we use the same search order that should also be
			// the theme we end up adding to the list.
			if ( listview->findItem( *it, DirColumn ) )
				continue;

			const QString path       = dir.path() + '/' + *it;
			const QString indexfile  = path + "/index.theme";
			const bool haveIndexFile = dir.exists( *it + "/index.theme" );
			const bool haveCursors   = dir.exists( *it + "/cursors" );

			// If the directory doesn't have a cursors subdir and lack an
			// index.theme file it's definately not a cursor theme
			if ( !haveIndexFile && !haveCursors )
				continue;

			// Defaults in case there's no index.theme file or it lacks
			// a name and a comment field.
			QString name   = *it;
			QString desc   = i18n( "No description available" );
			QString sample = "left_ptr";

			// Parse the index.theme file if the theme has one.
			if ( haveIndexFile )
			{
				KSimpleConfig c( indexfile, true );
				c.setGroup( "Icon Theme" );

				// Skip this theme if it's hidden.
				if ( c.readBoolEntry( "Hidden", false ) )
					continue;

				// If there's no cursors subdirectory we'll do a recursive scan
				// to check if the theme inherits a theme with one.
				if ( !haveCursors )
				{
					bool result = false;
					QStringList inherit = c.readListEntry( "Inherits" );
					for ( QStringList::ConstIterator it2 = inherit.begin(); it2 != inherit.end(); ++it2 )
						if ( result = isCursorTheme( *it2 ) )
							break;

					// If this theme doesn't inherit a cursor theme, proceed
					// to the next theme in the list.
					if ( !result )
						continue;
				}

				// Read the name, description and sample cursor
				name   = c.readEntry( "Name", name );
				desc   = c.readEntry( "Comment", desc );
				sample = c.readEntry( "Example", sample );
			}

			// Create a ThemeInfo object, and fill in the members
			ThemeInfo *info = new ThemeInfo;
			info->path     = path;
			info->writable = QFileInfo( path ).isWritable();
			themeInfo.insert( *it, info );

			// Create the listview item and insert it into the list.
			KListViewItem *item = new KListViewItem( listview, name, desc, /*hidden*/ *it );
			item->setPixmap( NameColumn, createIcon( *it, sample ) );
			listview->insertItem( item );
		}
	}

	// Note: If a default theme dir wasn't found in the above search, Xcursor will
	//       default to using the cursor font.

	// Sort the theme list
	listview->sort();

	KListViewItem *item = new KListViewItem( listview, ' ' + i18n( "No theme" ), i18n( "The old classic X cursors") , /*hidden*/ "none" );
	listview->insertItem( item );
	item = new KListViewItem( listview, ' ' + i18n( "System theme" ), i18n( "Do not change cursor theme") , /*hidden*/ "system" );
	listview->insertItem( item );
        // no ThemeInfo object for this one
}


QPixmap ThemePage::createIcon( const QString &theme, const QString &sample ) const
{
	XcursorImage *xcur;
	QPixmap pix;

	xcur = XcursorLibraryLoadImage( sample.latin1(), theme.latin1(), iconSize );
	if ( !xcur ) xcur = XcursorLibraryLoadImage( "left_ptr", theme.latin1(), iconSize );

	if ( xcur ) {
		// Calculate an auto-crop rectangle for the cursor image
		// (helps with cursors converted from windows .cur or .ani files)
		QRect r( QPoint( xcur->width, xcur->height ), QPoint() );
		XcursorPixel *src = xcur->pixels;

		for ( int y = 0; y < int( xcur->height ); y++ ) {
			for ( int x = 0; x < int( xcur->width ); x++ ) {
				if ( *(src++) >> 24 ) {
					if ( x < r.left() )   r.setLeft( x );
					if ( x > r.right() )  r.setRight( x );
					if ( y < r.top() )    r.setTop( y );
					if ( y > r.bottom() ) r.setBottom( y );
				}
			}
		}

		// Normalize the rectangle
		r = r.normalize();

		// Calculate the image size
		int size = kMax( iconSize, kMax( r.width(), r.height() ) );

		// Create the intermediate QImage
		QImage image( size, size, 32 );
		image.setAlphaBuffer( true );

		// Clear the image
		Q_UINT32 *dst = reinterpret_cast<Q_UINT32*>( image.bits() );
		for ( int i = 0; i < image.width() * image.height(); i++ )
			dst[i] = 0;

		// Compute the source and destination offsets
		QPoint dstOffset( (image.width() - r.width()) / 2, (image.height() - r.height()) / 2 );
		QPoint srcOffset( r.topLeft() );

		dst = reinterpret_cast<Q_UINT32*>( image.scanLine(dstOffset.y()) ) + dstOffset.x();
		src = reinterpret_cast<Q_UINT32*>( xcur->pixels ) + srcOffset.y() * xcur->width + srcOffset.x();

		// Copy the XcursorImage into the QImage, converting it from premultiplied
		// to non-premultiplied alpha and cropping it if needed.
		for ( int y = 0; y < r.height(); y++ )
		{
			for ( int x = 0; x < r.width(); x++, dst++, src++ ) {
				const Q_UINT32 pixel = *src;

				const Q_UINT8 a = qAlpha( pixel );
				const Q_UINT8 r = qRed( pixel );
				const Q_UINT8 g = qGreen( pixel );
				const Q_UINT8 b = qBlue( pixel );

				if ( !a || a == 255 ) {
					*dst = pixel;
				} else {
					float alpha = a / 255.0;
					*dst = qRgba( int(r / alpha), int(g / alpha), int(b / alpha), a );
				}
			}
			dst += ( image.width() - r.width() );
			src += ( xcur->width - r.width() );
		}

		// Scale down the image if we need to
		if ( image.width() > iconSize || image.height() > iconSize )
			image = image.smoothScale( iconSize, iconSize, QImage::ScaleMin );

		pix.convertFromImage( image );
		XcursorImageDestroy( xcur );
	} else {

		QImage image( iconSize, iconSize, 32 );
		image.setAlphaBuffer( true );

		Q_UINT32 *data = reinterpret_cast< Q_UINT32* >( image.bits() );
		for ( int i = 0; i < image.width() * image.height(); i++ )
			data[ i ] = 0;

		pix.convertFromImage( image );
	}

	return pix;
}


// vim: set noet ts=4 sw=4:
