/***************************************************************************
 *   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/>. *
 ***************************************************************************/

// Qt includes
#include <QDebug>
#include <QDir>
#include <QFileInfo>
#include <QStandardPaths>
#include <QTimer>
#include <QDirIterator>

// Attica includes
#include <Attica/Content>
#include <Attica/ProviderManager>

// KDE includes
#include <knewstuffcore_version.h>
#include <KNSCore/Engine>
#include <KNSCore/QuestionManager>
#include <KConfig>
#include <KConfigGroup>
#include <KLocalizedString>

// DiscoverCommon includes
#include "Transaction/Transaction.h"
#include "Transaction/TransactionModel.h"
#include "Category/Category.h"

// Own includes
#include "KNSBackend.h"
#include "KNSResource.h"
#include "KNSReviews.h"
#include <resources/StandardBackendUpdater.h>
#include "utils.h"

class KNSBackendFactory : public AbstractResourcesBackendFactory {
    Q_OBJECT
    Q_PLUGIN_METADATA(IID "org.kde.muon.AbstractResourcesBackendFactory")
    Q_INTERFACES(AbstractResourcesBackendFactory)
    public:
        KNSBackendFactory() {
            connect(KNSCore::QuestionManager::instance(), &KNSCore::QuestionManager::askQuestion, this, [](KNSCore::Question* q) {
                qWarning() << q->question() << q->questionType();
                q->setResponse(KNSCore::Question::InvalidResponse);
            });
        }

        QVector<AbstractResourcesBackend*> newInstance(QObject* parent, const QString &/*name*/) const override
        {
            QVector<AbstractResourcesBackend*> ret;
#if KNEWSTUFFCORE_VERSION_MAJOR==5 && KNEWSTUFFCORE_VERSION_MINOR>=57
            QStringList locations = KNSCore::Engine::configSearchLocations();
#else
            QStringList locations = QStandardPaths::standardLocations(QStandardPaths::GenericConfigLocation);
#endif
            QSet<QString> files;
            for (const QString &path: locations) {
                QDirIterator dirIt(path, {QStringLiteral("*.knsrc")}, QDir::Files);
                for(; dirIt.hasNext(); ) {
                    dirIt.next();

                    if (files.contains(dirIt.fileName()))
                        continue;
                    files << dirIt.fileName();

                    auto bk = new KNSBackend(parent, QStringLiteral("plasma"), dirIt.filePath());
                    if (bk->isValid())
                        ret += bk;
                    else
                        delete bk;
                }
            }
            return ret;
        }
};

Q_DECLARE_METATYPE(KNSCore::EntryInternal)

