/*
 *   File name: ktreemapview.cpp
 *   Summary:	High level classes for KDirStat
 *   License:	LGPL - See file COPYING.LIB for details.
 *   Author:	Stefan Hundhammer <sh@suse.de>
 *
 *   Updated:	2003-10-20
 */


#include <sys/stat.h>

#include <tqevent.h>
#include <tqregexp.h>

#include <kapp.h>
#include <kconfig.h>
#include <kglobal.h>
#include <klocale.h>

#include "kdirtree.h"
#include "ktreemapview.h"
#include "ktreemaptile.h"


using namespace KDirStat;

#define UpdateMinSize	20



KTreemapView::KTreemapView( KDirTree * tree, TQWidget * parent, const TQSize & initialSize )
    : TQCanvasView( parent )
    , _tree( tree )
    , _rootTile( 0 )
    , _selectedTile( 0 )
    , _selectionRect( 0 )
{
    // kdDebug() << k_funcinfo << endl;

    readConfig();

    // Default values for light sources taken from Wiik / Wetering's paper
    // about "cushion treemaps".

    _lightX		= 0.09759;
    _lightY		= 0.19518;
    _lightZ		= 0.9759;

    if ( _autoResize )
    {
	setHScrollBarMode( AlwaysOff );
	setVScrollBarMode( AlwaysOff );
    }

    if ( initialSize.isValid() )
	resize( initialSize );

    if ( tree && tree->root() )
    {
	if ( ! _rootTile )
	{
	    // The treemap might already be created indirectly by
	    // rebuildTreemap() called from resizeEvent() triggered by resize()
	    // above. If this is so, don't do it again.

	    rebuildTreemap( tree->root() );
	}
    }

    connect( this,	TQT_SIGNAL( selectionChanged( KFileInfo * ) ),
	     tree,	TQT_SLOT  ( selectItem	( KFileInfo * ) ) );

    connect( tree,	TQT_SIGNAL( selectionChanged( KFileInfo * ) ),
	     this,	TQT_SLOT  ( selectTile	( KFileInfo * ) ) );

    connect( tree,	TQT_SIGNAL( deletingChild	( KFileInfo * )	),
	     this,	TQT_SLOT  ( deleteNotify	( KFileInfo * ) ) );

    connect( tree,	TQT_SIGNAL( childDeleted()	 ),
	     this,	TQT_SLOT  ( rebuildTreemap() ) );
}


KTreemapView::~KTreemapView()
{
}


void
KTreemapView::clear()
{
    if ( canvas() )
	deleteAllItems( canvas() );

    _selectedTile	= 0;
    _selectionRect	= 0;
    _rootTile		= 0;
}


void
KTreemapView::deleteAllItems( TQCanvas * canvas )
{
    if ( ! canvas )
	return;

    TQCanvasItemList all = canvas->allItems();

    for ( TQCanvasItemList::Iterator it = all.begin(); it != all.end(); ++it )
	delete *it;
}


void
KTreemapView::readConfig()
{
    KConfig * config = kapp->config();
    config->setGroup( "Treemaps" );

    _ambientLight	= config->readNumEntry( "AmbientLight"		,  DefaultAmbientLight );

    _heightScaleFactor	= config->readDoubleNumEntry( "HeightScaleFactor" , DefaultHeightScaleFactor );
    _autoResize		= config->readBoolEntry( "AutoResize"		, true	);
    _squarify		= config->readBoolEntry( "Squarify"		, true	);
    _doCushionShading	= config->readBoolEntry( "CushionShading"	, true	);
    _ensureContrast	= config->readBoolEntry( "EnsureContrast"	, true	);
    _forceCushionGrid	= config->readBoolEntry( "ForceCushionGrid"	, false	);
    _minTileSize	= config->readNumEntry ( "MinTileSize"		, DefaultMinTileSize );

    _highlightColor	= readColorEntry( config, "HighlightColor"	, red			     );
    _cushionGridColor	= readColorEntry( config, "CushionGridColor"	, TQColor( 0x80, 0x80, 0x80 ) );
    _outlineColor	= readColorEntry( config, "OutlineColor"	, black			     );
    _fileFillColor	= readColorEntry( config, "FileFillColor"	, TQColor( 0xde, 0x8d, 0x53 ) );
    _dirFillColor	= readColorEntry( config, "DirFillColor"	, TQColor( 0x10, 0x7d, 0xb4 ) );

    if ( _autoResize )
    {
	setHScrollBarMode( AlwaysOff );
	setVScrollBarMode( AlwaysOff );
    }
    else
    {
	setHScrollBarMode( TQScrollView::Auto );
	setVScrollBarMode( TQScrollView::Auto );
    }
}


