/*****************************************************************

Copyright (c) 2001 Matthias Elter <elter@kde.org>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.#

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

******************************************************************/

#include <qpainter.h>
#include <qpixmap.h>
#include <qimage.h>
#include <qbitmap.h>
#include <qcolor.h>
#include <qtooltip.h>
#include <qstyle.h>

#include <kdebug.h>
#include <kapplication.h>
#include <klocale.h>
#include <kiconloader.h>
#include <kiconeffect.h>
#include <kimageeffect.h>
#include <kglobalsettings.h>

#include "tasklmbmenu.h"
#include "taskrmbmenu.h"
#include "taskcontainer.h"
#include "taskcontainer.moc"

QImage TaskContainer::blendGradient = QImage();
TaskBar::Action TaskContainer::leftButtonAction = TaskBar::ShowTaskList;
TaskBar::Action TaskContainer::middleButtonAction = TaskBar::ActivateRaiseOrIconify;
TaskBar::Action TaskContainer::rightButtonAction = TaskBar::ShowOperationsMenu;

TaskContainer::TaskContainer( Task *task, TaskManager* manager, bool show, bool sort, bool icon, QWidget *parent, const char *name )
    : QToolButton( parent, name )
    , arrowType(Qt::LeftArrow)
    , taskManager( manager )
    , showAll( show )
    , sortByDesktop( sort )
    , showIcon( icon )
    , discardNextMouseEvent( false )
{
    init();

    tasks.append( task );
    updateFilteredTaskList();
    sid = task->classClass();
    setAcceptDrops(true); // Always enabled to activate task during drag&drop.

    connect( task, SIGNAL( changed() ), SLOT( update() ) );
}

TaskContainer::TaskContainer( Startup *startup, PixmapList *startupFrames, TaskManager* manager, bool show, bool sort, bool icon, QWidget *parent, const char *name )
    : QToolButton( parent, name )
    , arrowType(Qt::LeftArrow)
    , taskManager( manager )
    , showAll( show )
    , sortByDesktop( sort )
    , showIcon( icon )
    , discardNextMouseEvent( false )
{
    init();

    startups.append( startup );
    sid = startup->bin();

    frames = startupFrames;

    connect( startup, SIGNAL( changed() ), SLOT( update() ) );
    animationTimer.start( 100 );
}

void TaskContainer::init()
{
    setBackgroundMode( NoBackground );

    tasks.setAutoDelete( FALSE );
    ftasks.setAutoDelete( FALSE );
    startups.setAutoDelete( FALSE );

    connect( this, SIGNAL( clicked() ), SLOT( slotClicked() ) );

    QToolTip::add( this, name() );

    animBg = QPixmap( 16, 16 );

    // timers
    connect( &animationTimer, SIGNAL( timeout() ), SLOT( animationTimerFired() ) );
    connect( &dragSwitchTimer, SIGNAL( timeout() ), SLOT( dragSwitch() ) );
    currentFrame = 0;
    frames = 0;
}

TaskContainer::~TaskContainer()
{
    animationTimer.stop();
    dragSwitchTimer.stop();
}

void TaskContainer::update()
{
    QToolTip::add( this, name() );
    repaint();
}

void TaskContainer::animationTimerFired()
{
    if (frames && showIcon) {
        QPixmap *pm = frames->at( currentFrame );

        // draw pixmap
        if ( pm && !pm->isNull() ) {
	    // we only have to redraw the background for frames 0, 8 and 9
	    if ( currentFrame == 0 || currentFrame > 7 ) {
		// double buffered painting
		QPixmap composite( animBg );
		bitBlt( &composite, 0, 0, pm );
		bitBlt( this, iconRect.x(), iconRect.y(), &composite );
    	    }
	    else
		bitBlt( this, iconRect.x(), iconRect.y(), pm );
	}

        // increment frame counter
        if ( currentFrame >= 9)
	    currentFrame = 0;
        else
	    currentFrame++;
    }
}

QSizePolicy TaskContainer::sizePolicy() const
{
    return QSizePolicy( QSizePolicy::Expanding, QSizePolicy::Expanding );
}

void TaskContainer::resizeEvent( QResizeEvent * )
{
    // calculate the icon rect
    QRect br( style().subRect( QStyle::SR_PushButtonContents, this ) );
    iconRect = QStyle::visualRect( QRect(br.x() + 2, (height() - 16) / 2, 16, 16), this );
}

void TaskContainer::add( Task* task )
{
    if ( !task ) return;

    tasks.append( task );
    updateFilteredTaskList();
    connect( task, SIGNAL( changed() ), SLOT( update() ) );
    if ( sid.isEmpty() )
	sid = task->classClass();

    update();
}

