/********************************************************************
 KWin - the KDE window manager
 This file is part of the KDE project.

Copyright (C) 2009 Martin Gräßlin <kde@martin-graesslin.com>

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

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.  If not, see <http://www.gnu.org/licenses/>.
*********************************************************************/


#include "desktopchangeosd.h"
#include <QTextStream>
#include "workspace.h"

#include <X11/extensions/shape.h>

#include <QEasingCurve>
#include <QPropertyAnimation>
#include <QHash>
#include <QGraphicsScene>
#include <QRect>
#include <KDE/Plasma/Theme>
#include <KDE/Plasma/PaintUtils>
#include <kiconloader.h>

namespace KWin
{

DesktopChangeOSD::DesktopChangeOSD( Workspace* ws )
    : QGraphicsView()
    , m_wspace( ws )
    , m_scene( 0 )
    , m_active( false )
    , m_show( false )
    , m_delayTime( 0 )
    , m_textOnly( false )
    {
    setWindowFlags( Qt::X11BypassWindowManagerHint );
    setFrameStyle( QFrame::NoFrame );
    viewport()->setAutoFillBackground( false );
    setHorizontalScrollBarPolicy( Qt::ScrollBarAlwaysOff );
    setVerticalScrollBarPolicy( Qt::ScrollBarAlwaysOff );
    setAttribute( Qt::WA_TranslucentBackground );
    m_frame.setImagePath( "dialogs/background" );
    m_frame.setCacheAllRenderedFrames( true );
    m_frame.setEnabledBorders( Plasma::FrameSvg::AllBorders );

    m_item_frame.setImagePath( "widgets/pager" );
    m_item_frame.setCacheAllRenderedFrames( true );
    m_item_frame.setEnabledBorders( Plasma::FrameSvg::AllBorders );

    m_delayedHideTimer.setSingleShot( true );
    connect( &m_delayedHideTimer, SIGNAL(timeout()), this, SLOT(hide()) );

    m_scene = new QGraphicsScene( 0 );
    setScene( m_scene );

    reconfigure();

    m_scene->addItem( new DesktopChangeText( m_wspace ) );
    }

DesktopChangeOSD::~DesktopChangeOSD()
    {
    delete m_scene;
    }

void DesktopChangeOSD::reconfigure()
    {
    KSharedConfigPtr c(KGlobal::config());
    const KConfigGroup cg = c->group( "PopupInfo" );
    m_show = cg.readEntry( "ShowPopup", false );
    m_delayTime = cg.readEntry( "PopupHideDelay", 1000 );
    m_textOnly = cg.readEntry( "TextOnly", false );
    numberDesktopsChanged();
    }

void DesktopChangeOSD::desktopChanged( int old )
    {
    if( !m_show )
        return;
    // we have to stop in case the old desktop does not exist anymore
    if( old > m_wspace->numberOfDesktops() )
        return;
    // calculate where icons have to be shown
    QPoint diff = m_wspace->desktopGridCoords( m_wspace->currentDesktop() ) - m_wspace->desktopGridCoords( old );
    QHash< int, DesktopChangeItem::Arrow > hash = QHash< int, DesktopChangeItem::Arrow>();
    int desktop = old;
    int target = m_wspace->currentDesktop();
    int x = diff.x();
    int y = diff.y();
    if( y >= 0 )
        {
        // first go in x direction, then in y
        while( desktop != target )
            {
            if( x != 0 )
                {
                if( x < 0 )
                    {
                    x++;
                    hash.insert( desktop, DesktopChangeItem::LEFT );
                    desktop = m_wspace->desktopToLeft( desktop );
                    }
                else
                    {
                    x--;
                    hash.insert( desktop, DesktopChangeItem::RIGHT );
                    desktop = m_wspace->desktopToRight( desktop );
                    }
                continue;
                }
            y--;
            hash.insert( desktop, DesktopChangeItem::DOWN );
            desktop = m_wspace->desktopBelow( desktop );
            }
        }
    else
        {
        // first go in y direction, then in x
        while( target != desktop )
            {
            if( y != 0 )
                {
                // only go upward
                y++;
                hash.insert( desktop, DesktopChangeItem::UP );
                desktop = m_wspace->desktopAbove( desktop );
                continue;
                }
            if( x != 0 )
                {
                if( x < 0 )
                    {
                    x++;
                    hash.insert( desktop, DesktopChangeItem::LEFT );
                    desktop = m_wspace->desktopToLeft( desktop );
                    }
                else
                    {
                    x--;
                    hash.insert( desktop, DesktopChangeItem::RIGHT );
                    desktop = m_wspace->desktopToRight( desktop );
                    }
                }
            }
        }
    // now we know which desktop has to show an arrow -> set the arrow for each desktop
    int numberOfArrows = qAbs( diff.x() ) + qAbs( diff.y() );
    foreach( QGraphicsItem* it, m_scene->items() )
        {
        DesktopChangeItem* item = qgraphicsitem_cast< DesktopChangeItem* >( it );
        if( item )
            {
            if( hash.contains( item->desktop() ) )
                {
                QPoint distance = m_wspace->desktopGridCoords( m_wspace->currentDesktop() )
                    - m_wspace->desktopGridCoords( item->desktop() );
                int desktopDistance = numberOfArrows - ( qAbs( distance.x() ) + qAbs( distance.y() ) ); 
                int start = m_delayTime/numberOfArrows * desktopDistance - m_delayTime*0.15f;
                int stop = m_delayTime/numberOfArrows * (desktopDistance + 1) + m_delayTime*0.15f;
                start = qMax( start, 0 );
                item->setArrow( hash[ item->desktop() ], start, stop );
                }
            else
                {
                item->setArrow( DesktopChangeItem::NONE, 0, 0 );
                }
            if( old != m_wspace->currentDesktop() )
                {
                if( item->desktop() == m_wspace->currentDesktop() )
                    item->startDesktopHighLightAnimation( m_delayTime * 0.33 );
                if( m_active && item->desktop() == old )
                    item->stopDesktopHighLightAnimation();
                }
            }
        }
    if( m_active )
        {
        // for text only we need to resize
        if( m_textOnly )
            resize();
        // already active - just update and reset timer
        update();
        }
    else
        {
        m_active = true;
        resize();
        show();
        raise();
        }
    // Set a zero inputmask, effectively making clicks go "through" the popup
    // For those who impatiently wait to click on a dialog behind the it
    XShapeCombineRectangles( display(), winId(), ShapeInput, 0, 0, NULL, 0, ShapeSet, Unsorted );
    m_delayedHideTimer.start( m_delayTime );
    }

void DesktopChangeOSD::hideEvent( QHideEvent* )
    {
    m_delayedHideTimer.stop();
    m_active = false;
    }

void DesktopChangeOSD::drawBackground( QPainter* painter, const QRectF& rect )
    {
    painter->save();
    painter->setCompositionMode( QPainter::CompositionMode_Source );
    qreal left, top, right, bottom;
    m_frame.getMargins( left, top, right, bottom );
    m_frame.paintFrame( painter, rect.adjusted( -left, -top, right, bottom ) );
    painter->restore();
    }

void DesktopChangeOSD::numberDesktopsChanged()
    {
    foreach( QGraphicsItem* it, m_scene->items() )
        {
        DesktopChangeItem* item = qgraphicsitem_cast<DesktopChangeItem*>(it);
        if( item )
            {
            m_scene->removeItem( item );
            }
        }

    if( !m_textOnly )
        {
        for( int i=1; i<=m_wspace->numberOfDesktops(); i++ )
            {
            DesktopChangeItem* item = new DesktopChangeItem( m_wspace, this, i );
            m_scene->addItem( item );
            }
        }
    }

void DesktopChangeOSD::resize()
    {
    QRect screenRect = m_wspace->clientArea( ScreenArea, m_wspace->activeScreen(), m_wspace->currentDesktop() );
    QRect fullRect = m_wspace->clientArea( FullArea, m_wspace->activeScreen(), m_wspace->currentDesktop() );
    qreal left, top, right, bottom;
    m_frame.getMargins( left, top, right, bottom );

    QSize desktopGridSize = m_wspace->desktopGridSize();
    float itemWidth = fullRect.width()*0.1f;
    float itemHeight = fullRect.height()*0.1f;
    // 2 px distance between each desktop + each desktop a width of 5 % of full screen + borders
    float width = (desktopGridSize.width()-1)*2 + desktopGridSize.width()*itemWidth + left + right;
    float height = (desktopGridSize.height()-1)*2 + top + bottom;

    // bound width between ten and 33 percent of active screen
    float tempWidth = qBound( screenRect.width()*0.25f, width, screenRect.width()*0.5f );
    if( tempWidth != width )
        {
        // have to adjust the height
        width = tempWidth;
        itemWidth = (width - (desktopGridSize.width()-1)*2 - left - right)/desktopGridSize.width();
        itemHeight = itemWidth*(float)((float)fullRect.height()/(float)fullRect.width());
        }
    height += itemHeight*desktopGridSize.height();
    height += fontMetrics().height() + 4;

    // we do not increase height, but it's bound to a third of screen height
    float tempHeight = qMin( height, screenRect.height()*0.5f );
    float itemOffset = 0.0f;
    if( tempHeight != height )
        {
        // have to adjust item width
        height = tempHeight;
        itemHeight = (height - (fontMetrics().height() + 4) - top - bottom - (desktopGridSize.height()-1)*2)/
            desktopGridSize.height();
        itemOffset = itemWidth;
        itemWidth = itemHeight*(float)((float)fullRect.width()/(float)fullRect.height());
        itemOffset -= itemWidth;
        itemOffset *= (float)desktopGridSize.width()*0.5f;
        }

    // set size to the desktop name if the "pager" is not shown
    if( m_textOnly )
        {
        height = fontMetrics().height() + 4 + top + bottom;
        width = fontMetrics().boundingRect( m_wspace->desktopName( m_wspace->currentDesktop() ) ).width() +
                4 + left + right;
        }

    QRect rect = QRect( screenRect.x() + (screenRect.width()-width)/2,
        screenRect.y() + (screenRect.height()-height)/2,
        width,
        height );
    setGeometry( rect );
    m_scene->setSceneRect( 0, 0, width, height );
    m_frame.resizeFrame( QSize( width, height ) );

    if (Plasma::Theme::defaultTheme()->windowTranslucencyEnabled())
        {
        // blur background
        Plasma::WindowEffects::enableBlurBehind(winId(), true, m_frame.mask());
        Plasma::WindowEffects::overrideShadow(winId(), true);
        }
    else
        {
        // do not trim to mask with compositing enabled, otherwise shadows are cropped
        setMask( m_frame.mask() );
        }

    // resize item frame
    m_item_frame.setElementPrefix( "normal" );
    m_item_frame.resizeFrame( QSize( itemWidth, itemHeight ) );
    m_item_frame.setElementPrefix( "hover" );
    m_item_frame.resizeFrame( QSize( itemWidth, itemHeight ) );

    // reset the items
    foreach( QGraphicsItem* it, m_scene->items() )
        {
        DesktopChangeItem* item = qgraphicsitem_cast<DesktopChangeItem*>(it);
        if( item )
            {
            item->setWidth( itemWidth );
            item->setHeight( itemHeight );
            QPoint coords = m_wspace->desktopGridCoords( item->desktop() );
            item->setPos( left + itemOffset + coords.x()*(itemWidth+2),
                top + fontMetrics().height() + 4 + coords.y()*(itemHeight+4) );
            }
        DesktopChangeText* text = qgraphicsitem_cast<DesktopChangeText*>(it);
        if( text )
            {
            text->setPos( left, top );
            text->setWidth( width - left - right );
            if( m_textOnly )
                text->setHeight( fontMetrics().height() + 4 );
            else
                text->setHeight( fontMetrics().height() );
            }
        }
    }

//*******************************
// DesktopChangeText
//*******************************
DesktopChangeText::DesktopChangeText( Workspace* ws )
    : QGraphicsItem()
    , m_wspace( ws )
    , m_width( 0.0f )
    , m_height( 0.0f )
    {
    }

DesktopChangeText::~DesktopChangeText()
    {
    }

QRectF DesktopChangeText::boundingRect() const
    {
    return QRectF( 0, 0, m_width, m_height );
    }

void DesktopChangeText::paint( QPainter* painter, const QStyleOptionGraphicsItem* , QWidget* )
    {
    painter->setPen( Plasma::Theme::defaultTheme()->color( Plasma::Theme::TextColor ) );
    painter->drawText( boundingRect(), Qt::AlignCenter | Qt:: AlignVCenter,
        m_wspace->desktopName( m_wspace->currentDesktop() ) );
    }

//*******************************
// DesktopChangeItem
//*******************************
DesktopChangeItem::DesktopChangeItem( Workspace* ws, DesktopChangeOSD* parent, int desktop )
    : QGraphicsItem()
    , m_wspace( ws )
    , m_parent( parent )
    , m_desktop( desktop )
    , m_width( 0.0f )
    , m_height( 0.0f )
    , m_arrow( NONE )
    , m_arrowShown( false )
    , m_fadeInArrow( false )
    , m_fadeInHighLight( false )
    , m_arrowValue( 0.0 )
    , m_highLightValue( 0.0 )
    {
    m_delayed_show_arrow_timer.setSingleShot( true );
    m_delayed_hide_arrow_timer.setSingleShot( true );
    connect( &m_delayed_show_arrow_timer, SIGNAL(timeout()), this, SLOT(showArrow()));
    connect( &m_delayed_hide_arrow_timer, SIGNAL(timeout()), this, SLOT(hideArrow()));
    }

DesktopChangeItem::~DesktopChangeItem()
    {
    }

QRectF DesktopChangeItem::boundingRect() const
    {
    return QRectF( 0, 0, m_width, m_height );
    }

void DesktopChangeItem::setArrow( Arrow arrow, int start_delay, int hide_delay )
    {
    // stop timers
    m_delayed_show_arrow_timer.stop();
    m_delayed_hide_arrow_timer.stop();

    QPropertyAnimation *arrowAnimation = m_arrowAnimation.data();
    if( arrowAnimation )
        {
        arrowAnimation->stop();
        m_arrowAnimation.clear();
        }

    m_arrowShown = false;
    m_arrow = arrow;
    if( m_arrow != NONE )
        {
        m_delayed_show_arrow_timer.start( start_delay );
        m_delayed_hide_arrow_timer.start( hide_delay );
        }
    }

qreal DesktopChangeItem::arrowValue() const
    {
    qCritical() << __func__ << m_arrowValue;
    return m_arrowValue;
    }

qreal DesktopChangeItem::highLightValue() const
    {
    return m_highLightValue;
    }

void DesktopChangeItem::setArrowValue( qreal value )
        {
    m_arrowValue = value;

    update();
        }

void DesktopChangeItem::setHighLightValue( qreal value )
    {
    m_highLightValue = value;

    update();
    }

void DesktopChangeItem::showArrow()
    {
    m_arrowShown = true;

    QPropertyAnimation *arrowAnimation = m_arrowAnimation.data();
    if( !arrowAnimation )
        {
        arrowAnimation = new QPropertyAnimation( this, "arrowValue" );
        arrowAnimation->setDuration( m_parent->getDelayTime()*0.15f );
        arrowAnimation->setStartValue( 0.0 );
        arrowAnimation->setEndValue( 1.0 );

        m_arrowAnimation = arrowAnimation;
        }

    m_fadeInArrow = true;

    arrowAnimation->setEasingCurve( QEasingCurve::InQuad );
    arrowAnimation->setDirection( QAbstractAnimation::Forward );
    arrowAnimation->start();
    }

void DesktopChangeItem::hideArrow()
    {
    m_fadeInArrow = false;

    QPropertyAnimation *arrowAnimation = m_arrowAnimation.data();
    if( arrowAnimation )
        {
        arrowAnimation->setEasingCurve( QEasingCurve::OutQuad );
        arrowAnimation->setDirection( QAbstractAnimation::Backward );
        arrowAnimation->start( QAbstractAnimation::DeleteWhenStopped );

        connect( arrowAnimation, SIGNAL(finished()), this, SLOT(arrowAnimationFinished()) );
        }
    }

void DesktopChangeItem::startDesktopHighLightAnimation( int time )
    {
    QPropertyAnimation *highLightAnimation = m_highLightAnimation.data();
    if( !highLightAnimation )
        {
        highLightAnimation = new QPropertyAnimation( this, "highLightValue" );
        highLightAnimation->setDuration( time );
        highLightAnimation->setStartValue( 0.0 );
        highLightAnimation->setEndValue( 1.0 );

        m_highLightAnimation = highLightAnimation;
        }

    m_fadeInHighLight = true;

    highLightAnimation->setEasingCurve( QEasingCurve::InQuad );
    highLightAnimation->setDirection( QAbstractAnimation::Forward );
    highLightAnimation->start();
    }

void DesktopChangeItem::stopDesktopHighLightAnimation()
    {
    m_fadeInHighLight = false;

    QPropertyAnimation *highLightAnimation = m_highLightAnimation.data();
    if(highLightAnimation)
        {
        highLightAnimation->setEasingCurve( QEasingCurve::OutQuad );
        highLightAnimation->setDirection( QAbstractAnimation::Backward );
        highLightAnimation->start( QAbstractAnimation::DeleteWhenStopped );
        }
    }

void DesktopChangeItem::arrowAnimationFinished()
    {
    if( !m_fadeInArrow )
        m_arrowShown = false;
    }

void DesktopChangeItem::paint( QPainter* painter, const QStyleOptionGraphicsItem* , QWidget* )
    {
    if( m_wspace->currentDesktop() == m_desktop || (!m_highLightAnimation.isNull() &&
                m_highLightAnimation.data()->state() == QAbstractAnimation::Running) )
        {
        qreal left, top, right, bottom;
        m_parent->itemFrame()->getMargins( left, top, right, bottom );
        if( !m_highLightAnimation.isNull() &&
                m_highLightAnimation.data()->state() == QAbstractAnimation::Running )
            {
            // there is an animation - so we use transition from normal to active or vice versa
            if( m_fadeInHighLight )
                {
                m_parent->itemFrame()->setElementPrefix( "normal" );
                QPixmap normal = m_parent->itemFrame()->framePixmap();
                m_parent->itemFrame()->setElementPrefix( "hover" );
                QPixmap result = Plasma::PaintUtils::transition( normal,
                    m_parent->itemFrame()->framePixmap(), m_highLightValue );
                painter->drawPixmap( boundingRect().toRect(), result);
                }
            else
                {
                m_parent->itemFrame()->setElementPrefix( "hover" );
                QPixmap normal = m_parent->itemFrame()->framePixmap();
                m_parent->itemFrame()->setElementPrefix( "normal" );
                QPixmap result = Plasma::PaintUtils::transition( normal,
                    m_parent->itemFrame()->framePixmap(), 1.0 - m_highLightValue );
                painter->drawPixmap( boundingRect().toRect(), result);
                }
            }
        else
            {
            // no animation - just render the active frame
            m_parent->itemFrame()->setElementPrefix( "hover" );
            m_parent->itemFrame()->paintFrame( painter, boundingRect() );
            }
        QColor rectColor = Plasma::Theme::defaultTheme()->color( Plasma::Theme::TextColor );
        rectColor.setAlphaF( 0.6 * m_highLightValue );
        QBrush rectBrush = QBrush( rectColor );
        painter->fillRect( boundingRect().adjusted( left, top, -right, -bottom ), rectBrush );
        }
    else
        {
        m_parent->itemFrame()->setElementPrefix( "normal" );
        m_parent->itemFrame()->paintFrame( painter, boundingRect() );
        }

    if( !m_arrowShown )
        return;
    // paint the arrow
    QPixmap icon;
    int iconWidth = 32;
    qreal maxsize = qMin( boundingRect().width(), boundingRect().height() );
    if( maxsize > 128.0 )
        iconWidth = 128;
    else if( maxsize > 64.0 )
        iconWidth = 64.0;
    else if( maxsize > 32.0 )
        iconWidth = 32.0;
    else
        iconWidth = 16.0;
    QRect iconRect = QRect( boundingRect().x() + boundingRect().width()/2 - iconWidth/2,
        boundingRect().y() + boundingRect().height()/2 - iconWidth/2,
        iconWidth, iconWidth );
    switch( m_arrow )
        {
        case UP:
            icon = KIconLoader::global()->loadIcon( "go-up", KIconLoader::Desktop, iconWidth );
            break;
        case DOWN:
            icon = KIconLoader::global()->loadIcon( "go-down", KIconLoader::Desktop, iconWidth );
            break;
        case LEFT:
            icon = KIconLoader::global()->loadIcon( "go-previous", KIconLoader::Desktop, iconWidth );
            break;
        case RIGHT:
            icon = KIconLoader::global()->loadIcon( "go-next", KIconLoader::Desktop, iconWidth );
            break;
        default:
            break;
        }
    if( m_arrow != NONE )
        {
        if( !m_arrowAnimation.isNull() &&
                m_arrowAnimation.data()->state() == QAbstractAnimation::Running &&
                !qFuzzyCompare(m_arrowValue, qreal(1.0)) )
            {
            QPixmap temp( icon.size() );
            temp.fill( Qt::transparent );

            QPainter p( &temp );
            p.setCompositionMode( QPainter::CompositionMode_Source );
            p.drawPixmap( 0, 0, icon );
            p.setCompositionMode( QPainter::CompositionMode_DestinationIn );
            p.fillRect( temp.rect(), QColor( 0, 0, 0, 255*m_arrowValue ) );
            p.end();

            icon = temp;
            }
        painter->drawPixmap( iconRect, icon );
        }
    }
}

#include "desktopchangeosd.moc"
