/*
   This file is part of the Nepomuk KDE project.
   Copyright (C) 2007 Sebastian Trueg <trueg@kde.org>

   This library is free software; you can redistribute it and/or
   modify it under the terms of the GNU Library General Public
   License version 2 as published by the Free Software Foundation.

   This library 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
   Library General Public License for more details.

   You should have received a copy of the GNU Library General Public License
   along with this library; see the file COPYING.LIB.  If not, write to
   the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
   Boston, MA 02110-1301, USA.
 */

#include "tagcloud.h"
#include "newtagdialog.h"

#include <QtGui/QFont>
#include <QtGui/QFontMetrics>
#include <QtCore/QList>
#include <QtGui/QPushButton>
#include <QtCore/Qt>
#include <QtCore/QTime>
#include <QtGui/QPainter>
#include <QtGui/QMouseEvent>
#include <QtGui/QPalette>
#include <QtGui/QInputDialog>
#include <QtGui/QAction>

#include <KRandomSequence>
#include <KLocale>
#include <KColorScheme>
#include <KDebug>
#include <KMenu>
#include <KIcon>
#include <KMessageBox>

#include <Soprano/Client/DBusModel>
#include <Soprano/QueryResultIterator>
#include <Soprano/Vocabulary/RDF>
#include <Soprano/Vocabulary/NAO>

#include <nepomuk/resourcemanager.h>

#include <math.h>


namespace {
    const int s_hSpacing = 10;
    const int s_vSpacing = 5;

    class TagNode {
    public:
        TagNode()
            : weight( 0 ),
              selected( false ) {
        }

        bool operator==( const TagNode& other ) const {
            return tag == other.tag;
        }

        // fixed info
        Nepomuk::Tag tag;
        int weight;

        // misc
        bool selected;

        // info generated by rebuildCloud
        QFont font;
        QRect rect;
        QRect zoomedRect;
        QString text;
    };

    bool tagNodeNameLessThan( const TagNode& n1, const TagNode& n2 ) {
        return n1.text < n2.text;
    }

    bool tagNodeWeightLessThan( const TagNode& n1, const TagNode& n2 ) {
        return n1.weight < n2.weight;
    }

    int rowLength( const QList<TagNode*>& row ) {
        int rowLen = 0;
        for ( int j = 0; j < row.count(); ++j ) {
            rowLen += row[j]->rect.width();
            if ( j < row.count()-1 ) {
                rowLen += s_hSpacing;
            }
        }
        return rowLen;
    }

    int rowHeight( const QList<TagNode*>& row ) {
        int h = 0;
        for ( int j = 0; j < row.count(); ++j ) {
            h = qMax( row[j]->rect.height(), h );
        }
        return h;
    }

    QSize cloudSize( const QList<QList<TagNode*> >& rows ) {
        int w = 0;
        int h = 0;
        for ( int i = 0; i < rows.count(); ++i ) {
            w = qMax( w, rowLength( rows[i] ) );
            h += rowHeight( rows[i] );
            if ( i < rows.count()-1 ) {
                h += s_vSpacing;
            }
        }
        return QSize( w, h );
    }
}


class Nepomuk::TagCloud::Private
{
public:
    Private( TagCloud* parent )
        : maxFontSize( 0 ),
          minFontSize( 0 ),
          maxNumberDisplayedTags( 0 ),
          selectionEnabled( false ),
          newTagButtonEnabled( false ),
          alignment( Qt::AlignCenter ),
          sorting( SortAlpabetically ),
          zoomEnabled( true ),
          contextMenuEnabled( true ),
          showAllTags( false ),
          customNewTagAction( 0 ),
          hoverTag( 0 ),
          cachedHfwWidth( -1 ),
          m_parent( parent ) {
        newTagNode.text = i18nc( "@label", "New Tag..." );
    }

    int maxFontSize;
    int minFontSize;
    int maxNumberDisplayedTags;
    bool selectionEnabled;
    bool newTagButtonEnabled;
    Qt::Alignment alignment;
    Sorting sorting;
    bool zoomEnabled;
    bool contextMenuEnabled;

