/*
    This file is part of the KDE libraries
    SPDX-FileCopyrightText: 1999 Reginald Stadlbauer <reggie@kde.org>
    SPDX-FileCopyrightText: 1999 Simon Hausmann <hausmann@kde.org>
    SPDX-FileCopyrightText: 2000 Nicolas Hadacek <haadcek@kde.org>
    SPDX-FileCopyrightText: 2000 Kurt Granroth <granroth@kde.org>
    SPDX-FileCopyrightText: 2000 Michael Koch <koch@kde.org>
    SPDX-FileCopyrightText: 2001 Holger Freyther <freyther@kde.org>
    SPDX-FileCopyrightText: 2002 Ellis Whitehead <ellis@kde.org>
    SPDX-FileCopyrightText: 2002 Joseph Wenninger <jowenn@kde.org>
    SPDX-FileCopyrightText: 2003 Andras Mantia <amantia@kde.org>
    SPDX-FileCopyrightText: 2005-2006 Hamish Rodda <rodda@kde.org>

    SPDX-License-Identifier: LGPL-2.0-only
*/

#include "krecentfilesaction.h"
#include "krecentfilesaction_p.h"

#include <QActionGroup>
#include <QDir>
#include <QGuiApplication>
#include <QMenu>
#include <QMimeDatabase>
#include <QScreen>

#if HAVE_QTDBUS
#include <QDBusConnectionInterface>
#include <QDBusInterface>
#include <QDBusMessage>
#endif

#include <KConfig>
#include <KConfigGroup>
#include <KLocalizedString>
#include <KShell>

#include <memory_resource>
#include <set>

KRecentFilesAction::KRecentFilesAction(QObject *parent)
    : KSelectAction(parent)
    , d_ptr(new KRecentFilesActionPrivate(this))
{
    Q_D(KRecentFilesAction);
    d->init();
}

KRecentFilesAction::KRecentFilesAction(const QString &text, QObject *parent)
    : KSelectAction(parent)
    , d_ptr(new KRecentFilesActionPrivate(this))
{
    Q_D(KRecentFilesAction);
    d->init();

    // Want to keep the ampersands
    setText(text);
}

KRecentFilesAction::KRecentFilesAction(const QIcon &icon, const QString &text, QObject *parent)
    : KSelectAction(parent)
    , d_ptr(new KRecentFilesActionPrivate(this))
{
    Q_D(KRecentFilesAction);
    d->init();

    setIcon(icon);
    // Want to keep the ampersands
    setText(text);
}

void KRecentFilesActionPrivate::init()
{
    Q_Q(KRecentFilesAction);
    delete q->menu();
    q->setMenu(new QMenu());
    q->setToolBarMode(KSelectAction::MenuMode);
    m_noEntriesAction = q->menu()->addAction(i18n("No Entries"));
    m_noEntriesAction->setObjectName(QStringLiteral("no_entries"));
    m_noEntriesAction->setEnabled(false);
    clearSeparator = q->menu()->addSeparator();
    clearSeparator->setVisible(false);
    clearSeparator->setObjectName(QStringLiteral("separator"));
    clearAction = q->menu()->addAction(QIcon::fromTheme(QStringLiteral("edit-clear-history")), i18n("Clear List"), q, &KRecentFilesAction::clear);
    clearAction->setObjectName(QStringLiteral("clear_action"));
    clearAction->setVisible(false);
    q->setEnabled(false);
    q->connect(q, &KSelectAction::actionTriggered, q, [this](QAction *action) {
        urlSelected(action);
    });

    q->connect(q->menu(), &QMenu::aboutToShow, q, [q] {
        std::vector<RecentActionInfo> &recentActions = q->d_ptr->m_recentActions;
        // Set icons lazily based on the mimetype
        for (auto action : recentActions) {
            if (action.action->icon().isNull()) {
                if (!action.mimeType.isValid()) {
                    action.mimeType = QMimeDatabase().mimeTypeForFile(action.url.path(), QMimeDatabase::MatchExtension);
                }

                if (!action.mimeType.isDefault()) {
                    action.action->setIcon(QIcon::fromTheme(action.mimeType.iconName()));
                }
            }
        }
    });
}