TQColor
KTreemapView::readColorEntry( KConfig * config, const char * entryName, TQColor defaultColor )
{
    return config->readColorEntry( entryName, &defaultColor );
}


KTreemapTile *
KTreemapView::tileAt( TQPoint pos )
{
    KTreemapTile * tile = 0;

    TQCanvasItemList coll = canvas()->collisions( pos );
    TQCanvasItemList::Iterator it = coll.begin();

    while ( it != coll.end() && tile == 0 )
    {
	tile = dynamic_cast<KTreemapTile *> (*it);
	++it;
    }

    return tile;
}


void
KTreemapView::contentsMousePressEvent( TQMouseEvent * event )
{
    // kdDebug() << k_funcinfo << endl;

    KTreemapTile * tile = tileAt( event->pos() );

    switch ( event->button() )
    {
	case Qt::LeftButton:
	    selectTile( tile );
	    emit userActivity( 1 );
	    break;

	case Qt::MidButton:
	    // Select clicked tile's parent, if available

	    if ( _selectedTile &&
		 _selectedTile->rect().contains( event->pos() ) )
	    {
		if ( _selectedTile->parentTile() )
		    tile = _selectedTile->parentTile();
	    }

	    // Intentionally handling the middle button like the left button if
	    // the user clicked outside the (old) selected tile: Simply select
	    // the clicked tile. This makes using this middle mouse button
	    // intuitive: It can be used very much like the left mouse button,
	    // but it has added functionality. Plus, it cycles back to the
	    // clicked tile if the user has already clicked all the way up the
	    // hierarchy (i.e. the topmost directory is highlighted).

	    selectTile( tile );
	    emit userActivity( 1 );
	    break;

	case Qt::RightButton:

	    if ( tile )
	    {
		if ( _selectedTile &&
		     _selectedTile->rect().contains( event->pos() ) )
		{
		    // If a directory (non-leaf tile) is already selected,
		    // don't override this by

		    emit contextMenu( _selectedTile, event->globalPos() );
		}
		else
		{
		    selectTile( tile );
		    emit contextMenu( tile, event->globalPos() );
		}

		emit userActivity( 3 );
	    }
	    break;

	default:
	    // event->button() is an enum, so g++ complains
	    // if there are unhandled cases.
	    break;
    }
}


void
KTreemapView::contentsMouseDoubleClickEvent( TQMouseEvent * event )
{
    // kdDebug() << k_funcinfo << endl;

    KTreemapTile * tile = tileAt( event->pos() );

    switch ( event->button() )
    {
	case Qt::LeftButton:
	    if ( tile )
	    {
		selectTile( tile );
		zoomIn();
		emit userActivity( 5 );
	    }
	    break;

	case Qt::MidButton:
	    zoomOut();
	    emit userActivity( 5 );
	    break;

	case Qt::RightButton:
	    // Double-clicking the right mouse button is pretty useless - the
	    // first click opens the context menu: Single clicks are always
	    // delivered first. Even if that would be caught by using timers,
	    // it would still be very awkward to use: Click too slow, and
	    // you'll get the context menu rather than what you really wanted -
	    // then you'd have to get rid of the context menu first.
	    break;

	default:
	    // Prevent compiler complaints about missing enum values in switch
	    break;
    }
}


void
KTreemapView::zoomIn()
{
    if ( ! _selectedTile || ! _rootTile )
	return;

    KTreemapTile * newRootTile = _selectedTile;

    while ( newRootTile->parentTile() != _rootTile &&
	    newRootTile->parentTile() ) // This should never happen, but who knows?
    {
	newRootTile = newRootTile->parentTile();
    }

    if ( newRootTile )
    {
	KFileInfo * newRoot = newRootTile->orig();

	if ( newRoot->isDir() || newRoot->isDotEntry() )
	    rebuildTreemap( newRoot );
    }
}