    // The resource whose tags we are showing
    // invalid if we show all tags or a selection
    KUrl resource;
    bool showAllTags;

    // the actual nodes
    QList<TagNode> nodes;

    // just a helper structure for speeding up things
    QList<QList<TagNode*> > rows;

    TagNode newTagNode;
    QAction* customNewTagAction;

    TagNode* hoverTag;

    QMatrix zoomMatrix;

    QSize cachedSizeHint;
    int cachedHfwWidth;
    int cachedHfwHeight;

    void invalidateCachedValues() {
        cachedSizeHint = QSize();
        cachedHfwWidth = -1;
    }

    int getMinFontSize() const;
    int getMaxFontSize() const;
    void updateNodeWeights();
    void updateNodeFonts();
    void sortNodes();
    void rebuildCloud();
    TagNode* tagAt( const QPoint& pos );
    TagNode* findTagInRow( const QList<TagNode*>& row, const QPoint& pos );
    TagNode* nodeForTag( const Tag& tag );
    int calculateWeight( const Nepomuk::Tag& tag );

private:
    TagCloud* m_parent;
};


int Nepomuk::TagCloud::Private::getMinFontSize() const
{
    return minFontSize > 0 ? minFontSize : ( 8 * m_parent->font().pointSize() / 10 );
}


int Nepomuk::TagCloud::Private::getMaxFontSize() const
{
    return maxFontSize > 0 ? maxFontSize : ( 22 * m_parent->font().pointSize() / 10 );
}


int Nepomuk::TagCloud::Private::calculateWeight( const Nepomuk::Tag& tag )
{
    // stupid SPARQL has no functions such as count!
    Soprano::QueryResultIterator it
        = ResourceManager::instance()->mainModel()->executeQuery( QString( "select ?r where { ?r <%1> <%2> . }" )
                                                                  .arg( Soprano::Vocabulary::NAO::hasTag().toString() )
                                                                  .arg( QString::fromAscii( tag.resourceUri().toEncoded() ) ),
                                                                  Soprano::Query::QueryLanguageSparql );
    int w = 0;
    while ( it.next() ) {
        ++w;
    }
    return w;
}


void Nepomuk::TagCloud::Private::updateNodeWeights()
{
    bool changedWeights = false;
    for ( QList<TagNode>::iterator it = nodes.begin();
          it != nodes.end(); ++it ) {
        TagNode& node = *it;
        int w = calculateWeight( node.tag );
        if ( w != node.weight ) {
            node.weight = w;
            changedWeights = true;
        }
    }
    if ( changedWeights ) {
        updateNodeFonts();
    }
}


void Nepomuk::TagCloud::Private::updateNodeFonts()
{
    int maxWeight = 0;
    int minWeight = 0;
    for ( QList<TagNode>::iterator it = nodes.begin();
          it != nodes.end(); ++it ) {
        TagNode& node = *it;
        minWeight = qMin( minWeight, node.weight );
        maxWeight = qMax( maxWeight, node.weight );
    }

    // calculate font sizes
    // ----------------------------------------------
    int usedMinFontSize = getMinFontSize();
    int usedMaxFontSize = getMaxFontSize();
    for ( QList<TagNode>::iterator it = nodes.begin();
          it != nodes.end(); ++it ) {
        TagNode& node = *it;
        double normalizedWeight = (double)(node.weight - minWeight) / (double)qMax(maxWeight - minWeight, 1);
        node.font = m_parent->font();
        node.font.setPointSize( usedMinFontSize + (int)((double)(usedMaxFontSize-usedMinFontSize) * normalizedWeight) );
        if( normalizedWeight > 0.8 )
            node.font.setBold( true );
    }

    if ( newTagButtonEnabled ) {
        newTagNode.font = m_parent->font();
        newTagNode.font.setPointSize( usedMinFontSize );
        newTagNode.font.setUnderline( true );
    }
}