KNSBackend::KNSBackend(QObject* parent, const QString& iconName, const QString &knsrc)
    : AbstractResourcesBackend(parent)
    , m_fetching(false)
    , m_isValid(true)
    , m_reviews(new KNSReviews(this))
    , m_name(knsrc)
    , m_iconName(iconName)
    , m_updater(new StandardBackendUpdater(this))
{
    const QString fileName = QFileInfo(m_name).fileName();
    setName(fileName);
    setObjectName(knsrc);

    const KConfig conf(m_name, KConfig::SimpleConfig);
    if (!conf.hasGroup("KNewStuff3")) {
        markInvalid(QStringLiteral("Config group not found! Check your KNS3 installation."));
        return;
    }

    m_categories = QStringList{ fileName };

    const KConfigGroup group = conf.group("KNewStuff3");
    m_extends = group.readEntry("Extends", QStringList());
    m_reviews->setProviderUrl(QUrl(group.readEntry("ProvidersUrl", QString())));

    setFetching(true);

    // This ensures we have something to track when checking after the initialization timeout
    connect(this, &KNSBackend::initialized, this, [this](){ m_initialized = true; });
    // If we have not initialized in 60 seconds, consider this KNS backend invalid
    QTimer::singleShot(60000, this, [this](){
        if(!m_initialized) {
            markInvalid(i18n("Backend %1 took too long to initialize", m_displayName));
            m_responsePending = false;
            Q_EMIT searchFinished();
            Q_EMIT availableForQueries();
        }
    });

    const QVector<QPair<FilterType, QString>> filters = { {CategoryFilter, fileName } };
    const QSet<QString> backendName = { name() };
    m_displayName = group.readEntry("Name", QString());
    if (m_displayName.isEmpty()) {
        m_displayName = fileName.mid(0, fileName.indexOf(QLatin1Char('.')));
        m_displayName[0] = m_displayName[0].toUpper();
    }
    m_hasApplications = group.readEntry<bool>("X-Discover-HasApplications", false);

    const QStringList cats = group.readEntry<QStringList>("Categories", QStringList{});
    QVector<Category*> categories;
    if (cats.count() > 1) {
        m_categories += cats;
        for(const auto &cat: cats) {
            if (m_hasApplications)
                categories << new Category(cat, QStringLiteral("applications-other"), { {CategoryFilter, cat } }, backendName, {}, {}, true);
            else
                categories << new Category(cat, QStringLiteral("plasma"), { {CategoryFilter, cat } }, backendName, {}, {}, true);
        }
    }

    QVector<Category*> topCategories{categories};
    for (const auto &cat: categories) {
        const QString catName = cat->name().append(QLatin1Char('/'));
        for (const auto& potentialSubCat: categories) {
            if(potentialSubCat->name().startsWith(catName)) {
                cat->addSubcategory(potentialSubCat);
                topCategories.removeOne(potentialSubCat);
            }
        }
    }

    m_engine = new KNSCore::Engine(this);
    connect(m_engine, &KNSCore::Engine::signalErrorCode, this, &KNSBackend::signalErrorCode);
    connect(m_engine, &KNSCore::Engine::signalEntriesLoaded, this, &KNSBackend::receivedEntries, Qt::QueuedConnection);
    connect(m_engine, &KNSCore::Engine::signalEntryChanged, this, &KNSBackend::statusChanged, Qt::QueuedConnection);
    connect(m_engine, &KNSCore::Engine::signalEntryDetailsLoaded, this, &KNSBackend::detailsLoaded);
    connect(m_engine, &KNSCore::Engine::signalProvidersLoaded, this, &KNSBackend::fetchInstalled);
    connect(m_engine, &KNSCore::Engine::signalCategoriesMetadataLoded, this, [categories](const QList< KNSCore::Provider::CategoryMetadata>& categoryMetadatas){
        for (const KNSCore::Provider::CategoryMetadata& category : categoryMetadatas) {
            for (Category* cat : categories) {
                if (cat->orFilters().count() > 0 && cat->orFilters().first().second == category.name) {
                    cat->setName(category.displayName);
                    break;
                }
            }
        }
    });
    m_engine->setPageSize(100);
    m_engine->init(m_name);

    if(m_hasApplications) {
        auto actualCategory = new Category(m_displayName, QStringLiteral("applications-other"), filters, backendName, topCategories, QUrl(), false);
        auto applicationCategory = new Category(i18n("Applications"), QStringLiteral("applications-internet"), filters, backendName, { actualCategory }, QUrl(), false);
        applicationCategory->setAndFilter({ {CategoryFilter, QLatin1String("Application")} });
        m_categories.append(applicationCategory->name());
        m_rootCategories = { applicationCategory };
        // Make sure we filter out any apps which won't run on the current system architecture
        QStringList tagFilter = m_engine->tagFilter();
        if(QSysInfo::currentCpuArchitecture() == QLatin1String("arm")) {
            tagFilter << QLatin1String("application##architecture==armhf");
        } else if(QSysInfo::currentCpuArchitecture() == QLatin1String("arm64")) {
            tagFilter << QLatin1String("application##architecture==arm64");
        } else if(QSysInfo::currentCpuArchitecture() == QLatin1String("i386")) {
            tagFilter << QLatin1String("application##architecture==x86");
        } else if(QSysInfo::currentCpuArchitecture() == QLatin1String("ia64")) {
            tagFilter << QLatin1String("application##architecture==x86-64");
        } else if(QSysInfo::currentCpuArchitecture() == QLatin1String("x86_64")) {
            tagFilter << QLatin1String("application##architecture==x86");
            tagFilter << QLatin1String("application##architecture==x86-64");
        }
        m_engine->setTagFilter(tagFilter);
    } else {
        static const QSet<QString> knsrcPlasma = {
            QStringLiteral("aurorae.knsrc"), QStringLiteral("icons.knsrc"), QStringLiteral("kfontinst.knsrc"), QStringLiteral("lookandfeel.knsrc"), QStringLiteral("plasma-themes.knsrc"), QStringLiteral("plasmoids.knsrc"),
            QStringLiteral("wallpaper.knsrc"), QStringLiteral("xcursor.knsrc"),

            QStringLiteral("cgcgtk3.knsrc"), QStringLiteral("cgcicon.knsrc"), QStringLiteral("cgctheme.knsrc"), //GTK integration
            QStringLiteral("kwinswitcher.knsrc"), QStringLiteral("kwineffect.knsrc"), QStringLiteral("kwinscripts.knsrc"), //KWin
            QStringLiteral("comic.knsrc"), QStringLiteral("colorschemes.knsrc"),
            QStringLiteral("emoticons.knsrc"), QStringLiteral("plymouth.knsrc"),
            QStringLiteral("sddmtheme.knsrc"), QStringLiteral("wallpaperplugin.knsrc"),
            QStringLiteral("ksplash.knsrc"), QStringLiteral("window-decorations.knsrc")
        };
        const auto iconName = knsrcPlasma.contains(fileName)? QStringLiteral("plasma") : QStringLiteral("applications-other");
        auto actualCategory = new Category(m_displayName, iconName, filters, backendName, categories, QUrl(), true);

        const auto topLevelName = knsrcPlasma.contains(fileName)? i18n("Plasma Addons") : i18n("Application Addons");
        auto addonsCategory = new Category(topLevelName, iconName, filters, backendName, {actualCategory}, QUrl(), true);
        m_rootCategories = { addonsCategory };
    }
}

