/****************************************************************************************
 * Copyright (c) 2007-2008 Ian Monroe <ian@monroe.nu>                                   *
 * Copyright (c) 2007-2009 Nikolaj Hald Nielsen <nhn@kde.org>                           *
 * Copyright (c) 2008 Seb Ruiz <ruiz@kde.org>                                           *
 * Copyright (c) 2008 Soren Harward <stharward@gmail.com>                               *
 * Copyright (c) 2010 Nanno Langstraat <langstr@gmail.com>                              *
 * Copyright (c) 2010 Dennis Francis <dennisfrancis.in@gmail.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) version 3 or        *
 * any later version accepted by the membership of KDE e.V. (or its successor approved  *
 * by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of  *
 * version 3 of the license.                                                            *
 *                                                                                      *
 * 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/>.                           *
 ****************************************************************************************/

#define DEBUG_PREFIX "Playlist::Model"

#include "PlaylistModel.h"

#include "core/support/Amarok.h"
#include "SvgHandler.h"
#include "amarokconfig.h"
#include "AmarokMimeData.h"
#include "core/capabilities/ReadLabelCapability.h"
#include "core/support/Debug.h"
#include "DirectoryLoader.h"
#include "EngineController.h"
#include "core/capabilities/MultiSourceCapability.h"
#include "core/capabilities/SourceInfoCapability.h"
#include "core/collections/Collection.h"
#include "core/meta/Statistics.h"
#include "core/meta/support/MetaUtility.h"
#include "PlaylistColumnNames.h"
#include "PlaylistActions.h"
#include "PlaylistController.h"
#include "PlaylistItem.h"
#include "core-impl/playlists/types/file/PlaylistFileSupport.h"
#include "UndoCommands.h"

#include <KGlobal>
#include <KIconLoader>
#include <KLocale>
#include <KUrl>

#include <QAction>
#include <QTimer>
#include <QDate>
#include <QStringList>
#include <QTextDocument>

#include <typeinfo>

#define TOOLTIP_STATIC_LINEBREAK 50

bool Playlist::Model::s_tooltipColumns[NUM_COLUMNS];
bool Playlist::Model::s_showToolTip;

// ------- helper functions for the tooltip

static bool
fitsInOneLineHTML(const QString& text)
{
    // The size of the normal, standard line
    const int lnSize = TOOLTIP_STATIC_LINEBREAK;
    return (text.size() <= lnSize);
}

static QString
breakLongLinesHTML(const QString& text)
{
    // Now let's break up long lines so that the tooltip doesn't become hideously large
    if (fitsInOneLineHTML(text))
    {
        // If the text is not too long, return it as it is
        return text;
    }
    else
    {
        const int lnSize = TOOLTIP_STATIC_LINEBREAK;
        QString textInLines;

        QStringList words = text.trimmed().split(' ');
        int lineLength = 0;
        while(words.size() > 0)
        {
            QString word = words.first();
            // Let's check if the next word makes the current line too long.
            if (lineLength + word.size() + 1 > lnSize)
            {
                if (lineLength > 0)
                {
                    textInLines += "<br/>";
                }
                lineLength = 0;
                // Let's check if the next word is not too long for the new line to contain
                // If it is, cut it
                while (word.size() > lnSize)
                {
                    QString wordPart = word;
                    wordPart.resize(lnSize);
                    word.remove(0,lnSize);
                    textInLines += wordPart + "<br/>";
                }
            }
            textInLines += word + " ";
            lineLength += word.size() + 1;
            words.removeFirst();
        }
        return textInLines.trimmed();
    }
}

/**
* Prepares a row for the playlist tooltips consisting of an icon representing
* an mp3 tag and its value
* @param column The colunm used to display the icon
* @param value The QString value to be shown
* @return The line to be shown or an empty QString if the value is null
*/
static QString
HTMLLine( const Playlist::Column& column, const QString& value, bool force = false )
{
    if( !value.isEmpty() || force )
    {
        QString line;
        line += "<tr><td align=\"right\">";
        line += "<img src=\""+KIconLoader::global()->iconPath( Playlist::iconNames[column] , -16)+"\" />";
        line += "</td><td align=\"left\">";
        line += breakLongLinesHTML( value );
        line += "</td></tr>";
        return line;
    }
    else
        return QString();
}

/**
* Prepares a row for the playlist tooltips consisting of an icon representing
* an mp3 tag and its value
* @param column The colunm used to display the icon
* @param value The integer value to be shown
* @return The line to be shown or an empty QString if the value is 0
*/
static QString
HTMLLine( const Playlist::Column& column, const int value, bool force = false )
{
    // there is currenly no numeric meta-data that would have sense if it were negative.
    // also, zero denotes not available, unknow etc; don't show these unless forced.
    if( value > 0 || force )
    {
        return HTMLLine( column, QString::number( value ) );
    }
    else
        return QString();
}