void Nepomuk::TagCloud::Private::sortNodes()
{
    if ( sorting == SortAlpabetically ) {
        qSort( nodes.begin(), nodes.end(), tagNodeNameLessThan );
    }
    else if ( sorting == SortByWeight ) {
        qSort( nodes.begin(), nodes.end(), tagNodeWeightLessThan );
    }
    else if ( sorting == SortRandom ) {
        KRandomSequence().randomize( nodes );
    }
}


void Nepomuk::TagCloud::Private::rebuildCloud()
{
    // - Always try to be quadratic
    // - Always prefer to expand horizontally
    // - If we cannot fit everything into m_parent->contentsRect(), zoom
    // - If alignment & Qt::AlignJustify insert spaces between tags

    sortNodes();

    QRect contentsRect = m_parent->contentsRect();

    // initialize the nodes' sizes
    // ----------------------------------------------
    for ( QList<TagNode>::iterator it = nodes.begin();
          it != nodes.end(); ++it ) {
        TagNode& node = *it;
        node.rect = QFontMetrics( node.font ).boundingRect( node.text );
    }
    if ( newTagButtonEnabled ) {
        newTagNode.rect = QFontMetrics( newTagNode.font ).boundingRect( customNewTagAction ? customNewTagAction->text() : newTagNode.text );
    }


    // and position the nodes
    // ----------------------------------------------
    rows.clear();
    if ( !nodes.isEmpty() || newTagButtonEnabled ) {
        if ( 0 ) { // FIXME: make it configurable
            QRect lineRect;
            QRect totalRect;
            QList<TagNode*> row;
            for ( QList<TagNode>::iterator it = nodes.begin();
                  it != nodes.end(); /* We do increment it below */ ) {
                TagNode& node = *it;

                int usedSpacing = row.isEmpty() ? 0 : s_hSpacing;
                if ( lineRect.width() + usedSpacing + node.rect.width() <= contentsRect.width() ) {
                    node.rect.moveBottomLeft( QPoint( lineRect.right() + usedSpacing, lineRect.bottom() ) );
                    QRect newLineRect = lineRect.united( node.rect );
                    newLineRect.moveTopLeft( lineRect.topLeft() );
                    lineRect = newLineRect;
                    row.append( &node );

                    // update all other nodes in this line
                    Q_FOREACH( TagNode* n, row ) {
                        n->rect.moveBottom( lineRect.bottom() - ( lineRect.height() - n->rect.height() )/2 );
                    }

                    ++it;
                }
                else {
                    rows.append( row );
                    row.clear();
                    int newLineTop = lineRect.bottom() + s_vSpacing;
                    lineRect = QRect();
                    lineRect.moveTop( newLineTop );
                }
            }
            rows.append( row );
        }
        else {
            // initialize first row
            rows.append( QList<TagNode*>() );
            for ( QList<TagNode>::iterator it = nodes.begin();
                  it != nodes.end(); ++it ) {
                TagNode& node = *it;
                rows.first().append( &node );
            }
            if ( newTagButtonEnabled ) {
                rows.first().append( &newTagNode );
            }

            // calculate the rows
            QList<QList<TagNode*> > bestRows( rows );
            QSize size( rowLength( rows.first() ), rowHeight( rows.first() ) );
            QSize bestSize( size );
            while ( ( size.height() < size.width() ||
                      size.width() > contentsRect.width() ) &&
                    size.height() <= contentsRect.height() ) {
                // find the longest row
                int maxRow = 0;
                int maxLen = 0;
                for ( int i = 0; i < rows.count(); ++i ) {
                    int rowLen = rowLength( rows[i] );
                    if ( rowLen > maxLen ) {
                        maxLen = rowLen;
                        maxRow = i;
                    }
                }

                // move the last item from the longest row to the next row
                TagNode* node = rows[maxRow].takeLast();
                if ( rows.count() <= maxRow+1 ) {
                    rows.append( QList<TagNode*>() );
                }
                rows[maxRow+1].prepend( node );

                // update the size
                size = cloudSize( rows );

                if ( size.width() < bestSize.width() &&
                     ( size.width() > size.height()  ||
                       bestSize.width() > contentsRect.width() ) &&
                     size.height() <= contentsRect.height() ) {
                    bestSize = size;
                    bestRows = rows;
                }
            }
            rows = bestRows;

            // position the tags
            int y = 0;
            for ( QList<QList<TagNode*> >::iterator rowIt = rows.begin(); rowIt != rows.end(); ++rowIt ) {
                QList<TagNode*>& row = *rowIt;
                int h = rowHeight( row );
                int x = 0;
                Q_FOREACH( TagNode* node, row ) {
                    node->rect.moveTop( y + ( h - node->rect.height() )/2 );
                    node->rect.moveLeft( x );
                    x += s_hSpacing + node->rect.width();
                }
                y += h + s_vSpacing;
            }
        }


        // let's see if we have to zoom
        // ----------------------------------------------
        zoomMatrix = QMatrix();
        int w = contentsRect.width();
        if ( zoomEnabled ) {
            for ( QList<QList<TagNode*> >::iterator rowIt = rows.begin(); rowIt != rows.end(); ++rowIt ) {
                QList<TagNode*>& row = *rowIt;
                w = qMax( w, row.last()->rect.right() );
            }
            if ( w > contentsRect.width() ) {
                double zoomFactor = ( double )contentsRect.width() / ( double )w;
                zoomMatrix.scale( zoomFactor, zoomFactor );
            }
        }

        // force horizontal alignment
        // ----------------------------------------------
        for ( QList<QList<TagNode*> >::iterator rowIt = rows.begin(); rowIt != rows.end(); ++rowIt ) {
            QList<TagNode*>& row = *rowIt;
            int space = /*contentsRect.right()*/w - row.last()->rect.right();
            if ( alignment & ( Qt::AlignRight|Qt::AlignHCenter ) ) {
                Q_FOREACH( TagNode* node, row ) {
                    node->rect.moveLeft( node->rect.left() + ( alignment & Qt::AlignRight ? space : space/2 ) );
                }
            }
            else if ( alignment & Qt::AlignJustify && row.count() > 1 ) {
                space /= ( row.count()-1 );
                int i = 0;
                Q_FOREACH( TagNode* node, row ) {
                    node->rect.moveLeft( node->rect.left() + ( space * i++ ) );
                }
            }
        }

        // force vertical alignment
        // ----------------------------------------------
        int verticalSpace = contentsRect.bottom() - rows.last().first()->rect.bottom();
        if ( alignment & ( Qt::AlignBottom|Qt::AlignVCenter ) ) {
            for ( QList<QList<TagNode*> >::iterator rowIt = rows.begin(); rowIt != rows.end(); ++rowIt ) {
                Q_FOREACH( TagNode* node, *rowIt ) {
                    node->rect.moveTop( node->rect.top() + ( alignment & Qt::AlignBottom ? verticalSpace : verticalSpace/2 ) );
                }
            }
        }

        for( QList<TagNode>::iterator it = nodes.begin(); it != nodes.end(); ++it ) {
            it->zoomedRect = zoomMatrix.mapRect( it->rect );
        }
        newTagNode.zoomedRect = zoomMatrix.mapRect( newTagNode.rect );
    }

    m_parent->updateGeometry();
    m_parent->update();
}


