/****************************************************************************************
 * Copyright (c) 2007-2008 Maximilian Kossick <maximilian.kossick@googlemail.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/>.                           *
 ****************************************************************************************/

#define DEBUG_PREFIX "CollectionManager"

#include "core-impl/collections/support/CollectionManager.h"

#include "core/support/Debug.h"

#include "EngineController.h"
#include "core/capabilities/CollectionScanCapability.h"
#include "core/collections/Collection.h"
#include "core/collections/MetaQueryMaker.h"
#include "core/collections/support/SqlStorage.h"
#include "core/plugins/PluginManager.h"
#include "core/support/SmartPointerList.h"
#include "core-impl/meta/file/File.h"
#include "core-impl/meta/stream/Stream.h"
#include "core-impl/meta/timecode/TimecodeTrackProvider.h"

#include <QList>
#include <QMetaEnum>
#include <QMetaObject>
#include <QPair>
#include <QTimer>

#include <KBuildSycocaProgressDialog>
#include <KConfigGroup>
#include <KMessageBox>
#include <KPluginLoader>
#include <KPluginFactory>

#include <cstdlib>

typedef QPair<Collections::Collection*, CollectionManager::CollectionStatus> CollectionPair;

class SqlStorageWrapper : public SqlStorage
{
public:
    SqlStorageWrapper()
        : SqlStorage()
        , m_sqlStorage( 0 )
    {}

    virtual int sqlDatabasePriority() const { return ( m_sqlStorage ? m_sqlStorage->sqlDatabasePriority() : 0 ); }
    virtual QString type() const  { return ( m_sqlStorage ? m_sqlStorage->type() : "SqlStorageWrapper" ); }
    virtual QString escape( QString text ) const  { return ( m_sqlStorage ? m_sqlStorage->escape( text ) : text ); }
    virtual QStringList query( const QString &query )  { return ( m_sqlStorage ? m_sqlStorage->query( query ) : QStringList() ); }
    virtual int insert( const QString &statement, const QString &table )  { return ( m_sqlStorage ? m_sqlStorage->insert( statement, table ) : 0 ); }
    virtual QString boolTrue() const  { return ( m_sqlStorage ? m_sqlStorage->boolTrue() : "1" ); }
    virtual QString boolFalse() const  { return ( m_sqlStorage ? m_sqlStorage->boolFalse() : "0" ); }
    virtual QString idType() const  { return ( m_sqlStorage ? m_sqlStorage->idType() : "WRAPPER_NOT_IMPLEMENTED" ); }
    virtual QString textColumnType( int length ) const { return ( m_sqlStorage ? m_sqlStorage->textColumnType( length ) : "WRAPPER_NOT_IMPLEMENTED" ); }
    virtual QString exactTextColumnType( int length ) const { return ( m_sqlStorage ? m_sqlStorage->exactTextColumnType( length ) : "WRAPPER_NOT_IMPLEMENTED" ); }
    virtual QString exactIndexableTextColumnType( int length ) const { return ( m_sqlStorage ? m_sqlStorage->exactIndexableTextColumnType( length ) : "WRAPPER_NOT_IMPLEMENTED" ); };
    virtual QString longTextColumnType() const { return ( m_sqlStorage ? m_sqlStorage->longTextColumnType() : "WRAPPER_NOT_IMPLEMENTED" ); }
    virtual QString randomFunc() const { return ( m_sqlStorage ? m_sqlStorage->randomFunc() : "WRAPPER_NOT_IMPLEMENTED" ); }

    void setSqlStorage( SqlStorage *sqlStorage ) { m_sqlStorage = sqlStorage; }
private:
    SqlStorage *m_sqlStorage;
};

struct CollectionManager::Private
{
    QList<CollectionPair> collections;
    SmartPointerList<Collections::CollectionFactory> factories;
    SqlStorage *sqlDatabase;
    SqlStorageWrapper *sqlStorageWrapper;
    QList<Collections::Collection*> unmanagedCollections;
    SmartPointerList<Collections::Collection> managedCollections;
    QList<Collections::TrackProvider*> trackProviders;
    Collections::Collection *primaryCollection;
};

CollectionManager* CollectionManager::s_instance = 0;

CollectionManager *
CollectionManager::instance()
{
    return s_instance ? s_instance : new CollectionManager();
}

