// SPDX-FileCopyrightText: 2009 Jens-Michael Hoffmann <jmho@c-xx.com>
//
// SPDX-License-Identifier: LGPL-2.1-or-later

#include "DownloadQueueSet.h"

#include "MarbleDebug.h"

#include "HttpJob.h"

namespace Marble
{

DownloadQueueSet::DownloadQueueSet(QObject *const parent)
    : QObject(parent)
{
}

DownloadQueueSet::DownloadQueueSet(DownloadPolicy const &policy, QObject *const parent)
    : QObject(parent)
    , m_downloadPolicy(policy)
{
}

DownloadQueueSet::~DownloadQueueSet()
{
    // todo: delete HttpJobs
}

DownloadPolicy DownloadQueueSet::downloadPolicy() const
{
    return m_downloadPolicy;
}

void DownloadQueueSet::setDownloadPolicy(DownloadPolicy const &policy)
{
    m_downloadPolicy = policy;
}

bool DownloadQueueSet::canAcceptJob(const QUrl &sourceUrl, const QString &destinationFileName) const
{
    if (jobIsQueued(destinationFileName)) {
        mDebug() << "Download rejected: It's in the queue already:" << destinationFileName;
        return false;
    }
    if (jobIsWaitingForRetry(destinationFileName)) {
        mDebug() << "Download rejected: Will try to download again in some time:" << destinationFileName;
        return false;
    }
    if (jobIsActive(destinationFileName)) {
        mDebug() << "Download rejected: It's being downloaded already:" << destinationFileName;
        return false;
    }
    if (jobIsBlackListed(sourceUrl)) {
        mDebug() << "Download rejected: Blacklisted.";
        return false;
    }
    return true;
}

void DownloadQueueSet::addJob(HttpJob *const job)
{
    m_jobs.push(job);
    mDebug() << "addJob: new job queue size:" << m_jobs.count();
    Q_EMIT jobAdded();
    Q_EMIT progressChanged(m_activeJobs.size(), m_jobs.count());
    activateJobs();
}

void DownloadQueueSet::activateJobs()
{
    while (!m_jobs.isEmpty() && m_activeJobs.count() < m_downloadPolicy.maximumConnections()) {
        HttpJob *const job = m_jobs.pop();
        activateJob(job);
    }
}

void DownloadQueueSet::retryJobs()
{
    while (!m_retryQueue.isEmpty()) {
        HttpJob *const job = m_retryQueue.dequeue();
        mDebug() << "Requeuing" << job->destinationFileName();
        // FIXME: addJob calls activateJobs every time
        addJob(job);
    }
}

void DownloadQueueSet::purgeJobs()
{
    // purge all waiting jobs
    while (!m_jobs.isEmpty()) {
        HttpJob *const job = m_jobs.pop();
        job->deleteLater();
    }

    // purge all retry jobs
    qDeleteAll(m_retryQueue);
    m_retryQueue.clear();

    // cancel all current jobs
    while (!m_activeJobs.isEmpty()) {
        deactivateJob(m_activeJobs.first());
    }

    Q_EMIT progressChanged(m_activeJobs.size(), m_jobs.count());
}

void DownloadQueueSet::finishJob(HttpJob *job, const QByteArray &data)
{
    mDebug() << job->sourceUrl() << job->destinationFileName();

    deactivateJob(job);
    Q_EMIT jobRemoved();
    Q_EMIT jobFinished(data, job->destinationFileName(), job->initiatorId());
    job->deleteLater();
    activateJobs();
}

void DownloadQueueSet::redirectJob(HttpJob *job, const QUrl &newSourceUrl)
{
    mDebug() << job->sourceUrl() << " -> " << newSourceUrl;

    deactivateJob(job);
    Q_EMIT jobRemoved();
    Q_EMIT jobRedirected(newSourceUrl, job->destinationFileName(), job->initiatorId(), job->downloadUsage());
    job->deleteLater();
}

void DownloadQueueSet::retryOrBlacklistJob(HttpJob *job, const int errorCode)
{
    Q_ASSERT(errorCode != 0);
    Q_ASSERT(!m_retryQueue.contains(job));

    deactivateJob(job);
    Q_EMIT jobRemoved();

    if (job->tryAgain()) {
        mDebug() << QStringLiteral("Download of %1 to %2 failed, but trying again soon").arg(job->sourceUrl().toString(), job->destinationFileName());
        m_retryQueue.enqueue(job);
        Q_EMIT jobRetry();
    } else {
        mDebug() << "JOB-address: " << job << "Blacklist-size:" << m_jobBlackList.size() << "err:" << errorCode;
        m_jobBlackList.insert(job->sourceUrl().toString());
        mDebug() << QStringLiteral(
                        "Download of %1 Blacklisted. "
                        "Number of blacklist items: %2")
                        .arg(job->destinationFileName())
                        .arg(m_jobBlackList.size());

        job->deleteLater();
    }
    activateJobs();
}

void DownloadQueueSet::activateJob(HttpJob *const job)
{
    m_activeJobs.push_back(job);
    Q_EMIT progressChanged(m_activeJobs.size(), m_jobs.count());

    connect(job, SIGNAL(jobDone(HttpJob *, int)), SLOT(retryOrBlacklistJob(HttpJob *, int)));
    connect(job, SIGNAL(redirected(HttpJob *, QUrl)), SLOT(redirectJob(HttpJob *, QUrl)));
    connect(job, SIGNAL(dataReceived(HttpJob *, QByteArray)), SLOT(finishJob(HttpJob *, QByteArray)));

    job->execute();
}

/**
   pre condition: - job is in m_activeJobs
                  - job's signal are connected to our slots
   post condition: - job is not in m_activeJobs anymore (and btw not
                     in any other queue)
                   - job's signals are disconnected from our slots
 */
void DownloadQueueSet::deactivateJob(HttpJob *const job)
{
    const bool disconnected = job->disconnect();
    Q_ASSERT(disconnected);
    Q_UNUSED(disconnected); // for Q_ASSERT in release mode
    const bool removed = m_activeJobs.removeOne(job);
    Q_ASSERT(removed);
    Q_UNUSED(removed); // for Q_ASSERT in release mode
    Q_EMIT progressChanged(m_activeJobs.size(), m_jobs.count());
}

bool DownloadQueueSet::jobIsActive(QString const &destinationFileName) const
{
    QList<HttpJob *>::const_iterator pos = m_activeJobs.constBegin();
    QList<HttpJob *>::const_iterator const end = m_activeJobs.constEnd();
    for (; pos != end; ++pos) {
        if ((*pos)->destinationFileName() == destinationFileName) {
            return true;
        }
    }
    return false;
}

inline bool DownloadQueueSet::jobIsQueued(QString const &destinationFileName) const
{
    return m_jobs.contains(destinationFileName);
}

bool DownloadQueueSet::jobIsWaitingForRetry(QString const &destinationFileName) const
{
    QList<HttpJob *>::const_iterator pos = m_retryQueue.constBegin();
    QList<HttpJob *>::const_iterator const end = m_retryQueue.constEnd();
    for (; pos != end; ++pos) {
        if ((*pos)->destinationFileName() == destinationFileName) {
            return true;
        }
    }
    return false;
}

bool DownloadQueueSet::jobIsBlackListed(const QUrl &sourceUrl) const
{
    QSet<QString>::const_iterator const pos = m_jobBlackList.constFind(sourceUrl.toString());
    return pos != m_jobBlackList.constEnd();
}

inline bool DownloadQueueSet::JobStack::contains(const QString &destinationFileName) const
{
    return m_jobsContent.contains(destinationFileName);
}

inline int DownloadQueueSet::JobStack::count() const
{
    return m_jobs.count();
}

inline bool DownloadQueueSet::JobStack::isEmpty() const
{
    return m_jobs.isEmpty();
}

inline HttpJob *DownloadQueueSet::JobStack::pop()
{
    HttpJob *const job = m_jobs.pop();
    bool const removed = m_jobsContent.remove(job->destinationFileName());
    Q_UNUSED(removed); // for Q_ASSERT in release mode
    Q_ASSERT(removed);
    return job;
}

inline void DownloadQueueSet::JobStack::push(HttpJob *const job)
{
    m_jobs.push(job);
    m_jobsContent.insert(job->destinationFileName());
}

}

#include "moc_DownloadQueueSet.cpp"