KNSBackend::~KNSBackend()
{
    qDeleteAll(m_rootCategories);
}

void KNSBackend::markInvalid(const QString &message)
{
    m_rootCategories.clear();
    qWarning() << "invalid kns backend!" << m_name << "because:" << message;
    m_isValid = false;
    setFetching(false);
    Q_EMIT initialized();
}

void KNSBackend::fetchInstalled()
{
    auto search = new OneTimeAction([this]() {
        Q_EMIT startingSearch();
        m_onePage = true;
        m_responsePending = true;
        m_engine->checkForInstalled();
    }, this);

    if (m_responsePending) {
        connect(this, &KNSBackend::availableForQueries, search, &OneTimeAction::trigger, Qt::QueuedConnection);
    } else {
        search->trigger();
    }
}

void KNSBackend::setFetching(bool f)
{
    if(m_fetching!=f) {
        m_fetching = f;
        emit fetchingChanged();

        if (!m_fetching) {
            Q_EMIT initialized();
        }
    }

}

bool KNSBackend::isValid() const
{
    return m_isValid;
}

KNSResource* KNSBackend::resourceForEntry(const KNSCore::EntryInternal& entry)
{
    KNSResource* r = static_cast<KNSResource*>(m_resourcesByName.value(entry.uniqueId()));
    if (!r) {
        QStringList categories{name(), m_rootCategories.first()->name()};
        const auto cats = m_engine->categoriesMetadata();
        const int catIndex = kIndexOf(cats, [&entry](const KNSCore::Provider::CategoryMetadata& cat){ return entry.category() == cat.id; });
        if (catIndex > -1) {
            categories << cats.at(catIndex).name;
        }
        if(m_hasApplications) {
            categories << QLatin1String("Application");
        }
        r = new KNSResource(entry, categories, this);
        m_resourcesByName.insert(entry.uniqueId(), r);
    } else {
        r->setEntry(entry);
    }
    return r;
}