// binary search in row
TagNode* Nepomuk::TagCloud::Private::findTagInRow( const QList<TagNode*>& row, const QPoint& pos )
{
    int x = m_parent->width() ? row.count() * pos.x() / m_parent->width() : 0;

    int i = 0;
    while ( 1 ) {
        if ( x-i >= 0 && x-i < row.count() && row[x-i]->zoomedRect.contains( pos ) ) {
            return row[x-i];
        }
        else if ( x+i >= 0 && x+i < row.count() && row[x+i]->zoomedRect.contains( pos ) ) {
            return row[x+i];
        }
        if ( x-i < 0 && x+i >= row.count() ) {
            return 0;
        }
        ++i;
    }
    return 0;
}


// binary search in cloud
TagNode* Nepomuk::TagCloud::Private::tagAt( const QPoint& pos )
{
    int y = m_parent->height() ? rows.count() * pos.y() / m_parent->height() : 0;

    int i = 0;
    while ( 1 ) {
        if ( y-i >= 0 && y-i < rows.count() ) {
            if ( TagNode* node = findTagInRow( rows[y-i], pos ) ) {
                return node;
            }
        }
        if ( y+i >= 0 && y+i < rows.count() ) {
            if ( TagNode* node = findTagInRow( rows[y+i], pos ) ) {
                return node;
            }
        }
        if ( y-i < 0 && y+i >= rows.count() ) {
            return 0;
        }
        ++i;
    }
    return 0;
}


