/***************************************************************************
 *   Copyright © 2012 Aleix Pol Gonzalez <aleixpol@blue-systems.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/>. *
 ***************************************************************************/

#include "ApplicationUpdates.h"

// Qt includes
#include <QIcon>
#include <QAction>

// KDE includes
#include <KProtocolManager>
#include <KMessageBox>
#include <KActionCollection>
#include <KLocalizedString>
#include <KXmlGuiWindow>

// Own includes
#include <QApt/Transaction>

#include <ChangesDialog.h>
#include <MuonStrings.h>
#include <QAptActions.h>

#include "Application.h"
#include "ApplicationBackend.h"

ApplicationUpdates::ApplicationUpdates(ApplicationBackend* parent)
    : AbstractBackendUpdater(parent)
    , m_aptBackend(nullptr)
    , m_appBackend(parent)
    , m_lastRealProgress(0)
    , m_eta(0)
    , m_progressing(false)
{
    connect(m_appBackend, SIGNAL(fetchingChanged()), SLOT(fetchingChanged()));
}

bool ApplicationUpdates::hasUpdates() const
{
    return m_appBackend->updatesCount()>0;
}

qreal ApplicationUpdates::progress() const
{
    return m_lastRealProgress;
}

long unsigned int ApplicationUpdates::remainingTime() const
{
    return m_eta;
}

void ApplicationUpdates::setBackend(QApt::Backend* backend)
{
    Q_ASSERT(!m_aptBackend || m_aptBackend==backend);
    m_aptBackend = backend;
    // FIXME: Debconf support was lost during the port
    QApt::FrontendCaps caps = (QApt::FrontendCaps)(QApt::MediumPromptCap |
                              QApt::ConfigPromptCap | QApt::UntrustedPromptCap);
    m_aptBackend->setFrontendCaps(caps);
}

QList<AbstractResource*> ApplicationUpdates::toUpdate() const
{
    return m_toUpdate;
}

void ApplicationUpdates::restoreToCleanCache()
{
    if(!m_updatesCache.isEmpty())
        m_aptBackend->restoreCacheState(m_updatesCache);
    else {
        //this is my best bet for retrieving a clean cache, I'm unsure if there's a better way
        m_updatesCache = m_aptBackend->currentCacheState();
    }
    Q_ASSERT(m_aptBackend->markedPackages().isEmpty());
}

void ApplicationUpdates::prepare()
{
    restoreToCleanCache();

    m_aptBackend->markPackagesForDistUpgrade();
    calculateUpdates();
}

void ApplicationUpdates::start()
{
    Q_ASSERT(!m_updatesCache.isEmpty());
    auto changes = m_aptBackend->stateChanges(m_updatesCache, QApt::PackageList());
    if(changes.isEmpty()) {
        qWarning() << "couldn't find any apt updates";
        setProgressing(false);
        return;
    }
    for(auto it=changes.begin(); it!=changes.end(); ) {
        if(it.key()&QApt::Package::ToUpgrade) {
            it = changes.erase(it);
        } else {
            ++it;
        }
    }
    // Confirm additional changes beyond upgrading the files
    if(!changes.isEmpty()) {
        ChangesDialog d(m_appBackend->mainWindow(), changes);
        if(d.exec()==QDialog::Rejected) {
            setProgressing(false);
            return;
        }
    }
    
    // Create and run the transaction
    setupTransaction(m_aptBackend->commitChanges());
    m_trans->run();
    setProgressing(true);
}

void ApplicationUpdates::addResources(const QList<AbstractResource*>& apps)
{
    QList<QApt::Package*> packages;
    foreach(AbstractResource* res, apps) {
        Application* app = qobject_cast<Application*>(res);
        Q_ASSERT(app);
        packages += app->package();
    }
    m_aptBackend->markPackages(packages, QApt::Package::ToInstall);
}

void ApplicationUpdates::removeResources(const QList<AbstractResource*>& apps)
{
    QList<QApt::Package*> packages;
    foreach(AbstractResource* res, apps) {
        Application* app = qobject_cast<Application*>(res);
        Q_ASSERT(app);
        packages += app->package();
    }
    m_aptBackend->markPackages(packages, QApt::Package::ToKeep);
}

void ApplicationUpdates::setProgress(int progress)
{
    if (progress > 100)
        return;

    if (progress > m_lastRealProgress || progress<0) {
        m_lastRealProgress = progress;
        emit progressChanged((qreal)progress);
    }
}

void ApplicationUpdates::etaChanged(quint64 eta)
{
    if(m_eta != eta) {
        m_eta = eta;
        emit remainingTimeChanged();
    }
}

void ApplicationUpdates::installMessage(const QString& msg)
{
    setStatusMessage(msg);
}

void ApplicationUpdates::errorOccurred(QApt::ErrorCode error)
{
    if(error!=QApt::Success) {
        QAptActions::self()->displayTransactionError(error, m_trans);
        setProgressing(false);
    }
}