void TaskContainer::add( Startup* startup )
{
    if ( !startup ) return;

    startups.append( startup );
    if ( sid.isEmpty() )
	sid = startup->bin();

    connect( startup, SIGNAL( changed() ), SLOT( update() ) );
    if ( !animationTimer.isActive() )
	animationTimer.start( 100 );

    update();
}

void TaskContainer::remove( Task* task )
{
    if ( !task ) return;

    tasks.removeRef( task );
    updateFilteredTaskList();

    update();
}

void TaskContainer::remove( Startup* startup )
{
    if ( !startup ) return;

    startups.removeRef( startup );
    if ( startups.isEmpty() )
	animationTimer.stop();

    update();
}

bool TaskContainer::contains( Task* task )
{
    if ( !task ) return FALSE;
    return ( tasks.contains( task ) > 0 );
}

bool TaskContainer::contains( Startup* startup )
{
    if ( !startup ) return FALSE;
    return ( startups.contains( startup ) > 0 );
}

bool TaskContainer::contains( WId win )
{
    for ( Task* t = tasks.first(); t ; t = tasks.next() )
	if ( t->window() == win )
	    return TRUE;
    return FALSE;
}

bool TaskContainer::isEmpty()
{
    return ( tasks.isEmpty() && startups.isEmpty() );
}

QString TaskContainer::id()
{
    return sid;
}

QColor TaskContainer::blendColors( QColor c1, QColor c2 )
{
    int r1, g1, b1;
    int r2, g2, b2;

    c1.rgb( &r1, &g1, &b1 );
    c2.rgb( &r2, &g2, &b2 );

    r1 += (int) ( .5 * ( r2 - r1 ) );
    g1 += (int) ( .5 * ( g2 - g1 ) );
    b1 += (int) ( .5 * ( b2 - b1 ) );

    return QColor( r1, g1, b1 );
}