TagNode* Nepomuk::TagCloud::Private::nodeForTag( const Tag& tag )
{
    for ( QList<TagNode>::iterator it = nodes.begin();
          it != nodes.end(); ++it ) {
        TagNode& node = *it;
        if ( tag == node.tag ) {
            return &node;
        }
    }
    return 0;
}



Nepomuk::TagCloud::TagCloud( QWidget* parent )
    : QFrame( parent ),
      d( new Private(this) )
{
    QSizePolicy policy( QSizePolicy::Preferred,
                        QSizePolicy::Preferred );
    policy.setHeightForWidth( true );
    setSizePolicy( policy );
    setMouseTracking( true );

    // Since signals are delivered in no particular order
    // our slot might be called before the resources are updated
    // Then, we would use invalid cached data.
    // By using queued connections this problem should be solved.
    connect( ResourceManager::instance()->mainModel(),
             SIGNAL( statementAdded( const Soprano::Statement& ) ),
             this,
             SLOT( slotStatementAdded( const Soprano::Statement& ) ),
             Qt::QueuedConnection );
    connect( ResourceManager::instance()->mainModel(),
             SIGNAL( statementRemoved( const Soprano::Statement& ) ),
             this,
             SLOT( slotStatementRemoved( const Soprano::Statement& ) ),
             Qt::QueuedConnection );
}


Nepomuk::TagCloud::~TagCloud()
{
    delete d;
}


void Nepomuk::TagCloud::setMaxFontSize( int size )
{
    d->invalidateCachedValues();
    d->maxFontSize = size;
    d->updateNodeFonts();
    d->rebuildCloud();
}


void Nepomuk::TagCloud::setMinFontSize( int size )
{
    d->invalidateCachedValues();
    d->minFontSize = size;
    d->updateNodeFonts();
    d->rebuildCloud();
}


void Nepomuk::TagCloud::setMaxNumberDisplayedTags( int n )
{
    d->maxNumberDisplayedTags = n;
    d->rebuildCloud();
}


void Nepomuk::TagCloud::setSelectionEnabled( bool enabled )
{
    d->selectionEnabled = enabled;
    update();
}


void Nepomuk::TagCloud::setNewTagButtonEnabled( bool enabled )
{
    d->newTagButtonEnabled = enabled;
    d->rebuildCloud();
}


bool Nepomuk::TagCloud::zoomEnabled() const
{
    return d->zoomEnabled;
}


void Nepomuk::TagCloud::setZoomEnabled( bool zoom )
{
    if ( d->zoomEnabled != zoom ) {
        d->zoomEnabled = zoom;
        d->rebuildCloud();
    }
}


void Nepomuk::TagCloud::setContextMenuEnabled( bool enabled )
{
    d->contextMenuEnabled = enabled;
}


void Nepomuk::TagCloud::setAlignment( Qt::Alignment alignment )
{
    d->alignment = alignment;
    d->rebuildCloud();
}


void Nepomuk::TagCloud::setSorting( Sorting s )
{
    d->invalidateCachedValues();
    d->sorting = s;
    d->rebuildCloud();
}


void Nepomuk::TagCloud::showAllTags()
{
    showTags( Nepomuk::Tag::allTags() );
    d->showAllTags = true;
}


void Nepomuk::TagCloud::showResourceTags( const Resource& resource )
{
    showTags( resource.tags() );
    d->resource = resource.resourceUri();
}