KRecentFilesAction::~KRecentFilesAction() = default;

void KRecentFilesActionPrivate::urlSelected(QAction *action)
{
    Q_Q(KRecentFilesAction);

    auto it = findByAction(action);

    Q_ASSERT(it != m_recentActions.cend()); // Should never happen

    const QUrl url = it->url; // BUG: 461448; see iterator invalidation rules
    Q_EMIT q->urlSelected(url);
}

// TODO: remove this helper function, it will crash if you use it in a loop
void KRecentFilesActionPrivate::removeAction(std::vector<RecentActionInfo>::iterator it)
{
    Q_Q(KRecentFilesAction);
    delete q->KSelectAction::removeAction(it->action);
    m_recentActions.erase(it);
}

int KRecentFilesAction::maxItems() const
{
    Q_D(const KRecentFilesAction);
    return d->m_maxItems;
}

void KRecentFilesAction::setMaxItems(int maxItems)
{
    Q_D(KRecentFilesAction);
    // set new maxItems
    d->m_maxItems = std::max(maxItems, 0);

    // Remove all excess items, oldest (i.e. first added) first
    const int difference = static_cast<int>(d->m_recentActions.size()) - d->m_maxItems;
    if (difference > 0) {
        auto beginIt = d->m_recentActions.begin();
        auto endIt = d->m_recentActions.begin() + difference;
        for (auto it = beginIt; it < endIt; ++it) {
            // Remove the action from the menus, action groups ...etc
            delete KSelectAction::removeAction(it->action);
        }
        d->m_recentActions.erase(beginIt, endIt);
    }
}

static QString titleWithSensibleWidth(const QString &nameValue, const QString &value)
{
    // Calculate 3/4 of screen geometry, we do not want
    // action titles to be bigger than that
    // Since we do not know in which screen we are going to show
    // we choose the min of all the screens
    int maxWidthForTitles = INT_MAX;
    const auto screens = QGuiApplication::screens();
    for (QScreen *screen : screens) {
        maxWidthForTitles = qMin(maxWidthForTitles, screen->availableGeometry().width() * 3 / 4);
    }
    const QFontMetrics fontMetrics = QFontMetrics(QFont());

    QString title = nameValue + QLatin1String(" [") + value + QLatin1Char(']');
    const int nameWidth = fontMetrics.boundingRect(title).width();
    if (nameWidth > maxWidthForTitles) {
        // If it does not fit, try to cut only the whole path, though if the
        // name is too long (more than 3/4 of the whole text) we cut it a bit too
        const int nameValueMaxWidth = maxWidthForTitles * 3 / 4;
        QString cutNameValue;
        QString cutValue;
        if (nameWidth > nameValueMaxWidth) {
            cutNameValue = fontMetrics.elidedText(nameValue, Qt::ElideMiddle, nameValueMaxWidth);
            cutValue = fontMetrics.elidedText(value, Qt::ElideMiddle, maxWidthForTitles - nameValueMaxWidth);
        } else {
            cutNameValue = nameValue;
            cutValue = fontMetrics.elidedText(value, Qt::ElideMiddle, maxWidthForTitles - nameWidth);
        }
        title = cutNameValue + QLatin1String(" [") + cutValue + QLatin1Char(']');
    }
    return title;
}

void KRecentFilesAction::addUrl(const QUrl &url, const QString &name)
{
    addUrl(url, name, QString());
}