Playlist::Model::Model( QObject *parent )
        : QAbstractListModel( parent )
        , m_activeRow( -1 )
        , m_totalLength( 0 )
        , m_totalSize( 0 )
        , m_setStateOfItem_batchMinRow( -1 )
        , m_saveStateTimer( new QTimer(this) )
{
    DEBUG_BLOCK

    m_saveStateTimer->setInterval( 5000 );
    m_saveStateTimer->setSingleShot( true );
    connect( m_saveStateTimer, SIGNAL(timeout()),
             this, SLOT(saveState()) );
    connect( this, SIGNAL(modelReset()),
             this, SLOT(queueSaveState()) );
    connect( this, SIGNAL(dataChanged(QModelIndex,QModelIndex)),
             this, SLOT(queueSaveState()) );
    connect( this, SIGNAL(rowsInserted(QModelIndex,int,int)),
             this, SLOT(queueSaveState()) );
    connect( this, SIGNAL(rowsMoved(QModelIndex,int,int,QModelIndex,int)),
             this, SLOT(queueSaveState()) );
    connect( this, SIGNAL(rowsRemoved(QModelIndex,int,int)),
             this, SLOT(queueSaveState()) );
}

Playlist::Model::~Model()
{
    DEBUG_BLOCK

    // Save current playlist
    exportPlaylist( Amarok::defaultPlaylistPath() );

    qDeleteAll( m_items );
}

void
Playlist::Model::saveState()
{
    exportPlaylist( Amarok::defaultPlaylistPath() );
}

void
Playlist::Model::queueSaveState()
{
    if ( !m_saveStateTimer->isActive() )
        m_saveStateTimer->start();
}

QVariant
Playlist::Model::headerData( int section, Qt::Orientation orientation, int role ) const
{
    Q_UNUSED( orientation );

    if ( role != Qt::DisplayRole )
        return QVariant();

    return columnNames( section );
}

void
Playlist::Model::setTooltipColumns( bool columns[] )
{
    for( int i=0; i<Playlist::NUM_COLUMNS; ++i )
        s_tooltipColumns[i] = columns[i];
}

void
Playlist::Model::enableToolTip( bool enable )
{
    s_showToolTip = enable;
}

QString
Playlist::Model::tooltipFor( Meta::TrackPtr track ) const
{
    QString text;
    KLocale *locale = KGlobal::locale();
    // get the shared pointers now to be thread safe
    Meta::ArtistPtr artist = track->artist();
    Meta::AlbumPtr album = track->album();
    Meta::ArtistPtr albumArtist = album ? album->albumArtist() : Meta::ArtistPtr();
    Meta::GenrePtr genre = track->genre();
    Meta::ComposerPtr composer = track->composer();
    Meta::YearPtr year = track->year();
    Meta::StatisticsPtr statistics = track->statistics();

    if( s_tooltipColumns[Playlist::Title] )
        text += HTMLLine( Playlist::Title, track->name() );

    if( s_tooltipColumns[Playlist::Artist] && artist )
        text += HTMLLine( Playlist::Artist, artist->name() );

    // only show albumArtist when different from artist (it should suffice to compare pointers)
    if( s_tooltipColumns[Playlist::AlbumArtist] && albumArtist && albumArtist != artist )
        text += HTMLLine( Playlist::AlbumArtist, albumArtist->name() );

    if( s_tooltipColumns[Playlist::Album] && album )
        text += HTMLLine( Playlist::Album, album->name() );

    if( s_tooltipColumns[Playlist::DiscNumber] )
        text += HTMLLine( Playlist::DiscNumber, track->discNumber() );

    if( s_tooltipColumns[Playlist::TrackNumber] )
        text += HTMLLine( Playlist::TrackNumber, track->trackNumber() );

    if( s_tooltipColumns[Playlist::Composer] && composer )
        text += HTMLLine( Playlist::Composer, composer->name() );

    if( s_tooltipColumns[Playlist::Genre] && genre )
        text += HTMLLine( Playlist::Genre, genre->name() );

    if( s_tooltipColumns[Playlist::Year] && year && year->year() > 0 )
        text += HTMLLine( Playlist::Year, year->year() );

    if( s_tooltipColumns[Playlist::Bpm] )
        text += HTMLLine( Playlist::Bpm, track->bpm() );

    if( s_tooltipColumns[Playlist::Comment]) {
        if ( !(fitsInOneLineHTML( track->comment() ) ) )
            text += HTMLLine( Playlist::Comment, i18n( "(...)" ) );
        else
            text += HTMLLine( Playlist::Comment, track->comment() );
    }

    if( s_tooltipColumns[Playlist::Labels] && !track->labels().empty() )
    {
        QStringList labels;
        foreach( Meta::LabelPtr label, track->labels() )
        {
            if( label )
                labels << label->name();
        }
        text += HTMLLine( Playlist::Labels, labels.join( QString(", ") ) );
    }

    if( s_tooltipColumns[Playlist::Score] )
        text += HTMLLine( Playlist::Score, statistics->score() );

    if( s_tooltipColumns[Playlist::Rating] )
        text += HTMLLine( Playlist::Rating, QString::number( statistics->rating()/2.0 ) );

    if( s_tooltipColumns[Playlist::PlayCount] )
        text += HTMLLine( Playlist::PlayCount, statistics->playCount(), true );

    if( s_tooltipColumns[Playlist::LastPlayed] && statistics->lastPlayed().isValid() )
        text += HTMLLine( Playlist::LastPlayed, locale->formatDateTime( statistics->lastPlayed() ) );

    if( s_tooltipColumns[Playlist::Bitrate] && track->bitrate() )
        text += HTMLLine( Playlist::Bitrate, i18nc( "%1: bitrate", "%1 kbps", track->bitrate() ) );

    if( text.isEmpty() )
        text = QString( i18n( "No extra information available" ) );
    else
        text = QString("<table>"+ text +"</table>");

    return text;
}