void KNSBackend::receivedEntries(const KNSCore::EntryInternal::List& entries)
{
    m_responsePending = false;
    const auto filtered = kFilter<KNSCore::EntryInternal::List>(entries, [](const KNSCore::EntryInternal& entry){ return entry.isValid(); });
    const auto resources = kTransform<QVector<AbstractResource*>>(filtered, [this](const KNSCore::EntryInternal& entry){ return resourceForEntry(entry); });

    if (!resources.isEmpty()) {
        Q_EMIT receivedResources(resources);
    } else {
        Q_EMIT searchFinished();
        Q_EMIT availableForQueries();
        setFetching(false);
        return;
    }
//     qDebug() << "received" << objectName() << this << m_resourcesByName.count();
    if (m_onePage) {
        Q_EMIT availableForQueries();
        setFetching(false);
    }
}

void KNSBackend::fetchMore()
{
    if (m_responsePending)
        return;

    // We _have_ to set this first. If we do not, we may run into a situation where the
    // data request will conclude immediately, causing m_responsePending to remain true
    // for perpetuity as the slots will be called before the function returns.
    m_responsePending = true;
    m_engine->requestMoreData();
}

void KNSBackend::statusChanged(const KNSCore::EntryInternal& entry)
{
    resourceForEntry(entry);
}

void KNSBackend::signalErrorCode(const KNSCore::ErrorCode& errorCode, const QString& message, const QVariant& metadata)
{
    QString error = message;
    qDebug() << "KNS error in" << m_displayName << ":" << errorCode << message << metadata;
    bool invalidFile = false;
    switch(errorCode) {
        case KNSCore::ErrorCode::UnknownError:
            // This is not supposed to be hit, of course, but any error coming to this point should be non-critical and safely ignored.
            break;
        case KNSCore::ErrorCode::NetworkError:
            // If we have a network error, we need to tell the user about it. This is almost always fatal, so mark invalid and tell the user.
            error = i18n("Network error in backend %1: %2", m_displayName, metadata.toInt());
            markInvalid(error);
            invalidFile = true;
            break;
        case KNSCore::ErrorCode::OcsError:
            if(metadata.toInt() == 200) {
                // Too many requests, try again in a couple of minutes - perhaps we can simply postpone it automatically, and give a message?
                error = i18n("Too many requests sent to the server for backend %1. Please try again in a few minutes.", m_displayName);
            } else {
                // Unknown API error, usually something critical, mark as invalid and cry a lot
                error = i18n("Invalid %1 backend, contact your distributor.", m_displayName);
                markInvalid(error);
                invalidFile = true;
            }
            break;
        case KNSCore::ErrorCode::ConfigFileError:
            error = i18n("Invalid %1 backend, contact your distributor.", m_displayName);
            markInvalid(error);
            invalidFile = true;
            break;
        case KNSCore::ErrorCode::ProviderError:
            error = i18n("Invalid %1 backend, contact your distributor.", m_displayName);
            markInvalid(error);
            invalidFile = true;
            break;
        case KNSCore::ErrorCode::InstallationError:
            // This error is handled already, by forwarding the KNS engine's installer error message.
            break;
        case KNSCore::ErrorCode::ImageError:
            // Image fetching errors are not critical as such, but may lead to weird layout issues, might want handling...
            error = i18n("Could not fetch screenshot for the entry %1 in backend %2", metadata.toList().at(0).toString(), m_displayName);
            break;
        default:
            // Having handled all current error values, we should by all rights never arrive here, but for good order and future safety...
            error = i18n("Unhandled error in %1 backend. Contact your distributor.", m_displayName);
            break;
    }
    m_responsePending = false;
    Q_EMIT searchFinished();
    Q_EMIT availableForQueries();
    // Setting setFetching to false when we get an error ensures we don't end up in an eternally-fetching state
    this->setFetching(false);
    qWarning() << "kns error" << objectName() << error;
    if (!invalidFile)
        Q_EMIT passiveMessage(i18n("%1: %2", name(), error));
}

