//
// This file is part of the Marble Desktop Globe.
//
// This program is free software licensed under the GNU LGPL. You can
// find a copy of this license in LICENSE.txt in the top directory of
// the source code.
//
// Copyright 2006-2007 Torsten Rahn <tackat@kde.org>"
// Copyright 2007      Inge Wallin  <ingwa@kde.org>"
// Copyright 2008,2009 Jens-Michael Hoffmann <jensmh@gmx.de>
//

#include "HttpDownloadManager.h"

#include <QtCore/QDebug>
#include <QtCore/QTimer>

#include "HttpJob.h"
#include "StoragePolicy.h"
#include "NetworkPlugin.h"
#include "PluginManager.h"


// #define MARBLE_DOWNLOAD_DEBUG

using namespace Marble;

// Time before a failed download job is requeued in ms
const quint32 requeueTime = 60000;

HttpDownloadManager::HttpDownloadManager( const QUrl& serverUrl,
                                          StoragePolicy *policy )
    : m_downloadEnabled( true ), //enabled for now
      m_activatedJobsLimit( 40 ),
      m_jobQueueLimit( 1000 ),
      m_serverUrl( serverUrl ),
      m_storagePolicy( policy ),
      m_networkPlugin( 0 )
{
      m_requeueTimer = new QTimer( this );
      m_requeueTimer->setInterval( requeueTime );
      connect( m_requeueTimer, SIGNAL( timeout() ), this, SLOT( requeue() ) );
}


HttpDownloadManager::~HttpDownloadManager()
{
    m_downloadEnabled = false;

    qDeleteAll( m_jobQueue );
    m_jobQueue.clear();

    // activated jobs have to be deleted using deleteLater()
    // because they may be connected to signals
    QList<HttpJob*>::const_iterator pos = m_activatedJobList.constBegin();
    QList<HttpJob*>::const_iterator const end = m_activatedJobList.constEnd();
    for (; pos != end; ++pos ) {
        (*pos)->deleteLater();
    }
    m_activatedJobList.clear();

    qDeleteAll( m_waitingQueue );
    m_waitingQueue.clear();

    delete m_storagePolicy;
    delete m_networkPlugin;
}

void HttpDownloadManager::setServerUrl( const QUrl& serverUrl )
{
    m_serverUrl = serverUrl;
}

void HttpDownloadManager::setJobQueueLimit( int jobQueueLimit )
{
    m_jobQueueLimit = jobQueueLimit;
}

void HttpDownloadManager::setActivatedJobsLimit( int activatedJobsLimit )
{
    m_activatedJobsLimit = activatedJobsLimit;
}

void HttpDownloadManager::setDownloadEnabled( const bool enable )
{
    m_downloadEnabled = enable;
}

StoragePolicy* HttpDownloadManager::storagePolicy() const
{
    return m_storagePolicy;
}

void HttpDownloadManager::addJob( HttpJob * job )
{
    if ( acceptJob( job ) ) {
        m_jobQueue.push( job );
        emit jobAdded();
        activateJobs();
    }
    else {
        job->deleteLater();
    }
}

void HttpDownloadManager::addJob( const QString& relativeUrlString, const QString &id )
{
    if ( !m_downloadEnabled )
        return;

    QUrl sourceUrl( m_serverUrl );
    QString path = sourceUrl.path();
    sourceUrl.setPath( path + relativeUrlString );

    HttpJob *job = createJob( sourceUrl, relativeUrlString, id );
    if ( job )
        addJob( job );
}

void HttpDownloadManager::addJob( const QUrl& sourceUrl, const QString& destFileName,
                                  const QString &id )
{
    if ( !m_downloadEnabled )
        return;

    HttpJob *job = createJob( sourceUrl, destFileName, id );
    if ( job )
        addJob( job );
}

bool HttpDownloadManager::acceptJob( HttpJob  *job )
{
    // We update the initiatorId as the previous initiator
    // likely doesn't exist anymore

    QStack<HttpJob*>::iterator j = m_jobQueue.begin();
    QStack<HttpJob*>::iterator const jEnd = m_jobQueue.end();
    for (; j != jEnd; ++j ) {
        if ( job->destinationFileName() == (*j)->destinationFileName() ) {
#ifdef MARBLE_DOWNLOAD_DEBUG            
            qDebug() << "Download rejected: It's in the queue already.";
#endif
            (*j)->setInitiatorId( job->initiatorId() );
            return false;
        }
    }
    
    QList<HttpJob*>::iterator i = m_waitingQueue.begin();
    QList<HttpJob*>::iterator iEnd = m_waitingQueue.end();
    for (; i != iEnd; ++i) {
	if ( job->destinationFileName() == (*i)->destinationFileName() ) {
#ifdef MARBLE_DOWNLOAD_DEBUG
	    qDebug() << "Download rejected: Will try to download again in some time.";
#endif        
	    (*i)->setInitiatorId( job->initiatorId() );
	    return false;
	}
    }

    i = m_activatedJobList.begin();
    iEnd = m_activatedJobList.end();
    for (; i != iEnd; ++i ) {
        if ( job->destinationFileName() == (*i)->destinationFileName() ) {
#ifdef MARBLE_DOWNLOAD_DEBUG
            qDebug() << "Download rejected: It's being downloaded already:" << job->destinationFileName();
#endif
            (*i)->setInitiatorId( job->initiatorId() );
            return false;
        }
    }

    QSet<QString>::const_iterator const pos = m_jobBlackList.find( job->sourceUrl().toString() );
    if ( pos != m_jobBlackList.end() ) {
#ifdef MARBLE_DOWNLOAD_DEBUG
        qDebug() << "Download rejected: Blacklisted.";
#endif
        return false;
    }

    return true;
}