QVariant
Playlist::Model::data( const QModelIndex& index, int role ) const
{
    int row = index.row();

    if ( !index.isValid() || !rowExists( row ) )
        return QVariant();

    if ( role == UniqueIdRole )
        return QVariant( idAt( row ) );

    else if ( role == ActiveTrackRole )
        return ( row == m_activeRow );

    else if ( role == TrackRole && m_items.at( row )->track() )
        return QVariant::fromValue( m_items.at( row )->track() );

    else if ( role == StateRole )
        return m_items.at( row )->state();

    else if ( role == QueuePositionRole )
        return Actions::instance()->queuePosition( idAt( row ) ) + 1;

    else if ( role == InCollectionRole )
        return  m_items.at( row )->track()->inCollection();

    else if ( role == MultiSourceRole )
        return  m_items.at( row )->track()->has<Capabilities::MultiSourceCapability>();

    else if ( role == StopAfterTrackRole )
        return Actions::instance()->willStopAfterTrack( idAt( row ) );

    else if ( role == Qt::ToolTipRole && s_showToolTip )
        return tooltipFor( m_items.at( row )->track() );

    else if ( role == Qt::DisplayRole )
    {
        Meta::AlbumPtr album = m_items.at( row )->track()->album();
        Meta::StatisticsPtr statistics = m_items.at( row )->track()->statistics();
        switch ( index.column() )
        {
            case PlaceHolder:
                break;
            case Album:
            {
                if( album )
                    return album->name();
                break;
            }
            case AlbumArtist:
            {
                if( album && album->albumArtist() )
                    return album->albumArtist()->name();
                break;
            }
            case Artist:
            {
                if ( m_items.at( row )->track()->artist() )
                    return m_items.at( row )->track()->artist()->name();
                break;
            }
            case Bitrate:
            {
                return Meta::prettyBitrate( m_items.at( row )->track()->bitrate() );
            }
            case Bpm:
            {
                if ( m_items.at( row )->track()->bpm() > 0.0 )
                    return QString::number( m_items.at( row )->track()->bpm() );
                break;
            }
            case Comment:
            {
                return m_items.at( row )->track()->comment();
            }
            case Composer:
            {
                if ( m_items.at( row )->track()->composer() )
                    return m_items.at( row )->track()->composer()->name();
                break;
            }
            case CoverImage:
            {
                if( album )
                    return The::svgHandler()->imageWithBorder( album, 100 ); //FIXME:size?
                break;
            }
            case Directory:
            {
                if ( m_items.at( row )->track()->playableUrl().isLocalFile() )
                    return m_items.at( row )->track()->playableUrl().directory();
                break;
            }
            case DiscNumber:
            {
                if( m_items.at( row )->track()->discNumber() > 0 )
                    return m_items.at( row )->track()->discNumber();
                break;
            }
            case Filename:
            {

                if ( m_items.at( row )->track()->playableUrl().isLocalFile() )
                    return m_items.at( row )->track()->playableUrl().fileName();
                break;
            }
            case Filesize:
            {
                return Meta::prettyFilesize( m_items.at( row )->track()->filesize() );
            }
            case Genre:
            {
                if ( m_items.at( row )->track()->genre() )
                    return m_items.at( row )->track()->genre()->name();
                break;
            }
            case GroupLength:
            {
                return Meta::secToPrettyTime( 0 );
            }
            case GroupTracks:
            {
                return QString();
            }
            case Labels:
            {
                Meta::TrackPtr track = m_items.at( row )->track();
                if( track )
                {
                    QStringList labelNames;
                    foreach( const Meta::LabelPtr &label, track->labels() )
                    {
                        labelNames << label->prettyName();
                    }
                    return labelNames.join( ", " );
                }
                break;
            }
            case LastPlayed:
            {
                if( statistics->lastPlayed().isValid() )
                    return Amarok::verboseTimeSince( statistics->lastPlayed() );
                else
                    return i18nc( "The amount of time since last played", "Never" );
            }
            case Length:
            {
                return Meta::msToPrettyTime( m_items.at( row )->track()->length() );
            }
            case LengthInSeconds:
            {
                return m_items.at( row )->track()->length() / 1000;
            }
            case Mood:
            {
                return QString(); //FIXME
            }
            case PlayCount:
            {
                return statistics->playCount();
            }
            case Rating:
            {
                return statistics->rating();
            }
            case SampleRate:
            {
                if( m_items.at( row )->track()->sampleRate() > 0 )
                    return m_items.at( row )->track()->sampleRate();
                break;
            }
            case Score:
            {
                return int( statistics->score() ); // Cast to int, as we don't need to show the decimals in the view..
            }
            case Source:
            {
                QString sourceName;
                Capabilities::SourceInfoCapability *sic = m_items.at( row )->track()->create<Capabilities::SourceInfoCapability>();
                if ( sic )
                {
                    sourceName = sic->sourceName();
                    delete sic;
                }
                else
                {
                    sourceName = m_items.at( row )->track()->collection() ? m_items.at( row )->track()->collection()->prettyName() : QString();
                }
                return sourceName;
            }
            case SourceEmblem:
            {
                QPixmap emblem;
                Capabilities::SourceInfoCapability *sic = m_items.at( row )->track()->create<Capabilities::SourceInfoCapability>();
                if ( sic )
                {
                    QString source = sic->sourceName();
                    if ( !source.isEmpty() )
                        emblem = sic->emblem();
                    delete sic;
                }
                return emblem;
            }
            case Title:
            {
                return m_items.at( row )->track()->prettyName();
            }
            case TitleWithTrackNum:
            {
                QString trackString;
                QString trackName = m_items.at( row )->track()->prettyName();
                if ( m_items.at( row )->track()->trackNumber() > 0 )
                {
                    QString trackNumber = QString::number( m_items.at( row )->track()->trackNumber() );
                    trackString =  QString( trackNumber + " - " + trackName );
                } else
                    trackString = trackName;

                return trackString;
            }
            case TrackNumber:
            {
                if( m_items.at( row )->track()->trackNumber() > 0 )
                    return m_items.at( row )->track()->trackNumber();
                break;
            }
            case Type:
            {
                return m_items.at( row )->track()->type();
            }
            case Year:
            {
                Meta::YearPtr year = m_items.at( row )->track()->year();
                if( year && year->year() > 0 )
                    return year->year();
                break;
            }
            default:
                return QVariant(); // returning a variant instead of a string inside a variant is cheaper

        }
    }
    // else
    return QVariant();
}