void Nepomuk::TagCloud::showTags( const QList<Tag>& tags )
{
    d->resource = KUrl();
    d->showAllTags = false;
    d->invalidateCachedValues();
    d->nodes.clear();
    Q_FOREACH( const Tag &tag, tags ) {
        TagNode node;
        node.tag = tag;
        node.weight = d->calculateWeight( tag );
        node.text = node.tag.genericLabel();

        d->nodes.append( node );
    }
    d->updateNodeFonts();
    d->rebuildCloud();
}


void Nepomuk::TagCloud::setTagSelected( const Tag& tag, bool selected )
{
    if ( TagNode* node = d->nodeForTag( tag ) ) {
        node->selected = selected;
        if ( d->selectionEnabled ) {
            update( node->zoomedRect );
        }
    }
}


QSize Nepomuk::TagCloud::sizeHint() const
{
    // If we have tags d->rebuildCloud() has been called at least once,
    // thus, we have proper rects (i.e. needed sizes)

    if ( !d->cachedSizeHint.isValid() ) {
        QList<QList<TagNode*> > rows;
        rows.append( QList<TagNode*>() );
        for ( QList<TagNode>::iterator it = d->nodes.begin();
              it != d->nodes.end(); ++it ) {
            TagNode& node = *it;
            rows.first().append( &node );
        }
        if ( d->newTagButtonEnabled ) {
            rows.first().append( &d->newTagNode );
        }

        QSize size( rowLength( rows.first() ), rowHeight( rows.first() ) );
        QSize bestSize( size );
        while ( size.height() < size.width() ) {
            // find the longest row
            int maxRow = 0;
            int maxLen = 0;
            for ( int i = 0; i < rows.count(); ++i ) {
                int rowLen = rowLength( rows[i] );
                if ( rowLen > maxLen ) {
                    maxLen = rowLen;
                    maxRow = i;
                }
            }

            // move the last item from the longest row to the next row
            TagNode* node = rows[maxRow].takeLast();
            if ( rows.count() <= maxRow+1 ) {
                rows.append( QList<TagNode*>() );
            }
            rows[maxRow+1].prepend( node );

            // update the size
            size = cloudSize( rows );

            if ( size.width() < bestSize.width() &&
                 size.width() > size.height() ) {
                bestSize = size;
            }
        }

        d->cachedSizeHint = QSize( bestSize.width() + frameWidth()*2,
                                   bestSize.height() + frameWidth()*2 );
    }

    return d->cachedSizeHint;
}


QSize Nepomuk::TagCloud::minimumSizeHint() const
{
    return QFrame::minimumSizeHint();
    // If we have tags d->rebuildCloud() has been called at least once,
    // thus, we have proper rects (i.e. needed sizes)
    if ( d->nodes.isEmpty() && !d->newTagButtonEnabled ) {
        return QSize( fontMetrics().width( i18nc( "@label Indicator when no tags defined", "No Tags" ) ), fontMetrics().height() );
    }
    else {
        QSize size;
        for ( QList<TagNode>::iterator it = d->nodes.begin();
              it != d->nodes.end(); ++it ) {
            size.setWidth( qMax( size.width(), ( *it ).rect.width() ) );
            size.setHeight( qMax( size.height(), ( *it ).rect.height() ) );
        }
        if ( d->newTagButtonEnabled ) {
            size.setWidth( qMax( size.width(), d->newTagNode.rect.width() ) );
            size.setHeight( qMax( size.height(), d->newTagNode.rect.height() ) );
        }
        size.setWidth( size.width() + frameWidth()*2 );
        size.setHeight( size.height() + frameWidth()*2 );
        return size;
    }
}