void
KTreemapView::zoomOut()
{
    if ( _rootTile )
    {
	KFileInfo * root = _rootTile->orig();

	if ( root->parent() )
	    root = root->parent();

	rebuildTreemap( root );
    }
}


void
KTreemapView::selectParent()
{
    if ( _selectedTile && _selectedTile->parentTile() )
	selectTile( _selectedTile->parentTile() );
}


bool
KTreemapView::canZoomIn() const
{
    if ( ! _selectedTile || ! _rootTile )
	return false;

    if ( _selectedTile == _rootTile )
	return false;

    KTreemapTile * newRootTile = _selectedTile;

    while ( newRootTile->parentTile() != _rootTile &&
	    newRootTile->parentTile() ) // This should never happen, but who knows?
    {
	newRootTile = newRootTile->parentTile();
    }

    if ( newRootTile )
    {
	KFileInfo * newRoot = newRootTile->orig();

	if ( newRoot->isDir() || newRoot->isDotEntry() )
	    return true;
    }

    return false;
}


bool
KTreemapView::canZoomOut() const
{
    if ( ! _rootTile || ! _tree->root() )
	return false;

    return _rootTile->orig() != _tree->root();
}


bool
KTreemapView::canSelectParent() const
{
    return _selectedTile && _selectedTile->parentTile();
}


void
KTreemapView::rebuildTreemap()
{
    KFileInfo * root = 0;

    if ( ! _savedRootUrl.isEmpty() )
    {
	// kdDebug() << "Restoring old treemap with root " << _savedRootUrl << endl;

	root = _tree->locate( _savedRootUrl, true );	// node, findDotEntries
    }

    if ( ! root )
	root = _rootTile ? _rootTile->orig() : _tree->root();

    rebuildTreemap( root, canvas()->size() );
    _savedRootUrl = "";
}


void
KTreemapView::rebuildTreemap( KFileInfo *	newRoot,
			      const TQSize &	newSz )
{
    // kdDebug() << k_funcinfo << endl;

    TQSize newSize = newSz;

    if ( newSz.isEmpty() )
	newSize = visibleSize();


    // Delete all old stuff.
    clear();

    // Re-create a new canvas

    if ( ! canvas() )
    {
	TQCanvas * canv = new TQCanvas( TQT_TQOBJECT(this) );
	CHECK_PTR( canv );
	setCanvas( canv );
    }

    canvas()->resize( newSize.width(), newSize.height() );

    if ( newSize.width() >= UpdateMinSize && newSize.height() >= UpdateMinSize )
    {
	// The treemap contents is displayed if larger than a certain minimum
	// visible size. This is an easy way for the user to avoid
	// time-consuming delays when deleting a lot of files: Simply make the
	// treemap (sub-) window very small.

	// Fill the new canvas

	if ( newRoot )
	{
	    _rootTile = new KTreemapTile( this,		// parentView
					  0,		// parentTile
					  newRoot,	// orig
					  TQRect( TQPoint( 0, 0), newSize ),
					  KTreemapAuto );
	}


	// Synchronize selection with the tree

	if ( _tree->selection() )
	    selectTile( _tree->selection() );
    }
    else
    {
	// kdDebug() << "Too small - suppressing treemap contents" << endl;
    }

    emit treemapChanged();
}


void
KTreemapView::deleteNotify( KFileInfo * )
{
    if ( _rootTile )
    {
	if ( _rootTile->orig() != _tree->root() )
	{
	    // If the user zoomed the treemap in, save the root's URL so the
	    // current state can be restored upon the next rebuildTreemap()
	    // call (which is triggered by the childDeleted() signal that the
	    // tree emits after deleting is done).
	    //
	    // Intentionally using debugUrl() here rather than just url() so
	    // the correct zoom can be restored even when a dot entry is the
	    // current treemap root.

	    _savedRootUrl = _rootTile->orig()->debugUrl();
	}
	else
	{
	    // A shortcut for the most common case: No zoom. Simply use the
	    // tree's root for the next treemap rebuild.

	    _savedRootUrl = "";
	}
    }
    else
    {
	// Intentionally leaving _savedRootUrl alone: Otherwise multiple
	// deleteNotify() calls might cause a previously saved _savedRootUrl to
	// be unnecessarily deleted, thus the treemap couldn't be restored as
	// it was.
    }

    clear();
}