Qt::DropActions
Playlist::Model::supportedDropActions() const
{
    return Qt::MoveAction | QAbstractListModel::supportedDropActions();
}

Qt::ItemFlags
Playlist::Model::flags( const QModelIndex &index ) const
{
    if ( index.isValid() )
        return ( Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDropEnabled | Qt::ItemIsDragEnabled | Qt::ItemIsEditable );
    return Qt::ItemIsDropEnabled;
}

QStringList
Playlist::Model::mimeTypes() const
{
    QStringList ret = QAbstractListModel::mimeTypes();
    ret << AmarokMimeData::TRACK_MIME;
    ret << "text/uri-list"; //we do accept urls
    return ret;
}

QMimeData*
Playlist::Model::mimeData( const QModelIndexList &indexes ) const
{
    AmarokMimeData* mime = new AmarokMimeData();
    Meta::TrackList selectedTracks;

    foreach( const QModelIndex &it, indexes )
    selectedTracks << m_items.at( it.row() )->track();

    mime->setTracks( selectedTracks );
    return mime;
}

bool
Playlist::Model::dropMimeData( const QMimeData* data, Qt::DropAction action, int row, int, const QModelIndex& parent )
{
    DEBUG_BLOCK
    if ( action == Qt::IgnoreAction )
        return true;

    int beginRow;
    if ( row != -1 )
        beginRow = row;
    else if ( parent.isValid() )
        beginRow = parent.row();
    else
        beginRow = m_items.size();

    if( data->hasFormat( AmarokMimeData::TRACK_MIME ) )
    {
        debug() << "this is a track";
        const AmarokMimeData* trackListDrag = qobject_cast<const AmarokMimeData*>( data );
        if( trackListDrag )
        {

            Meta::TrackList tracks = trackListDrag->tracks();
            qStableSort( tracks.begin(), tracks.end(), Meta::Track::lessThan );

            The::playlistController()->insertTracks( beginRow, tracks );
        }
        return true;
    }
    else if( data->hasFormat( AmarokMimeData::PLAYLIST_MIME ) )
    {
        debug() << "this is a playlist";
        const AmarokMimeData* dragList = qobject_cast<const AmarokMimeData*>( data );
        if( dragList )
            The::playlistController()->insertPlaylists( beginRow, dragList->playlists() );
        return true;
    }
    else if( data->hasFormat( AmarokMimeData::PODCASTEPISODE_MIME ) )
    {
        debug() << "this is a podcast episode";
        const AmarokMimeData* dragList = qobject_cast<const AmarokMimeData*>( data );
        if( dragList )
        {
            Meta::TrackList tracks;
            foreach( Podcasts::PodcastEpisodePtr episode, dragList->podcastEpisodes() )
                tracks << Meta::TrackPtr::staticCast( episode );
            The::playlistController()->insertTracks( beginRow, tracks );
        }
        return true;
    }
    else if( data->hasFormat( AmarokMimeData::PODCASTCHANNEL_MIME ) )
    {
        debug() << "this is a podcast channel";
        const AmarokMimeData* dragList = qobject_cast<const AmarokMimeData*>( data );
        if( dragList )
        {
            Meta::TrackList tracks;
            foreach( Podcasts::PodcastChannelPtr channel, dragList->podcastChannels() )
                foreach( Podcasts::PodcastEpisodePtr episode, channel->episodes() )
                    tracks << Meta::TrackPtr::staticCast( episode );
            The::playlistController()->insertTracks( beginRow, tracks );
        }
        return true;
    }
    else if( data->hasUrls() )
    {
        debug() << "this is _something_ with a url....";
        DirectoryLoader* dl = new DirectoryLoader(); //this deletes itself
        dl->insertAtRow( beginRow );
        dl->init( data->urls() );
        return true;
    }

    debug() << "I have no idea what the hell this is...";
    return false;
}

