/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator.
**
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
****************************************************************************/

#include "settingspage.h"
#include "updateinfoplugin.h"

#include <coreplugin/actionmanager/actioncontainer.h>
#include <coreplugin/actionmanager/actionmanager.h>
#include <coreplugin/coreconstants.h>
#include <coreplugin/icore.h>
#include <coreplugin/settingsdatabase.h>
#include <coreplugin/shellcommand.h>
#include <utils/algorithm.h>
#include <utils/fileutils.h>
#include <utils/infobar.h>
#include <utils/qtcassert.h>
#include <utils/qtcprocess.h>

#include <QDate>
#include <QDomDocument>
#include <QFile>
#include <QFileInfo>
#include <QLabel>
#include <QLoggingCategory>
#include <QMenu>
#include <QMetaEnum>
#include <QPointer>
#include <QProcessEnvironment>
#include <QTimer>
#include <QVersionNumber>

Q_LOGGING_CATEGORY(log, "qtc.updateinfo", QtWarningMsg)

const char UpdaterGroup[] = "Updater";
const char MaintenanceToolKey[] = "MaintenanceTool";
const char AutomaticCheckKey[] = "AutomaticCheck";
const char CheckForNewQtVersionsKey[] = "CheckForNewQtVersions";
const char CheckIntervalKey[] = "CheckUpdateInterval";
const char LastCheckDateKey[] = "LastCheckDate";
const char LastMaxQtVersionKey[] = "LastMaxQtVersion";
const quint32 OneMinute = 60000;
const quint32 OneHour = 3600000;
const char InstallUpdates[] = "UpdateInfo.InstallUpdates";
const char InstallQtUpdates[] = "UpdateInfo.InstallQtUpdates";

using namespace Core;

namespace UpdateInfo {
namespace Internal {

class UpdateInfoPluginPrivate
{
public:
    QString m_maintenanceTool;
    QPointer<ShellCommand> m_checkUpdatesCommand;
    QPointer<FutureProgress> m_progress;
    QString m_collectedOutput;
    QTimer *m_checkUpdatesTimer = nullptr;