void HttpDownloadManager::removeJob( HttpJob* job )
{
    int pos = m_activatedJobList.indexOf( job );

    if ( pos > 0 ) {
        m_activatedJobList.removeAt( pos );
//        qDebug() << "Removing: " << job->initiatorId();
        job->deleteLater();
    }
    emit jobRemoved();

    activateJobs();
}

void HttpDownloadManager::activateJob( HttpJob * const job )
{
    // qDebug() << "On activatedJobList: " << job->sourceUrl().toString()
    //          << job->destinationFileName();
    m_activatedJobList.push_back( job );

    // No duplicate connections please
    // FIXME: move calls to disconnect to the right place, here it just
    // unconditionally disconnects even if there was no connection before
    disconnect( job, SIGNAL( jobDone( Marble::HttpJob*, int ) ),
                this, SLOT( reportResult( Marble::HttpJob*, int ) ) );
    disconnect( job, SIGNAL( redirected( HttpJob *, QUrl ) ),
                this, SLOT( jobRedirected( HttpJob *, QUrl ) ) );
    disconnect( job, SIGNAL( dataReceived( HttpJob *, QByteArray ) ),
                this, SLOT( jobDataReceived( HttpJob *, QByteArray ) ) );
    connect( job, SIGNAL( jobDone( Marble::HttpJob*, int ) ),
             this, SLOT( reportResult( Marble::HttpJob*, int ) ) );
    connect( job, SIGNAL( redirected( HttpJob *, QUrl ) ),
             SLOT( jobRedirected( HttpJob *, QUrl ) ) );
    connect( job, SIGNAL( dataReceived( HttpJob *, QByteArray ) ),
             SLOT( jobDataReceived( HttpJob *, QByteArray ) ) );

    job->execute();
}

void HttpDownloadManager::activateJobs()
{
    while ( m_jobQueue.count() > 0
            && m_activatedJobList.count() < m_activatedJobsLimit )
    {
        HttpJob *job = m_jobQueue.pop();
        activateJob( job );
    }
}

void HttpDownloadManager::reportResult( HttpJob* job, int err )
{
    if ( err != 0 ) {
        int pos = m_activatedJobList.indexOf( job );

        if ( pos >= 0 ) {
            m_activatedJobList.removeAt( pos );
	    emit jobRemoved();

	    // This should always return true
	    if( !m_waitingQueue.contains( job ) ) {
		if( job->tryAgain() ) {
#ifdef MARBLE_DOWNLOAD_DEBUG
		    qDebug() << QString( "Download of %1 failed, but trying again soon" )
			.arg( job->destinationFileName() );
#endif
		    m_waitingQueue.enqueue( job );
                    if( !m_requeueTimer->isActive() )
                        m_requeueTimer->start();
		}
		else {
#ifdef MARBLE_DOWNLOAD_DEBUG
            qDebug() << "JOB-address: " << job << "Blacklist-size:" << m_jobBlackList.size() << "err:" << err;
		    m_jobBlackList.insert( job->sourceUrl().toString() );
		    qDebug() << QString( "Download of %1 Blacklisted. Number of blacklist items: %2" )
			.arg( job->destinationFileName() ).arg( m_jobBlackList.size() );
#endif
                    job->deleteLater();
		}
	    }
	}
    }
    else {
//         qDebug() << "HttpDownloadManager: Download Complete:"
//                  << job->destinationFileName() << job->initiatorId();
        emit downloadComplete( job->destinationFileName(), job->initiatorId() );
        removeJob( job );
    }
    
    activateJobs();
}

void HttpDownloadManager::requeue()
{
    m_requeueTimer->stop();
    while( !m_waitingQueue.isEmpty() ) {
	HttpJob* job = m_waitingQueue.dequeue();
#ifdef MARBLE_DOWNLOAD_DEBUG
	qDebug() << QString( "Requeuing %1." ).arg( job->destinationFileName() );
#endif
	addJob( job );
    }
}

HttpJob *HttpDownloadManager::createJob( const QUrl& sourceUrl, const QString& destFileName,
                                         const QString &id )
{
    if ( !m_networkPlugin ) {
        PluginManager pluginManager;
        QList<NetworkPlugin *> networkPlugins = pluginManager.createNetworkPlugins();
        if ( !networkPlugins.isEmpty() ) {
            // FIXME: not just take the first plugin, but use some configuration setting
            // take the first plugin and delete the rest
            m_networkPlugin = networkPlugins.takeFirst();
            qDeleteAll( networkPlugins );
            m_networkPlugin->setParent( this );
        }
        else {
            m_downloadEnabled = false;
            return 0;
        }
    }
    Q_ASSERT( m_networkPlugin );
    return m_networkPlugin->createJob( sourceUrl, destFileName, id );
}

void HttpDownloadManager::jobRedirected( HttpJob *job, QUrl newLocation )
{
#ifdef MARBLE_DOWNLOAD_DEBUG
    qDebug() << "jobRedirected" << job->sourceUrl() << " -> " << newLocation;
#endif
    HttpJob * const newJob = m_networkPlugin->createJob( newLocation, job->destinationFileName(),
                                                         job->initiatorId() );
    removeJob( job );
    activateJob( newJob );
}

void HttpDownloadManager::jobDataReceived( HttpJob *job, QByteArray data )
{
    emit downloadComplete( data, job->initiatorId() );
    const bool error = storagePolicy()
        && !storagePolicy()->updateFile( job->destinationFileName(), data );
    reportResult( job, error );
}

#include "HttpDownloadManager.moc"