void
Playlist::Model::setActiveRow( int row )
{
    if ( rowExists( row ) )
    {
        setStateOfRow( row, Item::Played );
        m_activeRow = row;
        emit activeTrackChanged( m_items.at( row )->id() );
    }
    else
    {
        m_activeRow = -1;
        emit activeTrackChanged( 0 );
    }
}

void
Playlist::Model::emitQueueChanged()
{
    emit queueChanged();
}

int
Playlist::Model::queuePositionOfRow( int row )
{
    return Actions::instance()->queuePosition( idAt( row ) ) + 1;
}

Playlist::Item::State
Playlist::Model::stateOfRow( int row ) const
{
    if ( rowExists( row ) )
        return m_items.at( row )->state();
    else
        return Item::Invalid;
}

bool
Playlist::Model::containsTrack( const Meta::TrackPtr track ) const
{
    foreach( Item* i, m_items )
    {
        if ( i->track() == track )
            return true;
    }
    return false;
}

int
Playlist::Model::firstRowForTrack( const Meta::TrackPtr track ) const
{
    int row = 0;
    foreach( Item* i, m_items )
    {
        if ( i->track() == track )
            return row;
        row++;
    }
    return -1;
}

QSet<int>
Playlist::Model::allRowsForTrack( const Meta::TrackPtr track ) const
{
    QSet<int> trackRows;

    int row = 0;
    foreach( Item* i, m_items )
    {
        if ( i->track() == track )
            trackRows.insert( row );
        row++;
    }
    return trackRows;
}

Meta::TrackPtr
Playlist::Model::trackAt( int row ) const
{
    if ( rowExists( row ) )
        return m_items.at( row )->track();
    else
        return Meta::TrackPtr();
}

Meta::TrackPtr
Playlist::Model::activeTrack() const
{
    if ( rowExists( m_activeRow ) )
        return m_items.at( m_activeRow )->track();
    else
        return Meta::TrackPtr();
}

int
Playlist::Model::rowForId( const quint64 id ) const
{
    return m_items.indexOf( m_itemIds.value( id ) );    // Returns -1 on miss, same as our API.
}