void
CollectionManager::destroy()
{
    if (s_instance) {
        delete s_instance;
        s_instance = 0;
    }
}

CollectionManager::CollectionManager()
    : QObject()
    , d( new Private )
{
    qRegisterMetaType<SqlStorage *>( "SqlStorage*" );
    d->sqlDatabase = 0;
    d->primaryCollection = 0;
    d->sqlStorageWrapper = new SqlStorageWrapper();
    s_instance = this;
    m_haveEmbeddedMysql = false;

    init();
}

CollectionManager::~CollectionManager()
{
    DEBUG_BLOCK

    //not deleting SqlStorageWrapper here as somebody might be caching it
    //Amarok really needs a proper state management...
    d->sqlStorageWrapper->setSqlStorage( 0 );
    delete m_timecodeTrackProvider;
    d->collections.clear();
    d->unmanagedCollections.clear();
    d->trackProviders.clear();
    qDeleteAll( d->managedCollections );
    qDeleteAll( d->factories );


    delete d;
}

void
CollectionManager::init()
{
    DEBUG_BLOCK

    //register the timecode track provider now, as it needs to get added before loading
    //the stored playlist... Since it can have playable urls that might also match other providers, it needs to get added first.
    m_timecodeTrackProvider = new TimecodeTrackProvider();
    addTrackProvider( m_timecodeTrackProvider );

    KService::List plugins = Plugins::PluginManager::query( "[X-KDE-Amarok-plugintype] == 'collection'" );
    debug() << "Received [" << QString::number( plugins.count() ) << "] collection plugin offers";

    if( plugins.isEmpty() )
    {
        debug() << "No Amarok plugins found, running kbuildsycoca4.";
        KBuildSycocaProgressDialog::rebuildKSycoca( 0 );

        plugins = Plugins::PluginManager::query( "[X-KDE-Amarok-plugintype] == 'collection'" );
        debug() << "Second attempt: Received [" << QString::number( plugins.count() ) << "] collection plugin offers";

        if( plugins.isEmpty() )
        {
            KMessageBox::error( 0, i18n(
                    "<p>Amarok could not find any collection plugins. "
                    "It is possible that Amarok is installed under the wrong prefix, please fix your installation using:<pre>"
                    "$ cd /path/to/amarok/source-code/<br>"
                    "$ su -c \"make uninstall\"<br>"
                    "$ cmake -DCMAKE_INSTALL_PREFIX=`kde4-config --prefix` && su -c \"make install\"<br>"
                    "$ kbuildsycoca4 --noincremental<br>"
                    "$ amarok</pre>"
                    "More information can be found in the README file. For further assistance join us at #amarok on irc.freenode.net.</p>" ) );
            // don't use QApplication::exit, as the eventloop may not have started yet
            std::exit( EXIT_SUCCESS );
        }
    }

    KService::List orderedPlugins;
    const bool useMySqlServer = Amarok::config( "MySQL" ).readEntry( "UseServer", false );
    foreach( const KService::Ptr &service, plugins )
    {
        const QString name = service->property( "X-KDE-Amarok-name" ).toString();
        if( name == "mysqlserver-collection" )
        {
            if( useMySqlServer )
                orderedPlugins.prepend( service );
        }
        else if( name == "mysqle-collection" )
        {
            if( !useMySqlServer )
                orderedPlugins.prepend( service );
        }
        else
        {
            orderedPlugins.append( service );
        }
    }
    loadServices( orderedPlugins );
}

void
CollectionManager::loadServices( const KService::List &services )
{
    DEBUG_BLOCK
    foreach( const KService::Ptr &service, services )
    {
        const QString name( service->property( "X-KDE-Amarok-name" ).toString() );
        KPluginLoader loader( *( service.constData() ) );
        KPluginFactory *pluginFactory = loader.factory();
        if( pluginFactory )
        {
            Collections::CollectionFactory* factory( 0 );
            if( (factory = pluginFactory->create<Collections::CollectionFactory>( this )) )
            {
                connect( factory, SIGNAL(newCollection(Collections::Collection*)),
                         this, SLOT(slotNewCollection(Collections::Collection*)) );
                d->factories.append( factory );
                debug() << "Initialising" << name;
                factory->init();
                if( name == "mysqle-collection" )
                    m_haveEmbeddedMysql = true;
            }
            else
            {
                debug() << QString( "Plugin '%1' has wrong factory class: %2" )
                                             .arg( name, loader.errorString() );
            }
        }
        else
        {
            warning() << QString( "Failed to get factory '%1' from KPluginLoader: %2" )
                                                     .arg( name, loader.errorString() );
        }
    }
}