    struct Settings
    {
        bool automaticCheck = true;
        UpdateInfoPlugin::CheckUpdateInterval checkInterval = UpdateInfoPlugin::WeeklyCheck;
        bool checkForQtVersions = true;
    };
    Settings m_settings;
    QDate m_lastCheckDate;
    QVersionNumber m_lastMaxQtVersion;
};

UpdateInfoPlugin::UpdateInfoPlugin()
    : d(new UpdateInfoPluginPrivate)
{
    d->m_checkUpdatesTimer = new QTimer(this);
    d->m_checkUpdatesTimer->setTimerType(Qt::VeryCoarseTimer);
    d->m_checkUpdatesTimer->setInterval(OneHour);
    connect(d->m_checkUpdatesTimer, &QTimer::timeout,
            this, &UpdateInfoPlugin::doAutoCheckForUpdates);
}

UpdateInfoPlugin::~UpdateInfoPlugin()
{
    stopCheckForUpdates();
    if (!d->m_maintenanceTool.isEmpty())
        saveSettings();

    delete d;
}

void UpdateInfoPlugin::startAutoCheckForUpdates()
{
    doAutoCheckForUpdates();

    d->m_checkUpdatesTimer->start();
}

void UpdateInfoPlugin::stopAutoCheckForUpdates()
{
    d->m_checkUpdatesTimer->stop();
}

void UpdateInfoPlugin::doAutoCheckForUpdates()
{
    if (d->m_checkUpdatesCommand)
        return; // update task is still running (might have been run manually just before)

    if (nextCheckDate().isValid() && nextCheckDate() > QDate::currentDate())
        return; // not a time for check yet

    startCheckForUpdates();
}

void UpdateInfoPlugin::startCheckForUpdates()
{
    stopCheckForUpdates();

    d->m_checkUpdatesCommand = new ShellCommand({}, Utils::Environment::systemEnvironment());
    d->m_checkUpdatesCommand->setDisplayName(tr("Checking for Updates"));
    connect(d->m_checkUpdatesCommand, &ShellCommand::stdOutText, this, &UpdateInfoPlugin::collectCheckForUpdatesOutput);
    connect(d->m_checkUpdatesCommand, &ShellCommand::finished, this, &UpdateInfoPlugin::checkForUpdatesFinished);
    d->m_checkUpdatesCommand->addJob({Utils::FilePath::fromString(d->m_maintenanceTool),
                                      {"ch", "-g", "*=false,ifw.package.*=true"}},
                                     60 * 3, // 3 minutes timeout
                                     /*workingDirectory=*/{},
                                     [](int /*exitCode*/) {
                                         return Utils::QtcProcess::FinishedWithSuccess;
                                     });
    if (d->m_settings.checkForQtVersions) {
        d->m_checkUpdatesCommand
            ->addJob({Utils::FilePath::fromString(d->m_maintenanceTool),
                      {"se", "qt[.]qt[0-9][.][0-9]+$", "-g", "*=false,ifw.package.*=true"}},
                     60 * 3, // 3 minutes timeout
                     /*workingDirectory=*/{},
                     [](int /*exitCode*/) { return Utils::QtcProcess::FinishedWithSuccess; });
    }
    d->m_checkUpdatesCommand->execute();
    d->m_progress = d->m_checkUpdatesCommand->futureProgress();
    if (d->m_progress) {
        d->m_progress->setKeepOnFinish(FutureProgress::KeepOnFinishTillUserInteraction);
        d->m_progress->setSubtitleVisibleInStatusBar(true);
    }
    emit checkForUpdatesRunningChanged(true);
}

void UpdateInfoPlugin::stopCheckForUpdates()
{
    if (!d->m_checkUpdatesCommand)
        return;

    d->m_collectedOutput.clear();
    d->m_checkUpdatesCommand->disconnect();
    d->m_checkUpdatesCommand->cancel();
    d->m_checkUpdatesCommand = nullptr;
    emit checkForUpdatesRunningChanged(false);
}

void UpdateInfoPlugin::collectCheckForUpdatesOutput(const QString &contents)
{
    d->m_collectedOutput += contents;
}

struct Update
{
    QString name;
    QString version;
};

static QList<Update> availableUpdates(const QDomDocument &document)
{
    if (document.isNull() || !document.firstChildElement().hasChildNodes())
        return {};
    QList<Update> result;
    const QDomNodeList updates = document.firstChildElement().elementsByTagName("update");
    for (int i = 0; i < updates.size(); ++i) {
        const QDomNode node = updates.item(i);
        if (node.isElement()) {
            const QDomElement element = node.toElement();
            if (element.hasAttribute("name"))
                result.append({element.attribute("name"), element.attribute("version")});
        }
    }
    return result;
}

struct QtPackage
{
    QString displayName;
    QVersionNumber version;
    bool installed;
    bool isPrerelease = false;
};

static QList<QtPackage> availableQtPackages(const QDomDocument &document)
{
    if (document.isNull() || !document.firstChildElement().hasChildNodes())
        return {};
    QList<QtPackage> result;
    const QDomNodeList packages = document.firstChildElement().elementsByTagName("package");
    for (int i = 0; i < packages.size(); ++i) {
        const QDomNode node = packages.item(i);
        if (node.isElement()) {
            const QDomElement element = node.toElement();
            if (element.hasAttribute("displayname") && element.hasAttribute("name")
                && element.hasAttribute("version")) {
                QtPackage package{element.attribute("displayname"),
                                  QVersionNumber::fromString(element.attribute("version")),
                                  element.hasAttribute("installedVersion")};
                // Heuristic: Prerelease if the name is not "Qt x.y.z"
                // (prereleases are named "Qt x.y.z-alpha" etc)
                package.isPrerelease = package.displayName
                                       != QString("Qt %1").arg(package.version.toString());
                result.append(package);
            }
        }
    }
    std::sort(result.begin(), result.end(), [](const QtPackage &p1, const QtPackage &p2) {
        return p1.version > p2.version;
    });
    return result;
}

// Expects packages to be sorted, high version first.
static Utils::optional<QtPackage> highestInstalledQt(const QList<QtPackage> &packages)
{
    const auto highestInstalledIt = std::find_if(packages.cbegin(),
                                                 packages.cend(),
                                                 [](const QtPackage &p) { return p.installed; });
    if (highestInstalledIt == packages.cend()) // Qt not installed
        return {};
    return *highestInstalledIt;
}

// Expects packages to be sorted, high version first.
static Utils::optional<QtPackage> qtToNagAbout(const QList<QtPackage> &allPackages,
                                               QVersionNumber *highestSeen)
{
    // Filter out any Qt prereleases
    const QList<QtPackage> packages = Utils::filtered(allPackages, [](const QtPackage &p) {
        return !p.isPrerelease;
    });
    if (packages.isEmpty())
        return {};
    const QtPackage highest = packages.constFirst();
    qCDebug(log) << "Highest available (non-prerelease) Qt:" << highest.version;
    qCDebug(log) << "Highest previously seen (non-prerelease) Qt:" << *highestSeen;
    // if the highestSeen version is null, we don't know if the Qt version is new, and better don't nag
    const bool isNew = !highestSeen->isNull() && highest.version > *highestSeen;
    if (highestSeen->isNull() || isNew)
        *highestSeen = highest.version;
    if (!isNew)
        return {};
    const Utils::optional<QtPackage> highestInstalled = highestInstalledQt(packages);
    qCDebug(log) << "Highest installed Qt:"
                 << qPrintable(highestInstalled ? highestInstalled->version.toString()
                                                : QString("none"));
    if (!highestInstalled) // don't nag if no Qt is installed at all
        return {};
    if (highestInstalled->version == highest.version)
        return {};
    return highest;
}

static void showUpdateInfo(const QList<Update> &updates, const std::function<void()> &startUpdater)
{
    Utils::InfoBarEntry info(InstallUpdates,
                             UpdateInfoPlugin::tr("New updates are available. Start the update?"));
    info.addCustomButton(UpdateInfoPlugin::tr("Start Update"), [startUpdater] {
        Core::ICore::infoBar()->removeInfo(InstallUpdates);
        startUpdater();
    });
    info.setDetailsWidgetCreator([updates]() -> QWidget * {
        const QString updateText = Utils::transform(updates, [](const Update &u) {
                                       return u.version.isEmpty()
                                                  ? u.name
                                                  : UpdateInfoPlugin::tr("%1 (%2)",
                                                                         "Package name and version")
                                                        .arg(u.name, u.version);
                                   }).join("</li><li>");
        auto label = new QLabel;
        label->setText("<qt><p>" + UpdateInfoPlugin::tr("Available updates:") + "<ul><li>"
                       + updateText + "</li></ul></p></qt>");
        label->setContentsMargins(0, 0, 0, 8);
        return label;
    });
    Core::ICore::infoBar()->removeInfo(InstallUpdates); // remove any existing notifications
    Core::ICore::infoBar()->unsuppressInfo(InstallUpdates);
    Core::ICore::infoBar()->addInfo(info);
}

static void showQtUpdateInfo(const QtPackage &package,
                             const std::function<void()> &startPackageManager)
{
    Utils::InfoBarEntry info(InstallQtUpdates,
                             UpdateInfoPlugin::tr(
                                 "%1 is available. Check the <a "
                                 "href=\"https://www.qt.io/blog\">Qt blog</a> for details.")
                                 .arg(package.displayName));
    info.addCustomButton(UpdateInfoPlugin::tr("Start Package Manager"), [startPackageManager] {
        Core::ICore::infoBar()->removeInfo(InstallQtUpdates);
        startPackageManager();
    });
    info.addCustomButton(UpdateInfoPlugin::tr("Open Settings"), [] {
        Core::ICore::infoBar()->removeInfo(InstallQtUpdates);
        Core::ICore::showOptionsDialog(FILTER_OPTIONS_PAGE_ID);
    });
    Core::ICore::infoBar()->removeInfo(InstallQtUpdates); // remove any existing notifications
    Core::ICore::infoBar()->unsuppressInfo(InstallQtUpdates);
    Core::ICore::infoBar()->addInfo(info);
}

void UpdateInfoPlugin::checkForUpdatesFinished()
{
    setLastCheckDate(QDate::currentDate());

    QDomDocument document;
    // since the output can contain two toplevel items from the two separate MaintenanceTool runs,
    // surround with a toplevel element
    const QString xml = d->m_collectedOutput.isEmpty()
                            ? QString()
                            : ("<doc>" + d->m_collectedOutput + "</doc>");
    qCDebug(log) << "--- MaintenanceTool output (combined):";
    qCDebug(log) << qPrintable(xml);
    document.setContent(xml);

    stopCheckForUpdates();

    const QList<Update> updates = availableUpdates(document);
    const QList<QtPackage> qtPackages = availableQtPackages(document);
    if (log().isDebugEnabled()) {
        qCDebug(log) << "--- Available updates:";
        for (const Update &u : updates)
            qCDebug(log) << u.name << u.version;
        qCDebug(log) << "--- Available Qt packages:";
        for (const QtPackage &p : qtPackages) {
            qCDebug(log) << p.displayName << p.version << "installed:" << p.installed
                         << "prerelease:" << p.isPrerelease;
        }
    }
    Utils::optional<QtPackage> qtToNag = qtToNagAbout(qtPackages, &d->m_lastMaxQtVersion);

    if (!updates.isEmpty() || qtToNag) {
        // progress details are shown until user interaction for the "no updates" case,
        // so we can show the "No updates found" text, but if we have updates we don't
        // want to keep it around
        if (d->m_progress)
            d->m_progress->setKeepOnFinish(FutureProgress::HideOnFinish);
        emit newUpdatesAvailable(true);
        if (!updates.isEmpty())
            showUpdateInfo(updates, [this] { startUpdater(); });
        if (qtToNag)
            showQtUpdateInfo(*qtToNag, [this] { startPackageManager(); });
    } else {
        emit newUpdatesAvailable(false);
        if (d->m_progress)
            d->m_progress->setSubtitle(tr("No updates found."));
    }
}

bool UpdateInfoPlugin::isCheckForUpdatesRunning() const
{
    return d->m_checkUpdatesCommand;
}

void UpdateInfoPlugin::extensionsInitialized()
{
    if (isAutomaticCheck())
        QTimer::singleShot(OneMinute, this, &UpdateInfoPlugin::startAutoCheckForUpdates);
}

bool UpdateInfoPlugin::initialize(const QStringList & /* arguments */, QString *errorMessage)
{
    loadSettings();

    if (d->m_maintenanceTool.isEmpty()) {
        *errorMessage = tr("Could not determine location of maintenance tool. Please check "
            "your installation if you did not enable this plugin manually.");
        return false;
    }

    if (!QFileInfo(d->m_maintenanceTool).isExecutable()) {
        *errorMessage = tr("The maintenance tool at \"%1\" is not an executable. Check your installation.")
            .arg(d->m_maintenanceTool);
        d->m_maintenanceTool.clear();
        return false;
    }

    connect(ICore::instance(), &ICore::saveSettingsRequested,
            this, &UpdateInfoPlugin::saveSettings);

    (void) new SettingsPage(this);

    QAction *checkForUpdatesAction = new QAction(tr("Check for Updates"), this);
    checkForUpdatesAction->setMenuRole(QAction::ApplicationSpecificRole);
    Core::Command *checkForUpdatesCommand = Core::ActionManager::registerAction(checkForUpdatesAction, "Updates.CheckForUpdates");
    connect(checkForUpdatesAction, &QAction::triggered, this, &UpdateInfoPlugin::startCheckForUpdates);
    ActionContainer *const helpContainer = ActionManager::actionContainer(Core::Constants::M_HELP);
    helpContainer->addAction(checkForUpdatesCommand, Constants::G_HELP_UPDATES);

    return true;
}

void UpdateInfoPlugin::loadSettings() const
{
    UpdateInfoPluginPrivate::Settings def;
    QSettings *settings = ICore::settings();
    const QString updaterKey = QLatin1String(UpdaterGroup) + '/';
    d->m_maintenanceTool = settings->value(updaterKey + MaintenanceToolKey).toString();
    d->m_lastCheckDate = settings->value(updaterKey + LastCheckDateKey, QDate()).toDate();
    d->m_settings.automaticCheck
        = settings->value(updaterKey + AutomaticCheckKey, def.automaticCheck).toBool();
    const QMetaObject *mo = metaObject();
    const QMetaEnum me = mo->enumerator(mo->indexOfEnumerator(CheckIntervalKey));
    if (QTC_GUARD(me.isValid())) {
        const QString checkInterval = settings
                                          ->value(updaterKey + CheckIntervalKey,
                                                  me.valueToKey(def.checkInterval))
                                          .toString();
        bool ok = false;
        const int newValue = me.keyToValue(checkInterval.toUtf8(), &ok);
        if (ok)
            d->m_settings.checkInterval = static_cast<CheckUpdateInterval>(newValue);
    }
    const QString lastMaxQtVersionString = settings->value(updaterKey + LastMaxQtVersionKey)
                                               .toString();
    d->m_lastMaxQtVersion = QVersionNumber::fromString(lastMaxQtVersionString);
    d->m_settings.checkForQtVersions
        = settings->value(updaterKey + CheckForNewQtVersionsKey, def.checkForQtVersions).toBool();
}

void UpdateInfoPlugin::saveSettings()
{
    UpdateInfoPluginPrivate::Settings def;
    Utils::QtcSettings *settings = ICore::settings();
    settings->beginGroup(UpdaterGroup);
    settings->setValueWithDefault(LastCheckDateKey, d->m_lastCheckDate, QDate());
    settings->setValueWithDefault(AutomaticCheckKey,
                                  d->m_settings.automaticCheck,
                                  def.automaticCheck);
    // Note: don't save MaintenanceToolKey on purpose! This setting may be set only by installer.
    // If creator is run not from installed SDK, the setting can be manually created here:
    // [CREATOR_INSTALLATION_LOCATION]/share/qtcreator/QtProject/QtCreator.ini or
    // [CREATOR_INSTALLATION_LOCATION]/Qt Creator.app/Contents/Resources/QtProject/QtCreator.ini on OS X
    const QMetaObject *mo = metaObject();
    const QMetaEnum me = mo->enumerator(mo->indexOfEnumerator(CheckIntervalKey));
    settings->setValueWithDefault(CheckIntervalKey,
                                  QString::fromUtf8(me.valueToKey(d->m_settings.checkInterval)),
                                  QString::fromUtf8(me.valueToKey(def.checkInterval)));
    settings->setValueWithDefault(LastMaxQtVersionKey, d->m_lastMaxQtVersion.toString());
    settings->setValueWithDefault(CheckForNewQtVersionsKey,
                                  d->m_settings.checkForQtVersions,
                                  def.checkForQtVersions);
    settings->endGroup();
}

bool UpdateInfoPlugin::isAutomaticCheck() const
{
    return d->m_settings.automaticCheck;
}

void UpdateInfoPlugin::setAutomaticCheck(bool on)
{
    if (d->m_settings.automaticCheck == on)
        return;

    d->m_settings.automaticCheck = on;
    if (on)
        startAutoCheckForUpdates();
    else
        stopAutoCheckForUpdates();
}

UpdateInfoPlugin::CheckUpdateInterval UpdateInfoPlugin::checkUpdateInterval() const
{
    return d->m_settings.checkInterval;
}

void UpdateInfoPlugin::setCheckUpdateInterval(UpdateInfoPlugin::CheckUpdateInterval interval)
{
    if (d->m_settings.checkInterval == interval)
        return;

    d->m_settings.checkInterval = interval;
}

bool UpdateInfoPlugin::isCheckingForQtVersions() const
{
    return d->m_settings.checkForQtVersions;
}

void UpdateInfoPlugin::setCheckingForQtVersions(bool on)
{
    d->m_settings.checkForQtVersions = on;
}

QDate UpdateInfoPlugin::lastCheckDate() const
{
    return d->m_lastCheckDate;
}

void UpdateInfoPlugin::setLastCheckDate(const QDate &date)
{
    if (d->m_lastCheckDate == date)
        return;

    d->m_lastCheckDate = date;
    emit lastCheckDateChanged(date);
}

QDate UpdateInfoPlugin::nextCheckDate() const
{
    return nextCheckDate(d->m_settings.checkInterval);
}

QDate UpdateInfoPlugin::nextCheckDate(CheckUpdateInterval interval) const
{
    if (!d->m_lastCheckDate.isValid())
        return QDate();

    if (interval == DailyCheck)
        return d->m_lastCheckDate.addDays(1);
    if (interval == WeeklyCheck)
        return d->m_lastCheckDate.addDays(7);
    return d->m_lastCheckDate.addMonths(1);
}

void UpdateInfoPlugin::startUpdater()
{
    Utils::QtcProcess::startDetached(
        {Utils::FilePath::fromString(d->m_maintenanceTool), {"--updater"}});
}

void UpdateInfoPlugin::startPackageManager()
{
    Utils::QtcProcess::startDetached(
        {Utils::FilePath::fromString(d->m_maintenanceTool), {"--start-package-manager"}});
}

} //namespace Internal
} //namespace UpdateInfo