class KNSTransaction : public Transaction
{
public:
    KNSTransaction(QObject* parent, KNSResource* res, Transaction::Role role)
        : Transaction(parent, res, role)
        , m_id(res->entry().uniqueId())
    {
        setCancellable(false);

        auto manager = res->knsBackend()->engine();
        connect(manager, &KNSCore::Engine::signalEntryChanged, this, &KNSTransaction::anEntryChanged);
        TransactionModel::global()->addTransaction(this);

        std::function<void()> actionFunction;
        auto engine = res->knsBackend()->engine();
        if(role == RemoveRole)
            actionFunction = [res, engine]() {
                engine->uninstall(res->entry());
            };
        else if (res->linkIds().isEmpty())
            actionFunction = [res, engine]() {
                engine->install(res->entry());
            };
        else
            actionFunction = [res, engine]() {
                const auto links = res->linkIds();
                for(auto i : links)
                    engine->install(res->entry(), i);
            };
        QTimer::singleShot(0, res, actionFunction);
    }

    void anEntryChanged(const KNSCore::EntryInternal& entry) {
        if (entry.uniqueId() == m_id) {
            switch (entry.status()) {
                case KNS3::Entry::Invalid:
                    qWarning() << "invalid status for" << entry.uniqueId() << entry.status();
                    break;
                case KNS3::Entry::Installing:
                case KNS3::Entry::Updating:
                    setStatus(CommittingStatus);
                    break;
                case KNS3::Entry::Downloadable:
                case KNS3::Entry::Installed:
                case KNS3::Entry::Deleted:
                case KNS3::Entry::Updateable:
                    if (status() != DoneStatus) {
                        setStatus(DoneStatus);
                    }
                    break;
            }
        }
    }

    void cancel() override {}

private:
    const QString m_id;
};

Transaction* KNSBackend::removeApplication(AbstractResource* app)
{
    auto res = qobject_cast<KNSResource*>(app);
    return new KNSTransaction(this, res, Transaction::RemoveRole);
}

Transaction* KNSBackend::installApplication(AbstractResource* app)
{
    auto res = qobject_cast<KNSResource*>(app);
    return new KNSTransaction(this, res, Transaction::InstallRole);
}

Transaction* KNSBackend::installApplication(AbstractResource* app, const AddonList& /*addons*/)
{
    return installApplication(app);
}

int KNSBackend::updatesCount() const
{
    return m_updater->updatesCount();
}

AbstractReviewsBackend* KNSBackend::reviewsBackend() const
{
    return m_reviews;
}

static ResultsStream* voidStream()
{
    return new ResultsStream(QStringLiteral("KNS-void"), {});
}

ResultsStream* KNSBackend::search(const AbstractResourcesBackend::Filters& filter)
{
    if (!m_isValid || (!filter.resourceUrl.isEmpty() && filter.resourceUrl.scheme() != QLatin1String("kns")) || !filter.mimetype.isEmpty())
        return voidStream();

    if (filter.resourceUrl.scheme() == QLatin1String("kns")) {
        return findResourceByPackageName(filter.resourceUrl);
    } else if (filter.state >= AbstractResource::Installed) {
        auto stream = new ResultsStream(QStringLiteral("KNS-installed"));

        const auto start = [this, stream, filter]() {
            if (m_isValid) {
                auto filterFunction = [&filter](AbstractResource* r) { return r->state()>=filter.state && (r->name().contains(filter.search, Qt::CaseInsensitive) || r->comment().contains(filter.search, Qt::CaseInsensitive)); };
                const auto ret = kFilter<QVector<AbstractResource*>>(m_resourcesByName, filterFunction);

                if (!ret.isEmpty())
                    Q_EMIT stream->resourcesFound(ret);
            }
            stream->finish();
        };
        if (isFetching()) {
            connect(this, &KNSBackend::initialized, stream, start);
        } else {
            QTimer::singleShot(0, stream, start);
        }

        return stream;
    } else if ((!filter.category && !filter.search.isEmpty()) // Accept global searches
              // If there /is/ a category, make sure we actually are one of those requested before searching
           || (filter.category && kContains(m_categories, [&filter](const QString& cat) { return filter.category->matchesCategoryName(cat); }))) {
        auto r = new ResultsStream(QLatin1String("KNS-search-")+name());
        searchStream(r, filter.search);
        return r;
    }
    return voidStream();
}