Meta::TrackPtr
Playlist::Model::trackForId( const quint64 id ) const
{
    Item* item = m_itemIds.value( id, 0 );
    if ( item )
        return item->track();
    else
        return Meta::TrackPtr();
}

quint64
Playlist::Model::idAt( const int row ) const
{
    if ( rowExists( row ) )
        return m_items.at( row )->id();
    else
        return 0;
}

quint64
Playlist::Model::activeId() const
{
    if ( rowExists( m_activeRow ) )
        return m_items.at( m_activeRow )->id();
    else
        return 0;
}

Playlist::Item::State
Playlist::Model::stateOfId( quint64 id ) const
{
    Item* item = m_itemIds.value( id, 0 );
    if ( item )
        return item->state();
    else
        return Item::Invalid;
}

void
Playlist::Model::metadataChanged( Meta::TrackPtr track )
{
    DEBUG_BLOCK;

    int row = 0;
    foreach( Item* i, m_items )
    {
        if ( i->track() == track )
        {
            // ensure that we really have the correct album subscribed (in case it changed)
            Meta::AlbumPtr album = track->album();
            if( album )
                subscribeTo( album );

            emit dataChanged( index( row, 0 ), index( row, columnCount() - 1 ) );
            debug()<<"Metadata updated for track"<<track->prettyName();
        }
        row++;
    }
}

void
Playlist::Model::metadataChanged( Meta::AlbumPtr album )
{
    // Mainly to get update about changed covers

    // -- search for all the tracks having this album
    bool found = false;
    const int size = m_items.size();
    for ( int i = 0; i < size; i++ )
    {
        if ( m_items.at( i )->track()->album() == album )
        {
            emit dataChanged( index( i, 0 ), index( i, columnCount() - 1 ) );
            found = true;
            debug()<<"Metadata updated for album"<<album->prettyName();
        }
    }

    // -- unsubscribe if we don't have a track from that album left.
    // this can happen if the album of a track changed
    if( !found )
        unsubscribeFrom( album );
}

bool
Playlist::Model::exportPlaylist( const QString &path, bool relative ) const
{
    // check queue state
    QQueue<quint64> queueIds = The::playlistActions()->queue();
    QList<int> queued;
    foreach( quint64 id, queueIds ) {
      queued << rowForId( id );
    }
    return Playlists::exportPlaylistFile( tracks(), path, relative, queued );
}

Meta::TrackList
Playlist::Model::tracks() const
{
    Meta::TrackList tl;
    foreach( Item* item, m_items )
        tl << item->track();
    return tl;
}

QString
Playlist::Model::prettyColumnName( Column index ) //static
{
    switch ( index )
    {
    case Filename:   return i18nc( "The name of the file this track is stored in", "Filename" );
    case Title:      return i18n( "Title" );
    case Artist:     return i18n( "Artist" );
    case AlbumArtist: return i18n( "Album Artist" );
    case Composer:   return i18n( "Composer" );
    case Year:       return i18n( "Year" );
    case Album:      return i18n( "Album" );
    case DiscNumber: return i18n( "Disc Number" );
    case TrackNumber: return i18nc( "The Track number for this item", "Track" );
    case Bpm:        return i18n( "BPM" );
    case Genre:      return i18n( "Genre" );
    case Comment:    return i18n( "Comment" );
    case Directory:  return i18nc( "The location on disc of this track", "Directory" );
    case Type:       return i18n( "Type" );
    case Length:     return i18n( "Length" );
    case Bitrate:    return i18n( "Bitrate" );
    case SampleRate: return i18n( "Sample Rate" );
    case Score:      return i18n( "Score" );
    case Rating:     return i18n( "Rating" );
    case PlayCount:  return i18n( "Play Count" );
    case LastPlayed: return i18nc( "Column name", "Last Played" );
    case Mood:       return i18n( "Mood" );
    case Filesize:   return i18n( "File Size" );
    default:         return "This is a bug.";
    }

}


////////////
//Private Methods
////////////