void TaskContainer::drawButton( QPainter *p )
{
    // get a pointer to the pixmap we're drawing on
    QPixmap *pm( (QPixmap*)p->device() );
    QPixmap pixmap; // icon
    Task *task = NULL;
    bool iconified = true;

    // draw sunken if we contain the active task
    bool active = FALSE;
    for ( Task* t = ftasks.first(); t ; t = ftasks.next() ) {
	task = t;
	if ( ! task->isIconified() )
	    iconified = false;
	if ( t->isActive() ) {
	    active = TRUE;
	    break;
	}
    }

    // get the task icon
    if ( task )
	pixmap = task->pixmap();
    else // we must be a startup (and therefor can't be minimized)
	iconified = false;

    bool sunken = isOn() || active;
    bool reverse = QApplication::reverseLayout();
    QRect br( style().subRect( QStyle::SR_PushButtonContents, this ) );
    QPoint shift = QPoint( style().pixelMetric(QStyle::PM_ButtonShiftHorizontal),
			style().pixelMetric(QStyle::PM_ButtonShiftVertical) );

    // draw button background
    style().drawPrimitive( QStyle::PE_HeaderSection, p, QRect( 0, 0, width(), height() ),
                           colorGroup(), sunken ? QStyle::Style_Down : QStyle::Style_Raised );

    // shift button label on sunken buttons
    if ( sunken )
	p->translate( shift.x(), shift.y() );

    if ( showIcon ) {
        if ( pixmap.isNull() && !startups.isEmpty() )
	    pixmap = SmallIcon( startups.first()->icon() );

	if ( !pixmap.isNull() ) {

	    // make sure it is no larger than 16x16
	    if ( pixmap.width() > 16 || pixmap.height() > 16 ) {
	        QImage tmp = pixmap.convertToImage();
		pixmap.convertFromImage( tmp.smoothScale( 16, 16 ) );
	    }

	    // fade out the icon when minimized
	    if ( iconified )
		KIconEffect::semiTransparent( pixmap );

	    // draw icon
	    QRect pmr( 0, 0, pixmap.width(), pixmap.height() );
	    pmr.moveCenter( iconRect.center() );
	    p->drawPixmap( pmr, pixmap );
	}
    }

    // find text
    QString text = name();

    // modified overlay
    static QString modStr = "[" + i18n( "modified" ) + "]";
    int modStrPos = text.find( modStr );
    int textPos = ( showIcon && !pixmap.isNull() ) ? 2 + 16 + 2 : 0;

    if ( modStrPos >= 0 ) {

	// +1 because we include a space after the closing brace.
	text.remove( modStrPos, modStr.length() + 1 );

	QPixmap modPixmap = SmallIcon( "modified" );

	// draw modified overlay
	if ( ! modPixmap.isNull() ) {
	    QRect r = QStyle::visualRect( QRect( br.x() + textPos, (height() - 16) / 2, 16, 16 ), this );

	    if ( iconified )
	    	KIconEffect::semiTransparent( modPixmap );

	    p->drawPixmap( r, modPixmap );
	    textPos += 16 + 2;
	}
    }

    // draw text
    if ( !text.isEmpty() ) {

	QRect tr = QStyle::visualRect( QRect( br.x() + textPos + 1, 0, width() - textPos, height() ), this );
	int textFlags = AlignVCenter | SingleLine;
	textFlags |= reverse ? AlignRight : AlignLeft;
	QPen textPen;

	// get the color for the text label
	if ( iconified )
	    textPen = QPen( blendColors(colorGroup().button(), colorGroup().buttonText()) );
	else if ( ! active )
	    textPen = QPen( colorGroup().buttonText() );
	else // hack for the dotNET style and others
	    textPen = p->pen();

	if ( p->fontMetrics().width( text ) > width() - br.x() * 2 - textPos ) {

	    if ( blendGradient.isNull() || blendGradient.size() != size() ) {

		QPixmap bgpm( size() );
		QPainter bgp( &bgpm );
		bgpm.fill( black );

		if ( ! reverse ) {
		    QImage gradient = KImageEffect::gradient( QSize( 30, height() ), QColor( 0,0,0 ),
							QColor( 255,255,255 ), KImageEffect::HorizontalGradient );
		    bgp.drawImage( width() - 30, 0, gradient );
		} else {
		    QImage gradient = KImageEffect::gradient( QSize( 30, height() ), QColor( 255,255,255 ),
							QColor( 0,0,0 ), KImageEffect::HorizontalGradient );
		    bgp.drawImage( 0, 0, gradient );
		}

		blendGradient = bgpm.convertToImage();
	    }

	    // draw text into overlay pixmap
	    QPixmap tpm( *pm );
	    QPainter tp( &tpm );

	    // shift button label on sunken buttons
	    // (have to do it again because it's a different painter)
	    if ( sunken )
		tp.translate( shift.x(), shift.y() );

	    tp.setFont( KGlobalSettings::taskbarFont() );
	    if ( sunken ) {
		QFont f = tp.font();
		f.setBold( TRUE );
		tp.setFont( f );
	    }

	    tp.setPen( textPen );

	    tp.drawText( tr, textFlags, text );

	    // blend text into background image
	    QImage img = pm->convertToImage();
	    QImage timg = tpm.convertToImage();
	    KImageEffect::blend( img, timg, blendGradient, KImageEffect::Red );

	    pm->convertFromImage( img );
	}
	else {
	    p->setFont( KGlobalSettings::taskbarFont() );

	    if ( sunken ) {
		QFont f = p->font();
		f.setBold( TRUE );
		p->setFont( f );
	    }
    	    p->setPen( textPen );
	    p->drawText( tr, textFlags, text );
	}
    }

    if( frames && !startups.isEmpty())
    {
	QPixmap *anim = frames->at( currentFrame );
	if ( anim && !anim->isNull() ) {
	    // save the background for the other frames
	    bitBlt( &animBg, QPoint(0,0), pm, iconRect );
	    // draw the animation frame
	    bitBlt( pm, iconRect.x(), iconRect.y(), anim );
	}
    }

    // draw popup arrow
    if ( ftasks.count() >= 2 )
    {
        QStyle::PrimitiveElement e = QStyle::PE_ArrowLeft;

        switch ( arrowType )
        {
            case Qt::LeftArrow: e = QStyle::PE_ArrowLeft; break;
            case Qt::RightArrow: e = QStyle::PE_ArrowRight; break;
            case Qt::UpArrow: e = QStyle::PE_ArrowUp; break;
            case Qt::DownArrow: e = QStyle::PE_ArrowDown; break;
        }
        int flags = QStyle::Style_Enabled;
        QRect ar = QStyle::visualRect( QRect( br.x() + br.width() - 8 - 2, br.y(), 8, br.height() ), this );
        if ( sunken ) {
	    flags |= QStyle::Style_Down;
	    // Change the painter back so the arrow gets drawn in the right location
	    p->translate( -shift.x(), -shift.y() );
	}

	style().drawPrimitive( e, p, ar, colorGroup(), flags );
    }
}