int Nepomuk::TagCloud::heightForWidth( int contentsWidth ) const
{
    // If we have tags d->rebuildCloud() has been called at least once,
    // thus, we have proper rects (i.e. needed sizes)

    if ( d->cachedHfwWidth != contentsWidth ) {
        // have to keep in mind the frame
        contentsWidth -= frameWidth()*2;

        QList<TagNode*> allNodes;
        for ( QList<TagNode>::iterator it = d->nodes.begin();
              it != d->nodes.end(); ++it ) {
            TagNode& node = *it;
            allNodes.append( &node );
        }
        if ( d->newTagButtonEnabled ) {
            allNodes.append( &d->newTagNode );
        }

        int h = 0;
        bool newRow = true;
        int rowW = 0;
        int rowH = 0;
        int maxW = 0;
        for ( int i = 0; i < allNodes.count(); ++i ) {
            int w = rowW;
            if ( !newRow ) {
                w += s_hSpacing;
            }
            newRow = false;
            w += allNodes[i]->rect.width();
            if ( w <= contentsWidth ) {
                rowH = qMax( rowH, allNodes[i]->rect.height() );
                rowW = w;
            }
            else {
                if ( h > 0 ) {
                    h += s_vSpacing;
                }
                h += rowH;
                rowH = allNodes[i]->rect.height();
                rowW = allNodes[i]->rect.width();
            }
            maxW = qMax( maxW, rowW );
        }
        if ( rowH > 0 ) {
            h += s_vSpacing + rowH;
        }

        d->cachedHfwWidth = contentsWidth;
        d->cachedHfwHeight = h;

        // zooming
        if ( maxW > contentsWidth ) {
            d->cachedHfwHeight = d->cachedHfwHeight * contentsWidth / maxW;
        }
    }

    return d->cachedHfwHeight + frameWidth()*2;
}


void Nepomuk::TagCloud::resizeEvent( QResizeEvent* e )
{
    QFrame::resizeEvent( e );
    d->rebuildCloud();
    update();
}


void Nepomuk::TagCloud::paintEvent( QPaintEvent* e )
{
    QFrame::paintEvent( e );

    KStatefulBrush normalTextBrush( KColorScheme::View, KColorScheme::NormalText );
    KStatefulBrush activeTextBrush( KColorScheme::View, KColorScheme::VisitedText );
    KStatefulBrush hoverTextBrush( KColorScheme::View, KColorScheme::ActiveText );

    QPainter p( this );
    QRegion paintRegion = e->region();

    p.save();
    p.setMatrix( d->zoomMatrix );

    for ( QList<TagNode>::iterator it = d->nodes.begin();
          it != d->nodes.end(); ++it ) {
        TagNode& node = *it;

        if ( paintRegion.contains( node.zoomedRect ) ) {
            p.setFont( node.font );

            if ( &node == d->hoverTag ) {
                p.setPen( hoverTextBrush.brush( this ).color() );
            }
            else if ( d->selectionEnabled && node.selected ) {
                p.setPen( activeTextBrush.brush( this ).color() );
            }
            else {
                p.setPen( normalTextBrush.brush( this ).color() );
            }
            p.drawText( node.rect, Qt::AlignCenter, node.text );
        }
    }

    if ( d->newTagButtonEnabled ) {
        p.setFont( d->newTagNode.font );
        if ( &d->newTagNode == d->hoverTag ) {
            p.setPen( hoverTextBrush.brush( this ).color() );
        }
        else {
            p.setPen( normalTextBrush.brush( this ).color() );
        }
        p.drawText( d->newTagNode.rect, Qt::AlignCenter, d->customNewTagAction ? d->customNewTagAction->text() : d->newTagNode.text );
    }

    p.restore();
}