void
Playlist::Model::insertTracksCommand( const InsertCmdList& cmds )
{
    if ( cmds.size() < 1 )
        return;

    setAllNewlyAddedToUnplayed();

    int activeShift = 0;
    int min = m_items.size() + cmds.size();
    int max = 0;
    int begin = cmds.at( 0 ).second;
    foreach( const InsertCmd &ic, cmds )
    {
        min = qMin( min, ic.second );
        max = qMax( max, ic.second );
        activeShift += ( begin <= m_activeRow ) ? 1 : 0;
    }

    // actually do the insertion
    beginInsertRows( QModelIndex(), min, max );
    foreach( const InsertCmd &ic, cmds )
    {
        Meta::TrackPtr track = ic.first;
        m_totalLength += track->length();
        m_totalSize += track->filesize();
        subscribeTo( track );
        Meta::AlbumPtr album = track->album();
        if( album )
            subscribeTo( album );

        Item* newitem = new Item( track );
        m_items.insert( ic.second, newitem );
        m_itemIds.insert( newitem->id(), newitem );
    }
    endInsertRows();

    if( m_activeRow >= 0 )
        m_activeRow += activeShift;
    else
    {
        EngineController *engine = The::engineController();
        if( engine ) // test cases might create a playlist without having an EngineController
        {
            const Meta::TrackPtr engineTrack = engine->currentTrack();
            if( engineTrack )
            {
                int engineRow = firstRowForTrack( engineTrack );
                if( engineRow > -1 )
                    setActiveRow( engineRow );
            }
        }
    }
}


void
Playlist::Model::removeTracksCommand( const RemoveCmdList& cmds )
{
    DEBUG_BLOCK
    if ( cmds.size() < 1 )
        return;

    if ( cmds.size() == m_items.size() )
    {
        clearCommand();
        return;
    }

    int activeShift = 0;
    bool activeDeleted = false;
    foreach( const RemoveCmd &rc, cmds )
    {
        activeShift += ( rc.second < m_activeRow ) ? 1 : 0;
        if ( rc.second == m_activeRow )
            activeDeleted = true;
    }

    /* This next bit is probably more complicated that you expected it to be.
     * The reason for the complexity comes from the following:
     *
     * 1. Qt's Model/View architecture can handle removal of only consecutive rows
     * 2. The "remove rows" command from the Controller must handle
     *    non-consecutive rows, and the removal command probably isn't sorted
     *
     * So each item has to be removed individually, and you can't just iterate
     * over the commands, calling "m_items.removeAt(index)" as you go, because
     * the indices of m_items will change with every removeAt().  Thus the
     * following strategy of copying m_item, looking up the index in the copy and
     * removing the item from the original list. (The strategy was changed from
     * only replacing m_items at the end because that meant lying to the view -- Max)
     *
     * As a safety measure, the items themselves are not deleted until after m_items
     * has been replaced.  If you delete as you go, then m_items will be holding
     * dangling pointers, and the program will probably crash if the model is
     * accessed in this state.   -- stharward */

    QList<Item*> originalList(m_items); // copy the current item list
    QList<Item*> delitems;
    foreach( const RemoveCmd &rc, cmds )
    {
        Meta::TrackPtr track = rc.first;
        Item* item = originalList.at(rc.second);
        Q_ASSERT( track == item->track() );

        // -- remove the items from the lists
        int idx = rowForItem( item );
        if (idx != -1) {
            beginRemoveRows(QModelIndex(), idx, idx);
            delitems.append(item);
            m_items.removeAll( item );
            m_itemIds.remove( item->id() );
            // update totals here so they're right when endRemoveRows() called
            m_totalLength -= track->length();
            m_totalSize -= track->filesize();
            endRemoveRows();
        } else {
            error() << "tried to delete a non-existent item:" << rc.first->prettyName() << rc.second;
        }

        // -- unsubscribe
        if( !containsTrack( track ) ) // check against same track two times in playlist
        {
            unsubscribeFrom( track );

            Meta::AlbumPtr album = track->album();
            if( album )
            {
                // check if we are the last track in playlist with this album
                bool last = true;
                const int size = m_items.size();
                for ( int i = 0; i < size; i++ )
                {
                    if( m_items.at( i )->track()->album() == album )
                    {
                        last = false;
                        break;
                    }
                }
                if( last )
                    unsubscribeFrom( album );
            }
        }
    }

    qDeleteAll(delitems);
    delitems.clear();

    //update the active row
    if ( !activeDeleted && ( m_activeRow >= 0 ) )
    {
        m_activeRow = ( m_activeRow > 0 ) ? m_activeRow - activeShift : 0;
    }
    else
    {
        m_activeRow = -1;
    }

    // make sure that there are enough tracks if we just removed from a dynamic playlist.
    // This call needs to be delayed or else we would mess up the undo queue
    // BUG: 259675
    // FIXME: removing the track and normalizing the playlist should be grouped together
    //        so that an undo operation undos both.
    QTimer::singleShot(0, Playlist::Actions::instance(), SLOT(normalizeDynamicPlaylist()));
}


void Playlist::Model::clearCommand()
{
    setActiveRow( -1 );

    beginRemoveRows( QModelIndex(), 0, rowCount() - 1 );

    m_totalLength = 0;
    m_totalSize = 0;

    qDeleteAll( m_items );
    m_items.clear();
    m_itemIds.clear();

    endRemoveRows();
}