void ApplicationUpdates::setupTransaction(QApt::Transaction *trans)
{
    Q_ASSERT(trans);
    m_trans = trans;

    // Provide proxy/locale to the transaction
    if (KProtocolManager::proxyType() == KProtocolManager::ManualProxy) {
        trans->setProxy(KProtocolManager::proxyFor("http"));
    }

    trans->setLocale(QLatin1String(setlocale(LC_MESSAGES, 0)));

    connect(trans, SIGNAL(errorOccurred(QApt::ErrorCode)),
            SLOT(errorOccurred(QApt::ErrorCode)));
    connect(trans, SIGNAL(progressChanged(int)), SLOT(setProgress(int)));
    connect(trans, SIGNAL(statusDetailsChanged(QString)), SLOT(installMessage(QString)));
    connect(trans, SIGNAL(cancellableChanged(bool)), SIGNAL(cancelableChanged(bool)));
    connect(trans, SIGNAL(finished(QApt::ExitStatus)), trans, SLOT(deleteLater()));
    connect(trans, SIGNAL(statusChanged(QApt::TransactionStatus)),
            this, SLOT(statusChanged(QApt::TransactionStatus)));
    connect(trans, SIGNAL(mediumRequired(QString,QString)),
            this, SLOT(provideMedium(QString,QString)));
    connect(trans, SIGNAL(promptUntrusted(QStringList)),
            this, SLOT(untrustedPrompt(QStringList)));
    connect(trans, SIGNAL(configFileConflict(QString,QString)),
            this, SLOT(configFileConflict(QString,QString)));
    connect(trans, SIGNAL(downloadSpeedChanged(quint64)),
            this, SIGNAL(downloadSpeedChanged(quint64)));
    connect(trans, SIGNAL(finished(QApt::ExitStatus)),
            this, SLOT(transactionFinished(QApt::ExitStatus)));
}

void ApplicationUpdates::transactionFinished(QApt::ExitStatus )
{
    m_lastRealProgress = 0;
    m_updatesCache.clear();
    m_toUpdate.clear();
    m_appBackend->reload();
    setProgressing(false);
}


bool ApplicationUpdates::isAllMarked() const
{
    QApt::PackageList upgradeable = m_aptBackend->upgradeablePackages();
    int markedCount = m_aptBackend->packageCount(QApt::Package::ToUpgrade);
    return markedCount >= upgradeable.count();
}

QDateTime ApplicationUpdates::lastUpdate() const
{
    return m_aptBackend->timeCacheLastUpdated();
}

bool ApplicationUpdates::isCancelable() const
{
    return m_trans && m_trans->isCancellable();
}

bool ApplicationUpdates::isProgressing() const
{
    return m_progressing;
}

void ApplicationUpdates::provideMedium(const QString &label, const QString &medium)
{
    QString title = i18nc("@title:window", "Media Change Required");
    QString text = xi18nc("@label", "Please insert %1 into <filename>%2</filename>",
                         label, medium);

    KMessageBox::information(QAptActions::self()->mainWindow(), text, title);
    m_trans->provideMedium(medium);
}

void ApplicationUpdates::untrustedPrompt(const QStringList &untrustedPackages)
{
    QString title = i18nc("@title:window", "Warning - Unverified Software");
    QString text = xi18ncp("@label",
                          "The following piece of software cannot be verified. "
                          "<warning>Installing unverified software represents a "
                          "security risk, as the presence of unverifiable software "
                          "can be a sign of tampering.</warning> Do you wish to continue?",
                          "The following pieces of software cannot be verified. "
                          "<warning>Installing unverified software represents a "
                          "security risk, as the presence of unverifiable software "
                          "can be a sign of tampering.</warning> Do you wish to continue?",
                          untrustedPackages.size());
    int result = KMessageBox::warningContinueCancelList(QAptActions::self()->mainWindow(),
                 text, untrustedPackages, title);

    bool installUntrusted = (result == KMessageBox::Continue);
    m_trans->replyUntrustedPrompt(installUntrusted);
}

void ApplicationUpdates::configFileConflict(const QString &currentPath, const QString &newPath)
{
    QString title = i18nc("@title:window", "Configuration File Changed");
    QString text = xi18nc("@label Notifies a config file change",
                         "A new version of the configuration file "
                         "<filename>%1</filename> is available, but your version has "
                         "been modified. Would you like to keep your current version "
                         "or install the new version?", currentPath);

    KGuiItem useNew(i18nc("@action Use the new config file", "Use New Version"));
    KGuiItem useOld(i18nc("@action Keep the old config file", "Keep Old Version"));

    // TODO: diff current and new paths
    Q_UNUSED(newPath)

    int ret = KMessageBox::questionYesNo(QAptActions::self()->mainWindow(), text, title, useNew, useOld);

    m_trans->resolveConfigFileConflict(currentPath, (ret == KMessageBox::Yes));
}