void KRecentFilesAction::addUrl(const QUrl &url, const QString &name, const QString &mimeTypeStr)
{
    Q_D(KRecentFilesAction);

    // ensure we never add items if we want none
    if (d->m_maxItems == 0) {
        return;
    }

    if (url.isLocalFile() && url.toLocalFile().startsWith(QDir::tempPath())) {
        return;
    }

    // Remove url if it already exists in the list
    removeUrl(url);

    // Remove oldest item if already maxItems in list
    Q_ASSERT(d->m_maxItems > 0);
    if (static_cast<int>(d->m_recentActions.size()) == d->m_maxItems) {
        d->removeAction(d->m_recentActions.begin());
    }

    const QString pathOrUrl(url.toDisplayString(QUrl::PreferLocalFile));
    const QString tmpName = !name.isEmpty() ? name : url.fileName();
#ifdef Q_OS_WIN
    const QString file = url.isLocalFile() ? QDir::toNativeSeparators(pathOrUrl) : pathOrUrl;
#else
    const QString file = pathOrUrl;
#endif

    d->m_noEntriesAction->setVisible(false);
    d->clearSeparator->setVisible(true);
    d->clearAction->setVisible(true);
    setEnabled(true);
    // add file to list
    const QString title = titleWithSensibleWidth(tmpName, KShell::tildeCollapse(file));

#if HAVE_QTDBUS
    static bool isKdeSession = qgetenv("XDG_CURRENT_DESKTOP") == "KDE";
    if (isKdeSession) {
        const QDBusConnection bus = QDBusConnection::sessionBus();
        if (bus.isConnected()
            && bus.interface()->isServiceRegistered(QStringLiteral("org.kde.ActivityManager"))
            // skip files in hidden directories
            && !url.path().contains(QStringLiteral("/."))) {
            const static QString activityService = QStringLiteral("org.kde.ActivityManager");
            const static QString activityResources = QStringLiteral("/ActivityManager/Resources");
            const static QString activityResouceInferface = QStringLiteral("org.kde.ActivityManager.Resources");

            QString mimeTypeName = mimeTypeStr;
            if (mimeTypeName.isEmpty()) {
                mimeTypeName = QMimeDatabase().mimeTypeForFile(url.path(), QMimeDatabase::MatchExtension).name();
            }

            const auto urlString = url.toString(QUrl::PreferLocalFile);
            QDBusMessage message =
                QDBusMessage::createMethodCall(activityService, activityResources, activityResouceInferface, QStringLiteral("RegisterResourceEvent"));
            message.setArguments({qApp->desktopFileName(), uint(0) /* WinId */, urlString, uint(0) /* eventType Accessed */});
            bus.asyncCall(message);

            message = QDBusMessage::createMethodCall(activityService, activityResources, activityResouceInferface, QStringLiteral("RegisterResourceMimetype"));
            message.setArguments({urlString, mimeTypeName});
            bus.asyncCall(message);

            message = QDBusMessage::createMethodCall(activityService, activityResources, activityResouceInferface, QStringLiteral("RegisterResourceTitle"));
            message.setArguments({urlString, url.fileName()});
            bus.asyncCall(message);
        }
    }
#endif

    QAction *action = new QAction(title, selectableActionGroup());
    addAction(action, url, tmpName, QMimeType());
}

void KRecentFilesAction::addAction(QAction *action, const QUrl &url, const QString &name, const QMimeType &mimeType)
{
    Q_D(KRecentFilesAction);
    menu()->insertAction(menu()->actions().value(0), action);
    d->m_recentActions.push_back({action, url, name, mimeType});
}

QAction *KRecentFilesAction::removeAction(QAction *action)
{
    Q_D(KRecentFilesAction);
    auto it = d->findByAction(action);
    Q_ASSERT(it != d->m_recentActions.cend());
    d->m_recentActions.erase(it);
    return KSelectAction::removeAction(action);
}

void KRecentFilesAction::removeUrl(const QUrl &url)
{
    Q_D(KRecentFilesAction);

    auto it = d->findByUrl(url);

    if (it != d->m_recentActions.cend()) {
        d->removeAction(it);
    };
}