QString TaskContainer::name()
{
    // default to container id
    QString text = id();

    // Upper case first letter: seems to be the right thing to do for most cases
    text = text.left( 1 ).upper() + text.mid( 1 );

    // single task -> use mainwindow caption
    if ( ftasks.count() == 1 ) {
	if ( !ftasks.first()->visibleName().isEmpty() )
	    text = ftasks.first()->visibleName();
    }
    // multiple tasks -> use the common part of all captions
    // if it is more descriptive than the class name
    else if ( ftasks.count() > 1 ) {
	QString match;
	int i = 1;
	bool stop = FALSE;
	while ( match.length() < ftasks.first()->visibleName().length() ) {
	    match = ftasks.first()->visibleName().left( i );
	    for ( Task* t = ftasks.first(); t; t = ftasks.next() )
		if ( match.lower() !=  t->visibleName().left( i ).lower() ) {
		    stop = TRUE;
		    break;
		}

	    if ( stop ) {
		match = ftasks.first()->visibleName().left( --i );
		break;
	    }
	    i++;
	}

	// strip trailing crap
	while( match.length() > 0 && !match[ match.length() - 1].isLetterOrNumber() )
	    match.truncate( match.length() - 1 );

	// more descriptive than id()?
	if ( match.length() >= id().length() )
	    text = match;
    }

    // fall back to startup name
    else {
	for ( Startup* s = startups.first(); s ; s = startups.next() )
	    if ( !s->text().isEmpty() ) {
		text = s->text();
		break;
	    }
    }

    return text;
}

void TaskContainer::mousePressEvent( QMouseEvent* e )
{
    if( discardNextMouseEvent ) {
	discardNextMouseEvent = false;
	return;
    }

    // On left button, only do actions that invoke a menu.
    // Other actions will be handled in slotClicked().
    if( e->button() == LeftButton &&
        ((leftButtonAction == TaskBar::ShowTaskList && ftasks.count() > 1) ||
         leftButtonAction == TaskBar::ShowOperationsMenu) ) {
	performAction( leftButtonAction );
    } else if( e->button() == MidButton ) {
	performAction( middleButtonAction );
    } else if( e->button() == RightButton ) {
	performAction( rightButtonAction );
    } else {
	QToolButton::mousePressEvent( e );
    }
}

void TaskContainer::slotClicked()
{
    // We've already handled these cases above by
    // showing the menu.
    if((leftButtonAction == TaskBar::ShowTaskList && ftasks.count() > 1 ) ||
        leftButtonAction == TaskBar::ShowOperationsMenu )
	return;

    performAction( leftButtonAction );
}

void TaskContainer::performAction( TaskBar::Action action )
{
    if ( ftasks.isEmpty() )
	return;

    switch( action ) {
    case TaskBar::ShowTaskList:
	// If there is only one task, the correct behavior is
	// to activate, raise, or iconify it, not show the task menu.
	if( ftasks.count() > 1 ) {
	    popupMenu( TaskBar::ShowTaskList );
	} else {
	    performAction( TaskBar::ActivateRaiseOrIconify );
	}
	break;
    case TaskBar::ShowOperationsMenu:
	popupMenu( TaskBar::ShowOperationsMenu );
	break;
    case TaskBar::ActivateRaiseOrIconify:
	if ( ftasks.count() == 1) {
	    ftasks.first()->activateRaiseOrIconify();
	} else { // multiple tasks -> cycle list
	    for ( Task* t = ftasks.first(); t ; t = ftasks.next() ) {
		if ( t->isActive() ) {
		    // activate next
		    Task *t = ftasks.next();
		    if ( !t )
			t = ftasks.first();
		    t->activateRaiseOrIconify();
		    return;
	        }
	    }
	    ftasks.first()->activateRaiseOrIconify();
	}
	break;
    case TaskBar::Activate:
	ftasks.first()->activate();
	break;
    case TaskBar::Raise:
	ftasks.first()->raise();
	break;
    case TaskBar::Lower:
	ftasks.first()->lower();
	break;
    case TaskBar::Iconify:
	if( ftasks.first()->isIconified() ) {
	    ftasks.first()->restore();
	} else {
	    ftasks.first()->iconify();
	}
	break;
    default:
	kdWarning(1210) << "Unknown taskbar action!" << endl;
	break;
    }
}

