/*
    SPDX-FileCopyrightText: 2012-2022 Harald Sitter <sitter@kde.org>
    SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/

#include <utility>

#include <QClipboard>
#include <QGuiApplication>
#include <QIcon>
#include <QLocale>

#include <KAboutData>
#include <KAuth/Action>
#include <KAuth/ExecuteJob>
#include <KConfig>
#include <KConfigGroup>
#include <KCoreAddons>
#include <KLocalizedString>
#include <KOSRelease>
#include <KSharedConfig>

#include "CPUEntry.h"
#include "GPUEntry.h"
#include "GraphicsPlatformEntry.h"
#include "KernelEntry.h"
#include "MemoryEntry.h"
#include "PlasmaEntry.h"
#include "ServiceRunner.h"
#include "Version.h"

#include <KAboutData>
#include <KLocalizedString>
#include <KPluginFactory>
#include <KQuickAddons/ConfigModule>

class EntryModel : public QAbstractListModel
{
    Q_OBJECT
public:
    enum Role {
        ModelData = Qt::UserRole,
    };
    Q_ENUM(Role)

    using QAbstractListModel::QAbstractListModel;

    [[nodiscard]] int rowCount(const QModelIndex &parent) const override
    {
        Q_UNUSED(parent);
        return m_entries.size();
    }

    [[nodiscard]] QVariant data(const QModelIndex &index, int intRole) const override
    {
        if (!index.isValid()) {
            return {};
        }

        switch (static_cast<Role>(intRole)) {
        case Role::ModelData:
            return QVariant::fromValue(m_entries.at(index.row()));
        }

        return {};
    }

    [[nodiscard]] QHash<int, QByteArray> roleNames() const override
    {
        static QHash<int, QByteArray> roles;
        if (!roles.isEmpty()) {
            return roles;
        }

        roles = QAbstractListModel::roleNames();
        roles.insert(Role::ModelData, QByteArrayLiteral("modelData"));
        return roles;
    }

    void append(Entry *entry)
    {
        beginInsertRows(QModelIndex(), m_entries.size(), m_entries.size());
        m_entries.push_back(entry);
        endInsertRows();
    }

private:
    std::vector<Entry *> m_entries;
};
Q_DECLARE_METATYPE(EntryModel *)
Q_DECLARE_METATYPE(Entry *)

class KCMAboutSystem : public KQuickAddons::ConfigModule
{
    Q_OBJECT
    Q_DISABLE_COPY_MOVE(KCMAboutSystem) // rule of 5
public:
    explicit KCMAboutSystem(QObject *parent, const KPluginMetaData &data, const QVariantList &args)
        : ConfigModule(parent, data, args)
    {
        auto aboutData = new KAboutData;
        aboutData->setComponentName(QStringLiteral("kcm_about-distro"));
        aboutData->setDisplayName(i18nc("@title", "About this System"));
        aboutData->setVersion(QByteArrayLiteral(PROJECT_VERSION));
        aboutData->setLicense(KAboutLicense::LicenseKey::GPL_V3);
        aboutData->setCopyrightStatement(i18nc("@info:credit", "Copyright 2012-2020 Harald Sitter"));
        aboutData->addAuthor(i18nc("@info:credit", "Harald Sitter"), i18nc("@info:credit", "Author"), QStringLiteral("sitter@kde.org"));
        setAboutData(aboutData);

        qmlRegisterType<ServiceRunner>("org.kde.kinfocenter.about_distro.private", 1, 0, "ServiceRunner");

        // https://bugs.kde.org/show_bug.cgi?id=366158
        // When a KCM loads fast enough do a blocking load via the constructor.
        // Otherwise there is a noticeable rendering gap where dummy/no data is
        // shown. Makes it look bad.
        load();
    }

    ~KCMAboutSystem() override
    {
        qDeleteAll(m_entries);
    }

    void load() override
    {
        // load is called lazily, but also from the ctor -> prevent double init.
        if (!m_entries.empty()) {
            return;
        }

        loadOSData();
        loadEntries();
    }

    void loadOSData()
    {
        // NOTE: do not include globals, otherwise kdeglobals could provide values
        //       even though we only explicitly want them from our own config.
        KSharedConfig::Ptr config = KSharedConfig::openConfig(QStringLiteral("kcm-about-distrorc"), KConfig::NoGlobals);
        KConfigGroup cg = KConfigGroup(config, "General");

        KOSRelease os;

        QString logoPath = cg.readEntry("LogoPath", os.logo());
        if (logoPath.isEmpty()) {
            logoPath = QStringLiteral("start-here-kde");
        }
        m_distroLogo = logoPath;

        // We allow overriding of the OS name for branding purposes.
        // For example OS Ubuntu may be rebranded as Kubuntu. Also Kubuntu Active
        // as a product brand is different from Kubuntu.
        const QString distroName = cg.readEntry("Name", os.name());
        const QString osrVersion = cg.readEntry("UseOSReleaseVersion", false) ? os.version() : os.versionId();
        const QString versionId = cg.readEntry("Version", osrVersion);
        // This creates a trailing space if versionId is empty, so trimming String
        // to remove possibly trailing spaces
        const QString distroNameVersion = QStringLiteral("%1 %2").arg(distroName, versionId).trimmed();
        m_distroNameVersion = distroNameVersion;

        // Insert a dummy entry for debug info dumps.
        m_entries.push_back(new Entry(ki18n("Operating System:"), distroNameVersion));

        const QString variant = cg.readEntry("Variant", os.variant());
        m_distroVariant = variant;

        const QString url = cg.readEntry("Website", os.homeUrl());
        m_distroUrl = url;

        Q_EMIT changed();
    }

    static KLocalizedString systemInfoKey(const QString &key)
    {
        if (key == QStringLiteral("system-manufacturer")) {
            return ki18nc("@label", "Manufacturer:");
        }
        if (key == QStringLiteral("system-product-name")) {
            return ki18nc("@label", "Product Name:");
        }
        if (key == QStringLiteral("system-version")) {
            return ki18nc("@label", "System Version:");
        }
        if (key == QStringLiteral("system-serial-number")) {
            return ki18nc("@label", "Serial Number:");
        }
        qFatal("unexpected systeminfo key %s\n", qUtf8Printable(key));
        Q_UNREACHABLE();
    }

    void loadEntries()
    {
        auto addEntriesToGrid = [this](EntryModel *model, const std::vector<Entry *> &entries) {
            for (auto entry : entries) {
                if (!entry->isValid()) {
                    delete entry; // since we do not keep it around
                    continue;
                }
                model->append(entry);
                m_entries.push_back(entry);
            }
        };

        // software
        addEntriesToGrid(m_softwareEntries,
                         {new PlasmaEntry(),
                          new Entry(ki18n("KDE Frameworks Version:"), KCoreAddons::versionString()),
                          new Entry(ki18n("Qt Version:"), QString::fromLatin1(qVersion())),
                          new KernelEntry(),
                          new GraphicsPlatformEntry()});

        // hardware
        addEntriesToGrid(m_hardwareEntries, {new CPUEntry, new MemoryEntry, new GPUEntry});

        KAuth::Action action(QStringLiteral("org.kde.kinfocenter.dmidecode.systeminformation"));
        action.setHelperId(QStringLiteral("org.kde.kinfocenter.dmidecode"));
        KAuth::ExecuteJob *job = action.execute();
        connect(job, &KJob::result, this, [this, job, addEntriesToGrid] {
            QVariantMap data = job->data();
            static const QString systemSerialNumberKey = QStringLiteral("system-serial-number");
            const QString systemSerialNumber = data.take(systemSerialNumberKey).toString();
            for (auto it = data.cbegin(); it != data.cend(); ++it) {
                addEntriesToGrid(m_hardwareEntries, {new Entry(systemInfoKey(it.key()), it.value().toString())});
            }
            // Insert hidden entries at the end so it doesn't look weird visually to have a button mid-layout.
            if (!systemSerialNumber.isEmpty()) {
                addEntriesToGrid(m_hardwareEntries, {new Entry(systemInfoKey(systemSerialNumberKey), systemSerialNumber, Entry::Hidden::Yes)});
            }

            Q_EMIT changed();
        });
        job->start();

        Q_EMIT changed();
    }

    Q_SCRIPTABLE void copyToClipboard()
    {
        QString text;
        for (auto entry : std::as_const(m_entries)) {
            if (entry->isHidden()) {
                continue;
            }
            text += entry->diagnosticLine(Entry::Language::System);
        }
        storeInClipboard(text);
    }

    Q_SCRIPTABLE void copyToClipboardInEnglish()
    {
        QString text;
        for (auto entry : std::as_const(m_entries)) {
            if (entry->isHidden()) {
                continue;
            }
            text += entry->diagnosticLine(Entry::Language::English);
        }
        storeInClipboard(text);
    }

    Q_SCRIPTABLE static void storeInClipboard(const QString &text)
    {
        QGuiApplication::clipboard()->setText(text.trimmed());
    }

private:
    std::vector<const Entry *> m_entries;

    Q_SIGNAL void changed();

    Q_PROPERTY(EntryModel *softwareEntries MEMBER m_softwareEntries CONSTANT);
    EntryModel *m_softwareEntries = new EntryModel(this);
    Q_PROPERTY(EntryModel *hardwareEntries MEMBER m_hardwareEntries CONSTANT);
    EntryModel *m_hardwareEntries = new EntryModel(this);

    Q_PROPERTY(QString distroLogo MEMBER m_distroLogo NOTIFY changed);
    QString m_distroLogo;
    Q_PROPERTY(QString distroNameVersion MEMBER m_distroNameVersion NOTIFY changed);
    QString m_distroNameVersion;
    Q_PROPERTY(QString distroVariant MEMBER m_distroVariant NOTIFY changed);
    QString m_distroVariant;
    Q_PROPERTY(QString distroUrl MEMBER m_distroUrl NOTIFY changed);
    QString m_distroUrl;

    Q_PROPERTY(bool isEnglish MEMBER m_isEnglish CONSTANT);
    const bool m_isEnglish = QLocale::system().language() == QLocale::English || QLocale::system().language() == QLocale::C;
    Q_PROPERTY(bool isThisKInfoCenter MEMBER m_isThisKInfoCenter CONSTANT);
    const bool m_isThisKInfoCenter = qGuiApp->desktopFileName() == QLatin1String("org.kde.kinfocenter");
};

K_PLUGIN_CLASS_WITH_JSON(KCMAboutSystem, "about-distro.json")

#include "main.moc"
