/*
    SPDX-FileCopyrightText: 2007 Glenn Ergeerts <glenn.ergeerts@telenet.be>
    SPDX-FileCopyrightText: 2012 Marco Gulino <marco.gulino@xpeppers.com>
    SPDX-FileCopyrightText: 2023 Alexander Lohnau <alexander.lohnau@gmx.de>

    SPDX-License-Identifier: LGPL-2.0-or-later
*/

#include "firefox.h"
#include "bookmarkmatch.h"
#include "bookmarks_debug.h"
#include "favicon.h"
#include "faviconfromblob.h"
#include "fetchsqlite.h"
#include <KConfig>
#include <KConfigGroup>
#include <QFile>
#include <QRegularExpression>
#include <QStandardPaths>

using namespace Qt::StringLiterals;

Firefox::Firefox(const QString &firefoxConfigDir)
    : QObject()
    , m_dbCacheFile(QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation)
                    + QStringLiteral("/bookmarksrunner/bookmarkrunnerfirefoxdbfile.sqlite"))
    , m_dbCacheFile_fav(QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation)
                        + QStringLiteral("/bookmarksrunner/bookmarkrunnerfirefoxfavdbfile.sqlite"))
    , m_favicon(new FallbackFavicon)
    , m_fetchsqlite(nullptr)
{
    if (!QSqlDatabase::isDriverAvailable(QStringLiteral("QSQLITE"))) {
        qCWarning(RUNNER_BOOKMARKS) << "SQLITE driver isn't available";
        return;
    }

    // Try to get the right database file, the default profile is used
    KConfig firefoxProfile(QString(firefoxConfigDir + u"/profiles.ini"), KConfig::SimpleConfig);
    QStringList profilesList = firefoxProfile.groupList();
    profilesList = profilesList.filter(QRegularExpression(QStringLiteral("^Profile\\d+$")));

    QString profilePath;
    if (profilesList.size() == 1) {
        // There is only 1 profile so we select it
        KConfigGroup fGrp = firefoxProfile.group(profilesList.constFirst());
        profilePath = fGrp.readEntry("Path");
    } else {
        const QStringList installConfig = firefoxProfile.groupList().filter(QRegularExpression(u"^Install.*"_s));
        // The profile with Default=1 is not always the default profile, see BUG: 418526
        // If there is only one Install* group it contains the default profile
        if (installConfig.size() == 1) {
            profilePath = firefoxProfile.group(installConfig.first()).readEntry("Default");
        } else {
            // There are multiple profiles, find the default one
            for (const QString &profileName : std::as_const(profilesList)) {
                KConfigGroup fGrp = firefoxProfile.group(profileName);
                if (fGrp.readEntry<int>("Default", 0)) {
                    profilePath = fGrp.readEntry("Path");
                    break;
                }
            }
        }
    }
    if (profilePath.isEmpty()) {
        qCWarning(RUNNER_BOOKMARKS) << "No default firefox profile found";
        return;
    }
    profilePath.prepend(firefoxConfigDir + u'/');
    m_dbFile = profilePath + u"/places.sqlite";
    m_dbFile_fav = profilePath + u"/favicons.sqlite";

    // We can reuse the favicon instance over the lifetime of the plugin consequently the
    // icons that are already written to disk can be reused in multiple match sessions
    updateCacheFile(m_dbFile_fav, m_dbCacheFile_fav);
    auto fetchsqlite_fav = std::make_unique<FetchSqlite>(m_dbCacheFile_fav);
    m_favicon = FaviconFromBlob::firefox(std::move(fetchsqlite_fav));
}

Firefox::~Firefox()
{
    // Delete the cached databases
    if (!m_dbFile.isEmpty()) {
        QFile db_CacheFile(m_dbCacheFile);
        if (db_CacheFile.exists()) {
            db_CacheFile.remove();
        }
    }
    if (!m_dbFile_fav.isEmpty()) {
        QFile db_CacheFileFav(m_dbCacheFile_fav);
        if (db_CacheFileFav.exists()) {
            db_CacheFileFav.remove();
        }
    }
}

void Firefox::prepare()
{
    if (updateCacheFile(m_dbFile, m_dbCacheFile) != Error) {
        m_fetchsqlite = std::make_unique<FetchSqlite>(m_dbCacheFile);
        m_fetchsqlite->prepare();
    }
    updateCacheFile(m_dbFile_fav, m_dbCacheFile_fav);
    m_favicon->prepare();
}

QList<BookmarkMatch> Firefox::match(const QString &term, bool addEverything)
{
    QList<BookmarkMatch> matches;
    if (!m_fetchsqlite) {
        return matches;
    }

    QString query;
    if (addEverything) {
        query = QStringLiteral(
            "SELECT moz_bookmarks.fk, moz_bookmarks.title, moz_places.url "
            "FROM moz_bookmarks, moz_places WHERE "
            "moz_bookmarks.type = 1 AND moz_bookmarks.fk = moz_places.id");
    } else {
        query = QStringLiteral(
            "SELECT moz_bookmarks.fk, moz_bookmarks.title, moz_places.url "
            "FROM moz_bookmarks, moz_places WHERE "
            "moz_bookmarks.type = 1 AND moz_bookmarks.fk = moz_places.id AND "
            "(moz_bookmarks.title LIKE :term OR moz_places.url LIKE :term)");
    }
    const QMap<QString, QVariant> bindVariables{
        {QStringLiteral(":term"), QStringLiteral("%%%1%%").arg(term)},
    };
    const QList<QVariantMap> results = m_fetchsqlite->query(query, bindVariables);
    QMultiMap<QString, QString> uniqueResults;
    for (const QVariantMap &result : results) {
        const QString title = result.value(QStringLiteral("title")).toString();
        const QUrl url = result.value(QStringLiteral("url")).toUrl();
        if (url.isEmpty() || url.scheme() == QLatin1String("place")) {
            // Don't use bookmarks with empty url or Firefox's "place:" scheme,
            // e.g. used for "Most Visited" or "Recent Tags"
            // qDebug() << "element " << url << " was not added";
            continue;
        }

        auto urlString = url.toString();
        // After joining we may have multiple results for each URL:
        // 1) one for each bookmark folder (same or different titles)
        // 2) one for each tag (no title for all but the first entry)
        auto keyRange = uniqueResults.equal_range(urlString);
        auto it = keyRange.first;
        if (!title.isEmpty()) {
            while (it != keyRange.second) {
                if (*it == title) {
                    // same URL and title in multiple bookmark folders
                    break;
                }
                if (it->isEmpty()) {
                    // add a title if there was none for the URL
                    *it = title;
                    break;
                }
                ++it;
            }
        }
        if (it == keyRange.second) {
            // first or unique entry
            uniqueResults.insert(urlString, title);
        }
    }

    for (auto result = uniqueResults.constKeyValueBegin(); result != uniqueResults.constKeyValueEnd(); ++result) {
        const QString url = (*result).first;
        BookmarkMatch bookmarkMatch(term, (*result).second, url);
        if (addEverything || bookmarkMatch.matches()) {
            bookmarkMatch.setIcon(m_favicon->iconFor(url));
            matches << bookmarkMatch;
        }
    }

    return matches;
}

void Firefox::teardown()
{
    if (m_fetchsqlite) {
        m_fetchsqlite->teardown();
    }
    m_favicon->teardown();
}

#include "moc_firefox.cpp"