// Note: this function depends on 'MoveCmdList' to be a complete "cycle", in the sense
// that if row A is moved to row B, another row MUST be moved to row A.
// Very strange API design IMHO, because it forces our caller to e.g. move ALL ROWS in
// the playlist to move row 0 to the last row. This function should just have been
// equivalent to a 'removeTracks()' followed by an 'insertTracks()' IMHO.  --Nanno

void
Playlist::Model::moveTracksCommand( const MoveCmdList& cmds, bool reverse )
{
    DEBUG_BLOCK
    debug()<<"moveTracksCommand:"<<cmds.size()<<reverse;

    if ( cmds.size() < 1 )
        return;

    int min = m_items.size() + cmds.size();
    int max = 0;
    foreach( const MoveCmd &rc, cmds )
    {
        min = qMin( min, rc.first );
        min = qMin( min, rc.second );
        max = qMax( max, rc.first );
        max = qMax( max, rc.second );
    }

    int newActiveRow = m_activeRow;
    QList<Item*> oldItems( m_items );
    if ( reverse )
    {
        foreach( const MoveCmd &mc, cmds )
        {
            m_items[mc.first] = oldItems.at( mc.second );
            if ( m_activeRow == mc.second )
                newActiveRow = mc.first;
        }
    }
    else
    {
        foreach( const MoveCmd &mc, cmds )
        {
            m_items[mc.second] = oldItems.at( mc.first );
            if ( m_activeRow == mc.first )
                newActiveRow = mc.second;
        }
    }

    // We have 3 choices:
    //   - Call 'beginMoveRows()' / 'endMoveRows()'. Drawback: we'd need to do N of them, all causing resorts etc.
    //   - Emit 'layoutAboutToChange' / 'layoutChanged'. Drawback: unspecific, 'changePersistentIndex()' complications.
    //   - Emit 'dataChanged'. Drawback: a bit inappropriate. But not wrong.
    emit dataChanged( index( min, 0 ), index( max, columnCount() - 1 ) );

    //update the active row
    m_activeRow = newActiveRow;
}


// When doing a 'setStateOfItem_batch', we emit 1 crude 'dataChanged' signal. If we're
// unlucky, that signal may span many innocent rows that haven't changed at all.
// Although that "worst case" will cause unnecessary work in our listeners "upstream", it
// still has much better performance than the worst case of emitting very many tiny
// 'dataChanged' signals.
//
// Being more clever (coalesce multiple contiguous ranges, etc.) is not worth the effort.
void
Playlist::Model::setStateOfItem_batchStart()
{
    m_setStateOfItem_batchMinRow = rowCount() + 1;
    m_setStateOfItem_batchMaxRow = 0;
}

void
Playlist::Model::setStateOfItem_batchEnd()
{
    if ( ( m_setStateOfItem_batchMaxRow - m_setStateOfItem_batchMinRow ) >= 0 )
        emit dataChanged( index( m_setStateOfItem_batchMinRow, 0 ), index( m_setStateOfItem_batchMaxRow, columnCount() - 1 ) );

    m_setStateOfItem_batchMinRow = -1;
}

void
Playlist::Model::setStateOfItem( Item *item, int row, Item::State state )
{
    item->setState( state );

    if ( m_setStateOfItem_batchMinRow == -1 )    // If not in batch mode
        emit dataChanged( index( row, 0 ), index( row, columnCount() - 1 ) );
    else
    {
        m_setStateOfItem_batchMinRow = qMin( m_setStateOfItem_batchMinRow, row );
        m_setStateOfItem_batchMaxRow = qMax( m_setStateOfItem_batchMaxRow, row );
    }
}


// Unimportant TODO: the performance of this function is O(n) in playlist size.
// Not a big problem, because it's called infrequently.
// Can be fixed by maintaining a new member variable 'QMultiHash<Item::State, Item*>'.
void
Playlist::Model::setAllNewlyAddedToUnplayed()
{
    DEBUG_BLOCK

    setStateOfItem_batchStart();

    for ( int row = 0; row < rowCount(); row++ )
    {
        Item* item = m_items.at( row );
        if ( item->state() == Item::NewlyAdded )
            setStateOfItem( item, row, Item::Unplayed );
    }

    setStateOfItem_batchEnd();
}

void Playlist::Model::setAllUnplayed()
{
    DEBUG_BLOCK

    setStateOfItem_batchStart();

    for ( int row = 0; row < rowCount(); row++ )
    {
        Item* item = m_items.at( row );
        setStateOfItem( item, row, Item::Unplayed );
    }

    setStateOfItem_batchEnd();
}