void
KTreemapView::resizeEvent( TQResizeEvent * event )
{
    TQCanvasView::resizeEvent( event );

    if ( _autoResize )
    {
	bool tooSmall =
	    event->size().width()  < UpdateMinSize ||
	    event->size().height() < UpdateMinSize;

	if ( tooSmall && _rootTile )
	{
	    // kdDebug() << "Suppressing treemap contents" << endl;
	    rebuildTreemap( _rootTile->orig() );
	}
	else if ( ! tooSmall && ! _rootTile )
	{
	    if ( _tree->root() )
	    {
		// kdDebug() << "Redisplaying suppressed treemap contents" << endl;
		rebuildTreemap( _tree->root() );
	    }
	}
	else if ( _rootTile )
	{
	    // kdDebug() << "Auto-resizing treemap" << endl;
	    rebuildTreemap( _rootTile->orig() );
	}
    }
}


void
KTreemapView::selectTile( KTreemapTile * tile )
{
    // kdDebug() << k_funcinfo << endl;

    KTreemapTile * oldSelection = _selectedTile;
    _selectedTile = tile;


    // Handle selection (highlight) rectangle

    if ( _selectedTile )
    {
	if ( ! _selectionRect )
	    _selectionRect = new KTreemapSelectionRect( canvas(), _highlightColor );
    }

    if ( _selectionRect )
	_selectionRect->highlight( _selectedTile );

    canvas()->update();

    if ( oldSelection != _selectedTile )
    {
	emit selectionChanged( _selectedTile ? _selectedTile->orig() : 0 );
    }
}


void
KTreemapView::selectTile( KFileInfo * node )
{
    selectTile( findTile( node ) );
}



KTreemapTile *
KTreemapView::findTile( KFileInfo * node )
{
    if ( ! node )
	return 0;

    TQCanvasItemList itemList = canvas()->allItems();
    TQCanvasItemList::Iterator it = itemList.begin();

    while ( it != itemList.end() )
    {
	KTreemapTile * tile = dynamic_cast<KTreemapTile *> (*it);

	if ( tile && tile->orig() == node )
	    return tile;

	++it;
    }

    return 0;
}


TQSize
KTreemapView::visibleSize()
{
    ScrollBarMode oldHMode = hScrollBarMode();
    ScrollBarMode oldVMode = vScrollBarMode();

    setHScrollBarMode( AlwaysOff );
    setVScrollBarMode( AlwaysOff );

    TQSize size = TQSize( TQCanvasView::visibleWidth(),
			TQCanvasView::visibleHeight() );

    setHScrollBarMode( oldHMode );
    setVScrollBarMode( oldVMode );

    return size;
}