QList<QUrl> KRecentFilesAction::urls() const
{
    Q_D(const KRecentFilesAction);

    QList<QUrl> list;
    list.reserve(d->m_recentActions.size());

    using Info = KRecentFilesActionPrivate::RecentActionInfo;
    // Reverse order to match how the actions appear in the menu
    std::transform(d->m_recentActions.crbegin(), d->m_recentActions.crend(), std::back_inserter(list), [](const Info &info) {
        return info.url;
    });

    return list;
}

void KRecentFilesAction::clear()
{
    clearEntries();
    Q_EMIT recentListCleared();
}

void KRecentFilesAction::clearEntries()
{
    Q_D(KRecentFilesAction);
    KSelectAction::clear();
    d->m_recentActions.clear();
    d->m_noEntriesAction->setVisible(true);
    d->clearSeparator->setVisible(false);
    d->clearAction->setVisible(false);
    setEnabled(false);
}

void KRecentFilesAction::loadEntries(const KConfigGroup &_config)
{
    Q_D(KRecentFilesAction);
    clearEntries();

    QString value;
    QString nameValue;
    QString title;
    QUrl url;

    KConfigGroup cg = _config;
    // "<default>" means the group was constructed with an empty name
    if (cg.name() == QLatin1String("<default>")) {
        cg = KConfigGroup(cg.config(), QStringLiteral("RecentFiles"));
    }

    std::byte memory[sizeof(QUrl) * 32];
    std::pmr::monotonic_buffer_resource resource(memory, sizeof(memory));
    std::pmr::set<QUrl> seenUrls(&resource);

    bool thereAreEntries = false;
    // read file list
    for (int i = 1; i <= d->m_maxItems; i++) {
        char key[128]{};
        std::snprintf(key, sizeof key, "File%d", i);
        value = cg.readPathEntry(key, QString());
        if (value.isEmpty()) {
            continue;
        }
        url = QUrl::fromUserInput(value);

        auto [it, isNewUrl] = seenUrls.insert(url);
        // Don't restore if this url has already been restored (e.g. broken config)
        if (!isNewUrl) {
            continue;
        }

#ifdef Q_OS_WIN
        // convert to backslashes
        if (url.isLocalFile()) {
            value = QDir::toNativeSeparators(value);
        }
#endif

        char nameKey[128]{};
        std::snprintf(nameKey, sizeof nameKey, "Name%d", i);
        nameValue = cg.readPathEntry(nameKey, url.fileName());
        title = titleWithSensibleWidth(nameValue, KShell::tildeCollapse(value));
        if (!value.isNull()) {
            thereAreEntries = true;
            addAction(new QAction(title, selectableActionGroup()), url, nameValue);
        }
    }
    if (thereAreEntries) {
        d->m_noEntriesAction->setVisible(false);
        d->clearSeparator->setVisible(true);
        d->clearAction->setVisible(true);
        setEnabled(true);
    }
}

void KRecentFilesAction::saveEntries(const KConfigGroup &_cg)
{
    Q_D(KRecentFilesAction);

    KConfigGroup cg = _cg;
    // "<default>" means the group was constructed with an empty name
    if (cg.name() == QLatin1String("<default>")) {
        cg = KConfigGroup(cg.config(), QStringLiteral("RecentFiles"));
    }

    cg.deleteGroup();

    // write file list
    int i = 1;
    for (const auto &[action, url, shortName, _] : d->m_recentActions) {
        char key[128]{};
        std::snprintf(key, sizeof key, "File%d", i);
        cg.writePathEntry(key, url.toDisplayString(QUrl::PreferLocalFile));

        char nameKey[128]{};
        std::snprintf(nameKey, sizeof nameKey, "Name%d", i);
        cg.writePathEntry(nameKey, shortName);

        ++i;
    }
}

#include "moc_krecentfilesaction.cpp"