void Nepomuk::TagCloud::mousePressEvent( QMouseEvent* e )
{
    if ( e->button() == Qt::LeftButton ) {
        if ( TagNode* node = d->tagAt( e->pos() ) ) {
            kDebug() << "clicked" << node->text;
            if ( node == &d->newTagNode ) {
                if ( d->customNewTagAction ) {
                    d->customNewTagAction->trigger();
                }
                else {
                    // FIXME: nicer gui
                    Tag newTag = NewTagDialog::createTag( this );
                    if ( newTag.isValid() ) {
                        emit tagAdded( newTag );
                    }
                }
            }
            else {
                emit tagClicked( node->tag );
                if ( d->selectionEnabled ) {
                    kDebug() << "Toggleing tag" << node->text;
                    node->selected = !node->selected;
                    emit tagToggled( node->tag, node->selected );
                    update( node->zoomedRect );
                }
            }
        }
    }
    else if ( d->contextMenuEnabled &&
              e->button() == Qt::RightButton ) {
        if ( TagNode* node = d->tagAt( e->pos() ) ) {
            KMenu menu;
            QAction* a = menu.addAction( KIcon( "edit-delete" ), i18nc( "@action:menu", "Delete tag '%1'", node->text ) );
            if ( menu.exec( e->globalPos() ) == a &&
                 KMessageBox::questionYesNo( this, i18n( "Do you really want to delete tag '%1'?", node->text ) ) == KMessageBox::Yes ) {
                if ( d->selectionEnabled &&
                     node->selected ) {
                    node->selected = false;
                    emit tagToggled( node->tag, false );
                }
                node->tag.remove();
                d->nodes.removeAll( *node );
                d->rebuildCloud();
            }
        }
    }
}


void Nepomuk::TagCloud::mouseMoveEvent( QMouseEvent* e )
{
    if ( e->buttons() == Qt::NoButton ) {

        TagNode* oldHoverTag = d->hoverTag;

        if ( ( d->hoverTag = d->tagAt( e->pos() ) ) &&
             !d->selectionEnabled ) {
            setCursor( Qt::PointingHandCursor );
        }
        else if ( d->newTagButtonEnabled &&
                  d->newTagNode.zoomedRect.contains( e->pos() ) ) {
            d->hoverTag = &d->newTagNode;
            setCursor( Qt::PointingHandCursor );
        }
        else {
            unsetCursor();
        }

        if ( oldHoverTag || d->hoverTag ) {
            QRect updateRect;
            if ( d->hoverTag )
                updateRect = updateRect.united( d->hoverTag->zoomedRect );
            if ( oldHoverTag )
                updateRect = updateRect.united( oldHoverTag->zoomedRect );

            update( updateRect );
        }
    }
}


void Nepomuk::TagCloud::leaveEvent( QEvent* )
{
    unsetCursor();
    if ( d->hoverTag ) {
        QRect updateRect = d->hoverTag->zoomedRect;
        d->hoverTag = 0;
        update( updateRect );
    }
}


void Nepomuk::TagCloud::slotStatementAdded( const Soprano::Statement& s )
{
    if ( s.predicate().uri() == Soprano::Vocabulary::RDF::type() &&
         s.object().uri() == Nepomuk::Tag::resourceTypeUri() ) {
        // new tag created
        if ( d->showAllTags ) {
            showAllTags();
        }
    }
    else if ( s.predicate().uri() == Nepomuk::Resource::tagUri() ) {
        if ( s.subject().uri() == d->resource ) {
            showResourceTags( d->resource );
        }
        else {
            // weights might have changed
            d->updateNodeWeights();
            d->rebuildCloud();
        }
    }
}


void Nepomuk::TagCloud::slotStatementRemoved( const Soprano::Statement& s )
{
    // FIXME: In theory might contain empty nodes as wildcards

    if ( s.predicate().uri() == Nepomuk::Resource::tagUri() ) {
        if ( d->resource.isValid() &&
             d->resource == s.subject().uri() ) {
            showResourceTags( d->resource );
        }
        else {
            // weights might have changed
            d->updateNodeWeights();
            d->rebuildCloud();
        }
    }
    else if ( s.predicate().uri() == Soprano::Vocabulary::RDF::type() &&
         s.object().uri() == Nepomuk::Tag::resourceTypeUri() ) {
        // tag deleted
        if ( d->showAllTags ) {
            showAllTags();
        }
    }
}


void Nepomuk::TagCloud::setCustomNewTagAction( QAction* action )
{
    d->customNewTagAction = action;
    setNewTagButtonEnabled( action != 0 );
}

#include "tagcloud.moc"