void
CollectionManager::startFullScan()
{
    foreach( const CollectionPair &pair, d->collections )
    {
        QScopedPointer<Capabilities::CollectionScanCapability> csc( pair.first->create<Capabilities::CollectionScanCapability>());
        if( csc )
            csc->startFullScan();
    }
}

void
CollectionManager::startIncrementalScan( const QString &directory )
{
    foreach( const CollectionPair &pair, d->collections )
    {
        QScopedPointer<Capabilities::CollectionScanCapability> csc( pair.first->create<Capabilities::CollectionScanCapability>());
        if( csc )
            csc->startIncrementalScan( directory );
    }
}

void
CollectionManager::stopScan()
{
    foreach( const CollectionPair &pair, d->collections )
    {
        QScopedPointer<Capabilities::CollectionScanCapability> csc( pair.first->create<Capabilities::CollectionScanCapability>());
        if( csc )
            csc->stopScan();
    }
}

void
CollectionManager::checkCollectionChanges()
{
    startIncrementalScan( QString() );
}

Collections::QueryMaker*
CollectionManager::queryMaker() const
{
    QList<Collections::Collection*> colls;
    foreach( const CollectionPair &pair, d->collections )
    {
        if( pair.second & CollectionQueryable )
        {
            colls << pair.first;
        }
    }
    return new Collections::MetaQueryMaker( colls );
}

void
CollectionManager::slotNewCollection( Collections::Collection* newCollection )
{
    DEBUG_BLOCK
    if( !newCollection )
    {
        debug() << "Warning, newCollection in slotNewCollection is 0";
        return;
    }
    foreach( CollectionPair p, d->collections )
    {
        if( p.first == newCollection )
        {
            debug() << "Warning, newCollection is already being managed";
            return;
        }
    }

    const QMetaObject *mo = metaObject();
    const QMetaEnum me = mo->enumerator( mo->indexOfEnumerator( "CollectionStatus" ) );
    const QString &value = KGlobal::config()->group( "CollectionManager" ).readEntry( newCollection->collectionId() );
    int enumValue = me.keyToValue( value.toLocal8Bit().constData() );
    CollectionStatus status;
    enumValue == -1 ? status = CollectionEnabled : status = (CollectionStatus) enumValue;
    CollectionPair pair( newCollection, status );
    d->collections.append( pair );
    d->managedCollections.append( newCollection );
    d->trackProviders.append( newCollection );
    connect( newCollection, SIGNAL( remove() ), SLOT( slotRemoveCollection() ), Qt::QueuedConnection );
    connect( newCollection, SIGNAL( updated() ), SLOT( slotCollectionChanged() ), Qt::QueuedConnection );
    //by convention, collections that provide a SQL database have a Qt property called "sqlStorage"
    int propertyIndex = newCollection->metaObject()->indexOfProperty( "sqlStorage" );
    if( propertyIndex != -1 )
    {
        SqlStorage *sqlStorage = newCollection->property( "sqlStorage" ).value<SqlStorage*>();
        if( sqlStorage )
        {
            //let's cheat a bit and assume that sqlStorage and the primaryCollection are always the same
            //it is true for now anyway
            if( d->sqlDatabase )
            {
                if( d->sqlDatabase->sqlDatabasePriority() < sqlStorage->sqlDatabasePriority() )
                {
                    d->sqlDatabase = sqlStorage;
                    d->primaryCollection = newCollection;
                    d->sqlStorageWrapper->setSqlStorage( sqlStorage );
                }
            }
            else
            {
                d->sqlDatabase = sqlStorage;
                d->primaryCollection = newCollection;
                d->sqlStorageWrapper->setSqlStorage( sqlStorage );
            }
        }
        else
        {
            warning() << "Collection " << newCollection->collectionId() << " has sqlStorage property but did not provide a SqlStorage pointer";
        }
    }
    if( status & CollectionViewable )
    {
        emit collectionAdded( newCollection );
        emit collectionAdded( newCollection, status );
    }
}