void ApplicationUpdates::statusChanged(QApt::TransactionStatus status)
{
    switch (status) {
        case QApt::SetupStatus:
            setProgressing(true);
            setStatusMessage(i18nc("@info Status info, widget title",
                                        "Starting"));
            setProgress(-1);
            break;
        case QApt::AuthenticationStatus:
            setStatusMessage(i18nc("@info Status info, widget title",
                                        "Waiting for Authentication"));
            setProgress(-1);
            break;
        case QApt::WaitingStatus:
            setStatusMessage(i18nc("@info Status information, widget title",
                                        "Waiting"));
            setStatusDetail(i18nc("@info Status info",
                                        "Waiting for other transactions to finish"));
            setProgress(-1);
            break;
        case QApt::WaitingLockStatus:
            setStatusMessage(i18nc("@info Status information, widget title",
                                        "Waiting"));
            setStatusDetail(i18nc("@info Status info",
                                        "Waiting for other software managers to quit"));
            setProgress(-1);
            break;
        case QApt::WaitingMediumStatus:
            setStatusMessage(i18nc("@info Status information, widget title",
                                        "Waiting"));
            setStatusDetail(i18nc("@info Status info",
                                        "Waiting for required medium"));
            setProgress(-1);
            break;
        case QApt::WaitingConfigFilePromptStatus:
            setStatusMessage(i18nc("@info Status information, widget title",
                                        "Waiting"));
            setStatusDetail(i18nc("@info Status info",
                                        "Waiting for configuration file"));
            setProgress(-1);
            break;
        case QApt::RunningStatus:
            setStatusMessage(QString());
            setStatusDetail(QString());
            break;
        case QApt::LoadingCacheStatus:
            setStatusDetail(QString());
            setStatusMessage(i18nc("@info Status info",
                                        "Loading Software List"));
            break;
        case QApt::DownloadingStatus:
            switch (m_trans->role()) {
                case QApt::UpdateCacheRole:
                    setStatusMessage(i18nc("@info Status information, widget title",
                                                "Updating software sources"));
                    break;
                case QApt::DownloadArchivesRole:
                case QApt::CommitChangesRole:
                    setStatusMessage(i18nc("@info Status information, widget title",
                                                "Downloading Packages"));
                    break;
                default:
                    break;
            }
            break;
        case QApt::CommittingStatus:
            emit downloadSpeedChanged(-1);
            setStatusMessage(i18nc("@info Status information, widget title",
                                        "Applying Changes"));
            setStatusDetail(QString());
            break;
        case QApt::FinishedStatus:
            setProgress(100);
            setStatusMessage(i18nc("@info Status information, widget title",
                                        "Finished"));
            break;
    }
}

void ApplicationUpdates::setProgressing(bool progressing)
{
    if(progressing!=m_progressing) {
        m_progressing = progressing;
        emit progressingChanged(progressing);

        if(m_progressing)
            setProgress(-1);
        else
            restoreToCleanCache();
    }
}

void ApplicationUpdates::setStatusDetail(const QString& msg)
{
    if(m_statusDetail!=msg) {
        m_statusDetail = msg;
        emit statusDetailChanged(msg);
    }
}

void ApplicationUpdates::setStatusMessage(const QString& msg)
{
    if(m_statusMessage!=msg) {
        m_statusMessage = msg;
        emit statusMessageChanged(msg);
    }
}

QString ApplicationUpdates::statusDetail() const
{
    return m_statusDetail;
}

QString ApplicationUpdates::statusMessage() const
{
    return m_statusMessage;
}

void ApplicationUpdates::cancel()
{
    Q_ASSERT(m_trans->isCancellable());
    m_trans->cancel();
}

quint64 ApplicationUpdates::downloadSpeed() const
{
    return m_trans->downloadSpeed();
}

void ApplicationUpdates::fetchingChanged()
{
    if(m_appBackend && m_appBackend->isFetching())
        return;

    calculateUpdates();
    setProgressing(false);
}

void ApplicationUpdates::calculateUpdates()
{
    m_toUpdate.clear();
    auto changes = m_aptBackend->stateChanges(m_updatesCache, QApt::PackageList());
    for(auto pkgList : changes.values()) {
        for(QApt::Package* it : pkgList) {
            AbstractResource* res = m_appBackend->resourceByPackageName(it->name());
            if(!res) //If we couldn't find it by its name, try with
                res = m_appBackend->resourceByPackageName(QStringLiteral("%1:%2").arg(it->name()).arg(it->architecture()));

            if(res) {
                if (res->state() == Application::Upgradeable)
                    m_toUpdate += res;
            } else {
                qWarning() << "Couldn't find the package:" << it->name();
            }
            Q_ASSERT(res);
        }
    }
}

bool ApplicationUpdates::isMarked(AbstractResource* res) const
{
    Q_ASSERT(!res->backend()->isFetching());
    Application* app = qobject_cast<Application*>(res);
    Q_ASSERT(app);
    return app->package()->state() & QApt::Package::ToInstall;
}