void KNSBackend::searchStream(ResultsStream* stream, const QString &searchText)
{
    Q_EMIT startingSearch();

    auto start = [this, stream, searchText]() {
        Q_ASSERT(!isFetching());
        if (!m_isValid) {
            stream->finish();
            return;
        }
        // No need to explicitly launch a search, setting the search term already does that for us
        m_engine->setSearchTerm(searchText);
        m_onePage = false;
        m_responsePending = true;

        connect(stream, &ResultsStream::fetchMore, this, &KNSBackend::fetchMore);
        connect(this, &KNSBackend::receivedResources, stream, &ResultsStream::resourcesFound);
        connect(this, &KNSBackend::searchFinished, stream, &ResultsStream::finish);
        connect(this, &KNSBackend::startingSearch, stream, &ResultsStream::finish);
    };

    if (m_responsePending) {
        connect(this, &KNSBackend::availableForQueries, stream, start, Qt::QueuedConnection);
    } else if (isFetching()) {
        connect(this, &KNSBackend::initialized, stream, start);
    } else {
        QTimer::singleShot(0, stream, start);
    }
}

ResultsStream * KNSBackend::findResourceByPackageName(const QUrl& search)
{
    if (search.scheme() != QLatin1String("kns") || search.host() != name())
        return voidStream();

#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
    const auto pathParts = search.path().split(QLatin1Char('/'), QString::SkipEmptyParts);
#else
    const auto pathParts = search.path().split(QLatin1Char('/'), Qt::SkipEmptyParts);
#endif
    if (pathParts.size() != 2) {
        Q_EMIT passiveMessage(i18n("Wrong KNewStuff URI: %1", search.toString()));
        return voidStream();
    }
    const auto providerid = pathParts.at(0);
    const auto entryid = pathParts.at(1);

    auto stream = new ResultsStream(QLatin1String("KNS-byname-")+entryid);
    auto start = [this, entryid, stream, providerid]() {
        m_responsePending = true;
        m_engine->fetchEntryById(entryid);
        m_onePage = false;

        connect(m_engine, &KNSCore::Engine::signalErrorCode, stream, &ResultsStream::finish);
        connect(m_engine, &KNSCore::Engine::signalEntryDetailsLoaded, stream, [this, stream, entryid, providerid](const KNSCore::EntryInternal &entry) {
            if (entry.uniqueId() == entryid && providerid == QUrl(entry.providerId()).host()) {
                Q_EMIT stream->resourcesFound({resourceForEntry(entry)});
            } else
                qWarning() << "found invalid" << entryid << entry.uniqueId() << providerid << QUrl(entry.providerId()).host();
            m_responsePending = false;
            QTimer::singleShot(0, this, &KNSBackend::availableForQueries);
            stream->finish();
        });
    };
    if (m_responsePending) {
        connect(this, &KNSBackend::availableForQueries, stream, start);
    } else {
        start();
    }
    return stream;
}

bool KNSBackend::isFetching() const
{
    return m_fetching;
}

AbstractBackendUpdater* KNSBackend::backendUpdater() const
{
    return m_updater;
}

QString KNSBackend::displayName() const
{
    return QStringLiteral("KNewStuff");
}

void KNSBackend::detailsLoaded(const KNSCore::EntryInternal& entry)
{
    auto res = resourceForEntry(entry);
    res->longDescriptionChanged();
}

#include "KNSBackend.moc"