void
CollectionManager::slotRemoveCollection()
{
    Collections::Collection* collection = qobject_cast<Collections::Collection*>( sender() );
    if( collection )
    {
        CollectionStatus status = collectionStatus( collection->collectionId() );
        CollectionPair pair( collection, status );
        d->collections.removeAll( pair );
        d->managedCollections.removeAll( collection );
        d->trackProviders.removeAll( collection );
        QVariant v = collection->property( "sqlStorage" );
        if( v.isValid() )
        {
            SqlStorage *sqlDb = v.value<SqlStorage*>();
            if( sqlDb && sqlDb == d->sqlDatabase )
            {
                SqlStorage *newSqlDatabase = 0;
                foreach( const CollectionPair &pair, d->collections )
                {
                    QVariant variant = pair.first->property( "sqlStorage" );
                    if( !variant.isValid() )
                        continue;
                    SqlStorage *sqlDb = variant.value<SqlStorage*>();
                    if( sqlDb )
                    {
                        if( newSqlDatabase )
                        {
                            if( newSqlDatabase->sqlDatabasePriority() < sqlDb->sqlDatabasePriority() )
                                newSqlDatabase = sqlDb;
                        }
                        else
                            newSqlDatabase = sqlDb;
                    }
                }
                d->sqlDatabase = newSqlDatabase;
                d->sqlStorageWrapper->setSqlStorage( newSqlDatabase );
            }
        }
        emit collectionRemoved( collection->collectionId() );
        QTimer::singleShot( 0, collection, SLOT( deleteLater() ) );
    }
}

void
CollectionManager::slotCollectionChanged()
{
    Collections::Collection *collection = dynamic_cast<Collections::Collection*>( sender() );
    if( collection )
    {
        CollectionStatus status = collectionStatus( collection->collectionId() );
        if( status & CollectionViewable )
        {
            emit collectionDataChanged( collection );
        }
    }
}

QList<Collections::Collection*>
CollectionManager::viewableCollections() const
{
    QList<Collections::Collection*> result;
    foreach( const CollectionPair &pair, d->collections )
    {
        if( pair.second & CollectionViewable )
        {
            result << pair.first;
        }
    }
    return result;
}

QList<Collections::Collection*>
CollectionManager::queryableCollections() const
{
    QList<Collections::Collection*> result;
    foreach( const CollectionPair &pair, d->collections )
        if( pair.second & CollectionQueryable )
            result << pair.first;
    return result;

}

Collections::Collection*
CollectionManager::primaryCollection() const
{
    return d->primaryCollection;
}

SqlStorage*
CollectionManager::sqlStorage() const
{
    return d->sqlStorageWrapper;
}

Meta::TrackList
CollectionManager::tracksForUrls( const KUrl::List &urls )
{
    DEBUG_BLOCK

    debug() << "adding " << urls.size() << " tracks";

    Meta::TrackList tracks;
    foreach( const KUrl &url, urls )
    {
        Meta::TrackPtr track = trackForUrl( url );
        if( track )
            tracks.append( track );
    }
    return tracks;
}

Meta::TrackPtr
CollectionManager::trackForUrl( const KUrl &url )
{
    //TODO method stub
    //check all collections
    //might be a podcast, in that case we'll have additional meta information
    //might be a lastfm track, another stream
    //or a file which is not in any collection
    if( !url.isValid() )
        return Meta::TrackPtr( 0 );

    foreach( Collections::TrackProvider *provider, d->trackProviders )
    {
        if( provider->possiblyContainsTrack( url ) )
        {
            Meta::TrackPtr track = provider->trackForUrl( url );
            if( track )
                return track;
        }
    }

    if( url.protocol() == QLatin1String("http") || url.protocol() == QLatin1String("mms") ||
        url.protocol() == QLatin1String("smb") )
        return Meta::TrackPtr( new MetaStream::Track( url ) );

    if( url.protocol() == QLatin1String("file") && EngineController::canDecode( url ) )       
        return Meta::TrackPtr( new MetaFile::Track( url ) );

    return Meta::TrackPtr( 0 );
}