TQColor
KTreemapView::tileColor( KFileInfo * file )
{
    if ( file )
    {
	if ( file->isFile() )
	{
	    // Find the filename extension: Everything after the first '.'
	    TQString ext = file->name().section( '.', 1 );

	    while ( ! ext.isEmpty() )
	    {
		TQString lowerExt = ext.lower();

		// Try case sensitive comparisions first

		if ( ext == "~"		)	return TQt::red;
		if ( ext == "bak"	)	return TQt::red;

		if ( ext == "c"		)	return TQt::blue;
		if ( ext == "cpp"	)	return TQt::blue;
		if ( ext == "cc"	)	return TQt::blue;
		if ( ext == "h"		)	return TQt::blue;
		if ( ext == "hpp"	)	return TQt::blue;
		if ( ext == "el"	)	return TQt::blue;

		if ( ext == "o"		)	return TQColor( 0xff, 0xa0, 0x00 );
		if ( ext == "lo"	)	return TQColor( 0xff, 0xa0, 0x00 );
		if ( ext == "Po"	)	return TQColor( 0xff, 0xa0, 0x00 );
		if ( ext == "al"	)	return TQColor( 0xff, 0xa0, 0x00 );
		if ( ext == "moc.cpp"	)	return TQColor( 0xff, 0xa0, 0x00 );
		if ( ext == "moc.cc"	)	return TQColor( 0xff, 0xa0, 0x00 );
		if ( ext == "elc"	)	return TQColor( 0xff, 0xa0, 0x00 );
		if ( ext == "la"	)	return TQColor( 0xff, 0xa0, 0x00 );
		if ( ext == "a"		)	return TQColor( 0xff, 0xa0, 0x00 );
		if ( ext == "rpm"	)	return TQColor( 0xff, 0xa0, 0x00 );

		if ( lowerExt == "tar.bz2" )	return TQt::green;
		if ( lowerExt == "tar.gz"  )	return TQt::green;
		if ( lowerExt == "tgz"	)	return TQt::green;
		if ( lowerExt == "bz2"	)	return TQt::green;
		if ( lowerExt == "bz"	)	return TQt::green;
		if ( lowerExt == "gz"	)	return TQt::green;

		if ( lowerExt == "html"	)	return TQt::blue;
		if ( lowerExt == "htm"	)	return TQt::blue;
		if ( lowerExt == "txt"	)	return TQt::blue;
		if ( lowerExt == "doc"	)	return TQt::blue;

		if ( lowerExt == "png"	)	return TQt::cyan;
		if ( lowerExt == "jpg"	)	return TQt::cyan;
		if ( lowerExt == "jpeg"	)	return TQt::cyan;
		if ( lowerExt == "gif"	)	return TQt::cyan;
		if ( lowerExt == "tif"	)	return TQt::cyan;
		if ( lowerExt == "tiff"	)	return TQt::cyan;
		if ( lowerExt == "bmp"	)	return TQt::cyan;
		if ( lowerExt == "xpm"	)	return TQt::cyan;
		if ( lowerExt == "tga"	)	return TQt::cyan;

		if ( lowerExt == "wav"	)	return TQt::yellow;
		if ( lowerExt == "mp3"	)	return TQt::yellow;

		if ( lowerExt == "avi"	)	return TQColor( 0xa0, 0xff, 0x00 );
		if ( lowerExt == "mov"	)	return TQColor( 0xa0, 0xff, 0x00 );
		if ( lowerExt == "mpg"	)	return TQColor( 0xa0, 0xff, 0x00 );
		if ( lowerExt == "mpeg"	)	return TQColor( 0xa0, 0xff, 0x00 );

		if ( lowerExt == "pdf"	)	return TQt::blue;
		if ( lowerExt == "ps"	)	return TQt::cyan;


		// Some DOS/Windows types

		if ( lowerExt == "exe"	)	return TQt::magenta;
		if ( lowerExt == "com"	)	return TQt::magenta;
		if ( lowerExt == "dll"	)	return TQColor( 0xff, 0xa0, 0x00 );
		if ( lowerExt == "zip"	)	return TQt::green;
		if ( lowerExt == "arj"	)	return TQt::green;


		// No match so far? Try the next extension. Some files might have
		// more than one, e.g., "tar.bz2" - if there is no match for
		// "tar.bz2", there might be one for just "bz2".

		ext = ext.section( '.', 1 );
	    }

	    // Shared libs
	    if ( TQRegExp( "lib.*\\.so.*" ).exactMatch( file->name() ) )
		return TQColor( 0xff, 0xa0, 0x00 );

	    // Very special, but common: Core dumps
	    if ( file->name() == "core" )	return TQt::red;

	    // Special case: Executables
	    if ( ( file->mode() & S_IXUSR  ) == S_IXUSR )	return TQt::magenta;
	}
	else // Directories
	{
	    // TO DO
	    return TQt::blue;
	}
    }

    return TQt::white;
}






KTreemapSelectionRect::KTreemapSelectionRect( TQCanvas * canvas, const TQColor & color )
    : TQCanvasRectangle( canvas )
{
    setPen( TQPen( color, 2 ) );
    setZ( 1e10 );		// Higher than everything else
}



void
KTreemapSelectionRect::highlight( KTreemapTile * tile )
{
    if ( tile )
    {
	TQRect tileRect = tile->rect();

	move( tileRect.x(), tileRect.y() );
	setSize( tileRect.width(), tileRect.height() );

	if ( ! isVisible() )
	    show();
    }
    else
    {
	if ( isVisible() )
	    hide();
    }
}



// EOF