void TaskContainer::popupMenu( TaskBar::Action action )
{
    QPopupMenu* menu;
    if( action == TaskBar::ShowTaskList )
	menu = new TaskLMBMenu( &ftasks );
    else if( action == TaskBar::ShowOperationsMenu )
	menu = new TaskRMBMenu( &ftasks, taskManager );
    else
	return;

    // calc popup menu position
    QPoint pos( mapToGlobal( QPoint( 0, 0 ) ) );

    switch( arrowType ) {
	    case RightArrow:
		pos.setX( pos.x() + width() );
		break;
	    case LeftArrow:
		pos.setX( pos.x() - menu->sizeHint().width() );
		break;
	    case DownArrow:
		if ( QApplication::reverseLayout() )
		    pos.setX( pos.x() + width() - menu->sizeHint().width() );
		pos.setY( pos.y() + height() );
		break;
	    case UpArrow:
		if ( QApplication::reverseLayout() )
		    pos.setX( pos.x() + width() - menu->sizeHint().width() );
		pos.setY( pos.y() - menu->sizeHint().height() );
	    default:
		break;
	}
    menu->installEventFilter( this );
    menu->exec( pos );

    delete menu;
}

// This is the code that gives us the proper behavior
// when a popup menu is displayed and we are clicked:
// close the menu, and don't reopen it immediately.
// It's copied from QToolButton. Unfortunately Qt is lame
// as usual and makes interesting stuff private or
// non-virtual, so we have to copy code.
bool TaskContainer::eventFilter( QObject *o, QEvent *e )
{
    switch ( e->type() ) {
    case QEvent::MouseButtonPress:
    case QEvent::MouseButtonDblClick:
	{
	    QMouseEvent *me = (QMouseEvent*)e;
	    QPoint p = me->globalPos();
	    if ( QApplication::widgetAt( p, TRUE ) == this )
		discardNextMouseEvent = true;
	}
	break;
    default:
	break;
    }
    return QToolButton::eventFilter( o, e );
}

void TaskContainer::setArrowType( Qt::ArrowType at )
{
    if( arrowType == at )
	return;

    arrowType = at;
    repaint();
}

void TaskContainer::publishIconGeometry( QPoint global )
{
    QPoint p = global + geometry().topLeft();

    for ( Task* t = ftasks.first(); t ; t = ftasks.next() )
	t->publishIconGeometry( QRect( p.x(), p.y(), width(), height() ) );
}

void TaskContainer::dragEnterEvent( QDragEnterEvent* e )
{
    // if a dragitem is held for over a taskbutton for two seconds,
    // activate corresponding window

    if ( ftasks.count() < 1 ) return;

    if( !ftasks.first()->isActive() )
	dragSwitchTimer.start( 1000, TRUE );

    QToolButton::dragEnterEvent( e );
}

void TaskContainer::dragLeaveEvent( QDragLeaveEvent* e )
{
    dragSwitchTimer.stop();

    QToolButton::dragLeaveEvent( e );
}

void TaskContainer::dragSwitch()
{
    if ( ftasks.count() < 1 ) return;
    // FIXME
    ftasks.first()->activate();
}

int TaskContainer::desktop()
{
    if ( tasks.isEmpty() )
	return taskManager->currentDesktop();

    if ( tasks.count() > 1 )
	return taskManager->numberOfDesktops();

    return tasks.first()->desktop();
}

bool TaskContainer::onCurrentDesktop()
{
    if ( isEmpty() )
	return FALSE;

    if ( tasks.count() < 1
	 && startups.count() > 0 )
	return TRUE;

    for ( Task* t = tasks.first(); t ; t = tasks.next() )
	if ( t->isOnCurrentDesktop() )
	    return TRUE;

    return FALSE;
}

void TaskContainer::setShowAll( bool s )
{
    if ( s == showAll )
	return;

    showAll = s;
    updateFilteredTaskList();
    update();
}

void TaskContainer::setSortByDesktop( bool s )
{
    if ( s == sortByDesktop )
	return;

    sortByDesktop = s;
    updateFilteredTaskList();
    update();
}

void TaskContainer::setShowIcon( bool s )
{
    if ( s == showIcon )
	return;

    showIcon = s;
    updateFilteredTaskList();
    update();
}

void TaskContainer::updateFilteredTaskList()
{
    ftasks.clear();

    if ( showAll ) {
	ftasks = tasks;
    }
    else {
	for ( Task* t = tasks.first(); t ; t = tasks.next() )
	    if ( t->isOnCurrentDesktop() )
		ftasks.append( t );
    }

    // sort container list by desktop
    if ( sortByDesktop && ftasks.count() > 1 ) {

	TaskList sorted;
	for ( int desktop = -1; desktop <= taskManager->numberOfDesktops(); desktop++ ) {
	    for ( Task *t = ftasks.first(); t; t = ftasks.next() )
		if ( t->desktop() == desktop )
		    sorted.append( t );
	}
	ftasks = sorted;
    }
}

void TaskContainer::desktopChanged( int )
{
    updateFilteredTaskList();
}

void TaskContainer::windowDesktopChanged( WId )
{
    updateFilteredTaskList();
    update();
}