void
CollectionManager::slotArtistQueryResult( QString collectionId, Meta::ArtistList artists )
{
    Q_UNUSED(collectionId);

    foreach( Meta::ArtistPtr artist, artists )
    {
        if( !m_artistNameSet.contains( artist->name() ) )
        {
            m_resultArtistList.append( artist );
            m_artistNameSet.insert( artist->name() );
            if( m_resultArtistList.size() == m_maxArtists )
            {
                m_resultEmitted = true;
                emit( foundRelatedArtists( m_resultArtistList ) );
                return;
            }
        }
    }
    if( m_resultArtistList.size() == m_maxArtists && !m_resultEmitted )
    {
        m_resultEmitted = true;
        emit( foundRelatedArtists( m_resultArtistList ) );
    }
}

void
CollectionManager::addUnmanagedCollection( Collections::Collection *newCollection, CollectionStatus defaultStatus )
{
    if( newCollection && d->unmanagedCollections.indexOf( newCollection ) == -1 )
    {
        const QMetaObject *mo = metaObject();
        const QMetaEnum me = mo->enumerator( mo->indexOfEnumerator( "CollectionStatus" ) );
        const QString &value = KGlobal::config()->group( "CollectionManager" ).readEntry( newCollection->collectionId() );
        int enumValue = me.keyToValue( value.toLocal8Bit().constData() );
        CollectionStatus status;
        enumValue == -1 ? status = defaultStatus : status = (CollectionStatus) enumValue;
        d->unmanagedCollections.append( newCollection );
        CollectionPair pair( newCollection, status );
        d->collections.append( pair );
        d->trackProviders.append( newCollection );
        if( status & CollectionViewable )
        {
            emit collectionAdded( newCollection );
            emit collectionAdded( newCollection, status );
        }
        emit trackProviderAdded( newCollection );
    }
}

void
CollectionManager::removeUnmanagedCollection( Collections::Collection *collection )
{
    //do not remove it from collection if it is not in unmanagedCollections
    if( collection && d->unmanagedCollections.removeAll( collection ) )
    {
        CollectionPair pair( collection, collectionStatus( collection->collectionId() ) );
        d->collections.removeAll( pair );
        d->trackProviders.removeAll( collection );
        emit collectionRemoved( collection->collectionId() );
    }
}

void
CollectionManager::setCollectionStatus( const QString &collectionId, CollectionStatus status )
{
    foreach( const CollectionPair &pair, d->collections )
    {
        if( pair.first->collectionId() == collectionId )
        {
            if( ( pair.second & CollectionViewable ) &&
               !( status & CollectionViewable ) )
            {
                emit collectionRemoved( collectionId );
            }
            else if( ( pair.second & CollectionQueryable ) &&
                    !( status & CollectionViewable ) )
            {
                emit collectionAdded( pair.first );
                emit collectionAdded( pair.first, pair.second );
            }
            CollectionPair &pair2 = const_cast<CollectionPair&>( pair );
            pair2.second = status;
            const QMetaObject *mo = metaObject();
            const QMetaEnum me = mo->enumerator( mo->indexOfEnumerator( "CollectionStatus" ) );
            KGlobal::config()->group( "CollectionManager" ).writeEntry( collectionId, me.valueToKey( status ) );
            break;
        }
    }
}

CollectionManager::CollectionStatus
CollectionManager::collectionStatus( const QString &collectionId ) const
{
    foreach( const CollectionPair &pair, d->collections )
    {
        if( pair.first->collectionId() == collectionId )
        {
            return pair.second;
        }
    }
    return CollectionDisabled;
}

QHash<Collections::Collection*, CollectionManager::CollectionStatus>
CollectionManager::collections() const
{
    QHash<Collections::Collection*, CollectionManager::CollectionStatus> result;
    foreach( const CollectionPair &pair, d->collections )
    {
        result.insert( pair.first, pair.second );
    }
    return result;
}

void
CollectionManager::addTrackProvider( Collections::TrackProvider *provider )
{
    d->trackProviders.append( provider );
    emit trackProviderAdded( provider );
}

void
CollectionManager::removeTrackProvider( Collections::TrackProvider *provider )
{
    d->trackProviders.removeAll( provider );
}

#include "CollectionManager.moc"

