/*

    SPDX-FileCopyrightText: Andrew Stanley-Jones <asj@cban.com>
    SPDX-FileCopyrightText: 2000 Carsten Pfeiffer <pfeiffer@kde.org>
    SPDX-FileCopyrightText: 2004 Esben Mose Hansen <kde@mosehansen.dk>
    SPDX-FileCopyrightText: 2008 Dmitry Suzdalev <dimsuz@gmail.com>

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

#include "klipper.h"

#include <unordered_set>
#include <zlib.h>

#include "klipper_debug.h"
#include <QApplication>
#include <QBoxLayout>
#include <QDBusConnection>
#include <QDialog>
#include <QDir>
#include <QLabel>
#include <QMenu>
#include <QMessageBox>
#include <QPushButton>
#include <QSaveFile>
#include <QtConcurrent>

#include <KAboutData>
#include <KActionCollection>
#include <KGlobalAccel>
#include <KHelpMenu>
#include <KLocalizedString>
#include <KMessageBox>
#include <KNotification>
#include <KSystemClipboard>
#include <KToggleAction>
#include <KWayland/Client/connection_thread.h>
#include <KWayland/Client/plasmashell.h>
#include <KWayland/Client/registry.h>
#include <KWayland/Client/surface.h>
#include <KWindowSystem>

#include "../c_ptr.h"
#include "configdialog.h"
#include "history.h"
#include "historyitem.h"
#include "historymodel.h"
#include "historystringitem.h"
#include "klipperpopup.h"
#include "klippersettings.h"

#include <Prison/Prison>

#include <config-X11.h>
#if HAVE_X11
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
#include <private/qtx11extras_p.h>
#else
#include <QX11Info>
#endif
#include <chrono>
#include <xcb/xcb.h>

using namespace std::chrono_literals;
#endif

namespace
{
/**
 * Use this when manipulating the clipboard
 * from within clipboard-related signals.
 *
 * This avoids issues such as mouse-selections that immediately
 * disappear.
 * pattern: Resource Acquisition is Initialisation (RAII)
 *
 * (This is not threadsafe, so don't try to use such in threaded
 * applications).
 */
struct Ignore {
    Ignore(int &locklevel)
        : locklevelref(locklevel)
    {
        locklevelref++;
    }
    ~Ignore()
    {
        locklevelref--;
    }

private:
    int &locklevelref;
};
}

ClipboardContentTextEdit::ClipboardContentTextEdit(QWidget *parent)
    : KTextEdit(parent)
{
}

void ClipboardContentTextEdit::keyPressEvent(QKeyEvent *event)
{
    // Handle Ctrl+Enter to accept
    const int key = event->key();
    if (key == Qt::Key_Return || key == Qt::Key_Enter) {
        if ((key == Qt::Key_Enter && (event->modifiers() == Qt::KeypadModifier)) || !event->modifiers()) {
            Q_EMIT done();
            event->accept();
            return;
        }
    }
    KTextEdit::keyPressEvent(event);
}

// config == KGlobal::config for process, otherwise applet
Klipper::Klipper(QObject *parent, const KSharedConfigPtr &config, KlipperMode mode)
    : QObject(parent)
    , m_overflowCounter(0)
    , m_quitAction(nullptr)
    , m_selectionLocklevel(0)
    , m_clipboardLocklevel(0)
    , m_config(config)
    , m_pendingContentsCheck(false)
    , m_mode(mode)
    , m_saveFileTimer(nullptr)
    , m_plasmashell(nullptr)
{
    if (m_mode == KlipperMode::Standalone) {
        setenv("KSNI_NO_DBUSMENU", "1", 1);
    }
    QDBusConnection::sessionBus().registerService(QStringLiteral("org.kde.klipper"));
    QDBusConnection::sessionBus().registerObject(QStringLiteral("/klipper"), this, QDBusConnection::ExportScriptableSlots);

    updateTimestamp(); // read initial X user time
    m_clip = KSystemClipboard::instance();

    connect(m_clip, &KSystemClipboard::changed, this, &Klipper::newClipData);

    connect(&m_overflowClearTimer, &QTimer::timeout, this, &Klipper::slotClearOverflow);

    m_pendingCheckTimer.setSingleShot(true);
    connect(&m_pendingCheckTimer, &QTimer::timeout, this, &Klipper::slotCheckPending);

    m_history = new History(this);
    m_popup = new KlipperPopup(m_history);
    m_popup->setWindowFlags(m_popup->windowFlags() | Qt::FramelessWindowHint);
    connect(m_history, &History::changed, this, &Klipper::slotHistoryChanged);
    connect(m_history, &History::changed, m_popup, &KlipperPopup::slotHistoryChanged);
    connect(m_history, &History::topIsUserSelectedSet, m_popup, &KlipperPopup::slotTopIsUserSelectedSet);

    // we need that collection, otherwise KToggleAction is not happy :}
    m_collection = new KActionCollection(this);

    m_toggleURLGrabAction = new KToggleAction(this);
    m_collection->addAction(QStringLiteral("clipboard_action"), m_toggleURLGrabAction);
    m_toggleURLGrabAction->setText(i18nc("@action:inmenu Toggle automatic action", "Automatic Action Popup Menu"));
    KGlobalAccel::setGlobalShortcut(m_toggleURLGrabAction, QKeySequence(Qt::META | Qt::CTRL | Qt::Key_X));
    connect(m_toggleURLGrabAction, &QAction::toggled, this, &Klipper::setURLGrabberEnabled);

    /*
     * Create URL grabber
     */
    m_myURLGrabber = new URLGrabber(m_history);
    connect(m_myURLGrabber, &URLGrabber::sigPopup, this, &Klipper::showPopupMenu);
    connect(m_myURLGrabber, &URLGrabber::sigDisablePopup, this, &Klipper::disableURLGrabber);

    /*
     * Load configuration settings
     */
    loadSettings();

    // load previous history if configured
    if (m_bKeepContents) {
        loadHistory();
    }

    m_saveFileTimer = new QTimer(this);
    m_saveFileTimer->setSingleShot(true);
    m_saveFileTimer->setInterval(5s);
    connect(m_saveFileTimer, &QTimer::timeout, this, [this] {
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
        QtConcurrent::run(this, &Klipper::saveHistory, false);
#else
        QtConcurrent::run(&Klipper::saveHistory, this, false);
#endif
    });
    connect(m_history, &History::changed, this, [this] {
        if (m_bKeepContents) {
            m_saveFileTimer->start();
        }
    }); // only connect this signal after loading the history, to avoid the action of loading triggering a save

    m_clearHistoryAction = m_collection->addAction(QStringLiteral("clear-history"));
    m_clearHistoryAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-clear-history")));
    m_clearHistoryAction->setText(i18nc("@action:inmenu", "C&lear Clipboard History"));
    KGlobalAccel::setGlobalShortcut(m_clearHistoryAction, QKeySequence());
    connect(m_clearHistoryAction, &QAction::triggered, this, &Klipper::slotAskClearHistory);

    QString CONFIGURE = QStringLiteral("configure");
    m_configureAction = m_collection->addAction(CONFIGURE);
    m_configureAction->setIcon(QIcon::fromTheme(CONFIGURE));
    m_configureAction->setText(i18nc("@action:inmenu", "&Configure Klipper…"));
    connect(m_configureAction, &QAction::triggered, this, &Klipper::slotConfigure);

    if (KlipperMode::Standalone == m_mode) {
        m_quitAction = m_collection->addAction(QStringLiteral("quit"));
        m_quitAction->setIcon(QIcon::fromTheme(QStringLiteral("application-exit")));
        m_quitAction->setText(i18nc("@action:inmenu Quit Klipper", "&Quit"));
        connect(m_quitAction, &QAction::triggered, this, &Klipper::slotQuit);
    }

    m_repeatAction = m_collection->addAction(QStringLiteral("repeat_action"));
    m_repeatAction->setText(i18nc("@action:inmenu", "Manually Invoke Action on Current Clipboard"));
    m_repeatAction->setIcon(QIcon::fromTheme(QStringLiteral("open-menu-symbolic")));
    KGlobalAccel::setGlobalShortcut(m_repeatAction, QKeySequence(Qt::META | Qt::CTRL | Qt::Key_R));
    connect(m_repeatAction, &QAction::triggered, this, &Klipper::slotRepeatAction);

    // add an edit-possibility
    m_editAction = m_collection->addAction(QStringLiteral("edit_clipboard"));
    m_editAction->setIcon(QIcon::fromTheme(QStringLiteral("document-properties")));
    m_editAction->setText(i18nc("@action:inmenu", "&Edit Contents…"));
    KGlobalAccel::setGlobalShortcut(m_editAction, QKeySequence());
    connect(m_editAction, &QAction::triggered, this, [this]() {
        editData(m_history->first());
    });

    // add barcode for mobile phones
    m_showBarcodeAction = m_collection->addAction(QStringLiteral("show-barcode"));
    m_showBarcodeAction->setText(i18nc("@action:inmenu", "&Show Barcode…"));
    m_showBarcodeAction->setIcon(QIcon::fromTheme(QStringLiteral("view-barcode-qr")));
    KGlobalAccel::setGlobalShortcut(m_showBarcodeAction, QKeySequence());
    connect(m_showBarcodeAction, &QAction::triggered, this, [this]() {
        showBarcode(m_history->first());
    });

    // Cycle through history
    m_cycleNextAction = m_collection->addAction(QStringLiteral("cycleNextAction"));
    m_cycleNextAction->setText(i18nc("@action:inmenu", "Next History Item"));
    m_cycleNextAction->setIcon(QIcon::fromTheme(QStringLiteral("go-next")));
    KGlobalAccel::setGlobalShortcut(m_cycleNextAction, QKeySequence());
    connect(m_cycleNextAction, &QAction::triggered, this, &Klipper::slotCycleNext);
    m_cyclePrevAction = m_collection->addAction(QStringLiteral("cyclePrevAction"));
    m_cyclePrevAction->setText(i18nc("@action:inmenu", "Previous History Item"));
    m_cyclePrevAction->setIcon(QIcon::fromTheme(QStringLiteral("go-previous")));
    KGlobalAccel::setGlobalShortcut(m_cyclePrevAction, QKeySequence());
    connect(m_cyclePrevAction, &QAction::triggered, this, &Klipper::slotCyclePrev);

    // Action to show items popup on mouse position
    m_showOnMousePos = m_collection->addAction(QStringLiteral("show-on-mouse-pos"));
    m_showOnMousePos->setText(i18nc("@action:inmenu", "Show Items at Mouse Position"));
    m_showOnMousePos->setIcon(QIcon::fromTheme(QStringLiteral("view-list-text")));
    KGlobalAccel::setGlobalShortcut(m_showOnMousePos, QKeySequence(Qt::META | Qt::Key_V));
    connect(m_showOnMousePos, &QAction::triggered, this, &Klipper::slotPopupMenu);

    connect(history(), &History::topChanged, this, &Klipper::slotHistoryTopChanged);

    if (m_mode == KlipperMode::Standalone) {
        // system tray popup menu
        m_actionsPopup = new QMenu;
        m_actionsPopup->addSection(QIcon::fromTheme(QStringLiteral("klipper")),
                                   i18nc("%1 is application display name", "%1 - Clipboard Tool", QGuiApplication::applicationDisplayName()));
        m_actionsPopup->addAction(m_toggleURLGrabAction);
        m_actionsPopup->addAction(m_clearHistoryAction);
        m_actionsPopup->addAction(m_configureAction);
        m_actionsPopup->addAction(m_repeatAction);
        m_actionsPopup->addAction(m_editAction);
        m_actionsPopup->addAction(m_showBarcodeAction);

        m_actionsPopup->addSeparator();

        QMenu *helpMenu = (new KHelpMenu(m_actionsPopup, KAboutData::applicationData(), false))->menu();
        helpMenu->setIcon(QIcon::fromTheme(QStringLiteral("help-contents")));
        m_actionsPopup->addMenu(helpMenu);

        Q_ASSERT(m_quitAction);
        m_actionsPopup->addAction(m_quitAction);
        connect(m_actionsPopup, &QMenu::aboutToShow, this, &Klipper::slotStartShowTimer);
    }

    // session manager interaction
    if (m_mode == KlipperMode::Standalone) {
        connect(qApp, &QGuiApplication::commitDataRequest, this, &Klipper::saveSession);
    }

    connect(this, &Klipper::passivePopup, this, [this](const QString &caption, const QString &text) {
        if (m_notification) {
            m_notification->setTitle(caption);
            m_notification->setText(text);
        } else {
            m_notification = KNotification::event(KNotification::Notification, caption, text, QStringLiteral("klipper"));
            // When Klipper is run as part of plasma, we still need to pretend to be it for notification settings to work
            m_notification->setHint(QStringLiteral("desktop-entry"), QStringLiteral("org.kde.klipper"));
        }
    });

    if (KWindowSystem::isPlatformWayland()) {
        auto registry = new KWayland::Client::Registry(this);
        auto connection = KWayland::Client::ConnectionThread::fromApplication(qGuiApp);
        connect(registry, &KWayland::Client::Registry::plasmaShellAnnounced, this, [registry, this](quint32 name, quint32 version) {
            if (!m_plasmashell) {
                m_plasmashell = registry->createPlasmaShell(name, version);
            }
        });
        registry->create(connection);
        registry->setup();
    }
}

Klipper::~Klipper()
{
    delete m_myURLGrabber;
}

// DBUS
QString Klipper::getClipboardContents()
{
    return getClipboardHistoryItem(0);
}

void Klipper::showKlipperPopupMenu()
{
    slotPopupMenu();
}

void Klipper::showKlipperManuallyInvokeActionMenu()
{
    slotRepeatAction();
}

// DBUS - don't call from Klipper itself
void Klipper::setClipboardContents(const QString &s)
{
    if (s.isEmpty())
        return;
    Ignore selectionLock(m_selectionLocklevel);
    Ignore clipboardLock(m_clipboardLocklevel);
    updateTimestamp();
    HistoryItemPtr item(HistoryItemPtr(new HistoryStringItem(s)));
    setClipboard(*item, Clipboard | Selection);
    history()->insert(item);
}

// DBUS - don't call from Klipper itself
void Klipper::clearClipboardContents()
{
    updateTimestamp();
    slotClearClipboard();
}

// DBUS - don't call from Klipper itself
void Klipper::clearClipboardHistory()
{
    updateTimestamp();
    history()->slotClear();
    saveSession();
}

// DBUS - don't call from Klipper itself
void Klipper::saveClipboardHistory()
{
    if (m_bKeepContents) { // save the clipboard eventually
        saveHistory();
    }
}

void Klipper::slotStartShowTimer()
{
    m_showTimer.start();
}

void Klipper::loadSettings()
{
    // Security bug 142882: If user has save clipboard turned off, old data should be deleted from disk
    static bool firstrun = true;
    if (!firstrun && m_bKeepContents && !KlipperSettings::keepClipboardContents()) {
        saveHistory(true);
    }
    firstrun = false;

    m_bKeepContents = KlipperSettings::keepClipboardContents();
    m_bReplayActionInHistory = KlipperSettings::replayActionInHistory();
    m_bNoNullClipboard = KlipperSettings::preventEmptyClipboard();
    // 0 is the id of "Ignore selection" radiobutton
    m_bIgnoreSelection = KlipperSettings::ignoreSelection();
    m_bIgnoreImages = KlipperSettings::ignoreImages();
    m_bSynchronize = KlipperSettings::syncClipboards();
    // NOTE: not used atm - kregexpeditor is not ported to kde4
    m_bUseGUIRegExpEditor = KlipperSettings::useGUIRegExpEditor();
    m_bSelectionTextOnly = KlipperSettings::selectionTextOnly();

    m_bURLGrabber = KlipperSettings::uRLGrabberEnabled();
    // this will cause it to loadSettings too
    setURLGrabberEnabled(m_bURLGrabber);
    history()->setMaxSize(KlipperSettings::maxClipItems());
    history()->model()->setDisplayImages(!m_bIgnoreImages);

    // Convert 4.3 settings
    if (KlipperSettings::synchronize() != 3) {
        // 2 was the id of "Ignore selection" radiobutton
        m_bIgnoreSelection = KlipperSettings::synchronize() == 2;
        // 0 was the id of "Synchronize contents" radiobutton
        m_bSynchronize = KlipperSettings::synchronize() == 0;
        KConfigSkeletonItem *item = KlipperSettings::self()->findItem(QStringLiteral("SyncClipboards"));
        item->setProperty(m_bSynchronize);
        item = KlipperSettings::self()->findItem(QStringLiteral("IgnoreSelection"));
        item->setProperty(m_bIgnoreSelection);
        item = KlipperSettings::self()->findItem(QStringLiteral("Synchronize")); // Mark property as converted.
        item->setProperty(3);
        KlipperSettings::self()->save();
        KlipperSettings::self()->load();
    }
}

void Klipper::saveSettings() const
{
    m_myURLGrabber->saveSettings();
    KlipperSettings::self()->setVersion(QStringLiteral(KLIPPER_VERSION_STRING));
    KlipperSettings::self()->save();

    // other settings should be saved automatically by KConfigDialog
}

void Klipper::showPopupMenu(QMenu *menu)
{
    Q_ASSERT(menu != nullptr);
    if (m_plasmashell) {
        menu->hide();
    }
    menu->popup(QCursor::pos());
    if (m_plasmashell) {
        menu->windowHandle()->installEventFilter(this);
    }
}

bool Klipper::eventFilter(QObject *filtered, QEvent *event)
{
    const bool ret = QObject::eventFilter(filtered, event);
    auto menuWindow = qobject_cast<QWindow *>(filtered);
    if (menuWindow && event->type() == QEvent::Expose && menuWindow->isVisible()) {
        auto surface = KWayland::Client::Surface::fromWindow(menuWindow);
        auto plasmaSurface = m_plasmashell->createSurface(surface, menuWindow);
        plasmaSurface->openUnderCursor();
        plasmaSurface->setSkipTaskbar(true);
        plasmaSurface->setSkipSwitcher(true);
        menuWindow->removeEventFilter(this);
    }
    return ret;
}

bool Klipper::loadHistory()
{
    static const char failed_load_warning[] = "Failed to load history resource. Clipboard history cannot be read.";
    // don't use "appdata", klipper is also a kicker applet
    QFile history_file(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("klipper/history2.lst")));
    if (!history_file.exists()) {
        qCWarning(KLIPPER_LOG) << failed_load_warning << ": "
                               << "History file does not exist";
        return false;
    }
    if (!history_file.open(QIODevice::ReadOnly)) {
        qCWarning(KLIPPER_LOG) << failed_load_warning << ": " << history_file.errorString();
        return false;
    }
    QDataStream file_stream(&history_file);
    if (file_stream.atEnd()) {
        qCWarning(KLIPPER_LOG) << failed_load_warning << ": "
                               << "Error in reading data";
        return false;
    }
    QByteArray data;
    quint32 crc;
    file_stream >> crc >> data;
    if (crc32(0, reinterpret_cast<unsigned char *>(data.data()), data.size()) != crc) {
        qCWarning(KLIPPER_LOG) << failed_load_warning << ": "
                               << "CRC checksum does not match";
        return false;
    }
    QDataStream history_stream(&data, QIODevice::ReadOnly);

    char *version;
    history_stream >> version;
    delete[] version;

    QVector<HistoryItemPtr> items;
    std::unordered_set<QByteArray> uuidSet;
    for (HistoryItemPtr item = HistoryItem::create(history_stream); !item.isNull(); item = HistoryItem::create(history_stream)) {
        // BUG 466236 Due to a catastrophic bug existing in 5.27 beta and 5.27.0, there could be
        // duplicate items in klipper's history file, so filter them out here.
        if (uuidSet.count(item->uuid()) > 0) {
            qCWarning(KLIPPER_LOG) << "One item in history has the same UUID with another item, ignored.";
            continue;
        }
        items.append(item);
        uuidSet.insert(item->uuid());
    }

    history()->clearAndBatchInsert(items);

    if (!history()->empty()) {
        setClipboard(*history()->first(), Clipboard | Selection);
    }

    return true;
}

void Klipper::saveHistory(bool empty)
{
    QMutexLocker lock(m_history->model()->mutex());
    static const char failed_save_warning[] = "Failed to save history. Clipboard history cannot be saved.";
    // don't use "appdata", klipper is also a kicker applet
    QString history_file_name(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("klipper/history2.lst")));
    if (history_file_name.isNull() || history_file_name.isEmpty()) {
        // try creating the file
        QDir dir(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation));
        if (!dir.mkpath(QStringLiteral("klipper"))) {
            qCWarning(KLIPPER_LOG) << failed_save_warning;
            return;
        }
        history_file_name = dir.absoluteFilePath(QStringLiteral("klipper/history2.lst"));
    }
    if (history_file_name.isNull() || history_file_name.isEmpty()) {
        qCWarning(KLIPPER_LOG) << failed_save_warning;
        return;
    }
    QSaveFile history_file(history_file_name);
    if (!history_file.open(QIODevice::WriteOnly)) {
        qCWarning(KLIPPER_LOG) << failed_save_warning;
        return;
    }
    QByteArray data;
    QDataStream history_stream(&data, QIODevice::WriteOnly);
    history_stream << KLIPPER_VERSION_STRING; // const char*

    if (!empty) {
        HistoryItemConstPtr item = history()->first();
        if (item) {
            do {
                history_stream << item.data();
                item = HistoryItemConstPtr(history()->find(item->next_uuid()));
            } while (item != history()->first());
        }
    }

    quint32 crc = crc32(0, reinterpret_cast<unsigned char *>(data.data()), data.size());
    QDataStream ds(&history_file);
    ds << crc << data;
    if (!history_file.commit()) {
        qCWarning(KLIPPER_LOG) << failed_save_warning;
    }
}

// save session on shutdown. Don't simply use the c'tor, as that may not be called.
void Klipper::saveSession()
{
    if (m_bKeepContents) { // save the clipboard eventually
        saveHistory();
    }
    saveSettings();
}

void Klipper::disableURLGrabber()
{
    QMessageBox *message = new QMessageBox(QMessageBox::Information,
                                           QString(),
                                           xi18nc("@info",
                                                  "You can enable URL actions later in the "
                                                  "<interface>Actions</interface> page of the "
                                                  "Clipboard applet's configuration window"));
    message->setAttribute(Qt::WA_DeleteOnClose);
    message->setModal(false);
    message->show();

    setURLGrabberEnabled(false);
}

void Klipper::slotConfigure()
{
    if (KConfigDialog::showDialog(QStringLiteral("preferences"))) {
        // This will never happen, because of the WA_DeleteOnClose below.
        return;
    }

    ConfigDialog *dlg = new ConfigDialog(nullptr, KlipperSettings::self(), this, m_collection);
    QMetaObject::invokeMethod(dlg, "setHelp", Qt::DirectConnection, Q_ARG(QString, QString::fromLatin1("preferences")));
    // This is necessary to ensure that the dialog is recreated
    // and therefore the controls are initialised from the current
    // Klipper settings every time that it is shown.
    dlg->setAttribute(Qt::WA_DeleteOnClose);

    connect(dlg, &KConfigDialog::settingsChanged, this, &Klipper::loadSettings);
    dlg->show();
}

void Klipper::slotQuit()
{
    // If the menu was just opened, likely the user
    // selected quit by accident while attempting to
    // click the Klipper icon.
    if (m_showTimer.elapsed() < 300) {
        return;
    }

    saveSession();
    int autoStart = KMessageBox::questionTwoActionsCancel(nullptr,
                                                          i18n("Should Klipper start automatically when you login?"),
                                                          i18n("Automatically Start Klipper?"),
                                                          KGuiItem(i18n("Start")),
                                                          KGuiItem(i18n("Do Not Start")),
                                                          KStandardGuiItem::cancel(),
                                                          QStringLiteral("StartAutomatically"));

    KConfigGroup config(KSharedConfig::openConfig(), "General");
    if (autoStart == KMessageBox::PrimaryAction) {
        config.writeEntry("AutoStart", true);
    } else if (autoStart == KMessageBox::SecondaryAction) {
        config.writeEntry("AutoStart", false);
    } else // cancel chosen don't quit
        return;
    config.sync();

    qApp->quit();
}

void Klipper::slotPopupMenu()
{
    m_popup->ensureClean();
    m_popup->slotSetTopActive();
    showPopupMenu(m_popup);
}

void Klipper::slotRepeatAction()
{
    auto top = qSharedPointerCast<const HistoryStringItem>(history()->first());
    if (top) {
        m_myURLGrabber->invokeAction(top);
    }
}

void Klipper::setURLGrabberEnabled(bool enable)
{
    if (enable != m_bURLGrabber) {
        m_bURLGrabber = enable;
        m_lastURLGrabberTextSelection.clear();
        m_lastURLGrabberTextClipboard.clear();
        KlipperSettings::setURLGrabberEnabled(enable);
    }

    m_toggleURLGrabAction->setChecked(enable);

    // make it update its settings
    m_myURLGrabber->loadSettings();
}

void Klipper::slotHistoryTopChanged()
{
    if (m_selectionLocklevel || m_clipboardLocklevel) {
        return;
    }

    auto topitem = history()->first();
    if (topitem) {
        setClipboard(*topitem, Clipboard | Selection);
    }
    if (m_bReplayActionInHistory && m_bURLGrabber) {
        slotRepeatAction();
    }
}

void Klipper::slotClearClipboard()
{
    Ignore selectionLock(m_selectionLocklevel);
    Ignore clipboardLock(m_clipboardLocklevel);

    m_clip->clear(QClipboard::Selection);
    m_clip->clear(QClipboard::Clipboard);
}

HistoryItemPtr Klipper::applyClipChanges(const QMimeData *clipData, bool selectionMode)
{
    if (selectionMode && m_selectionLocklevel || !selectionMode && m_clipboardLocklevel) {
        return HistoryItemPtr();
    }
    Ignore lock(selectionMode ? m_selectionLocklevel : m_clipboardLocklevel);

    if (!(history()->empty())) {
        if (m_bIgnoreImages && history()->first()->type() == HistoryItemType::Image) {
            history()->remove(history()->first());
        }
    }

    HistoryItemPtr item = HistoryItem::create(clipData);

    bool saveToHistory = true;
    if (clipData->data(QStringLiteral("x-kde-passwordManagerHint")) == QByteArrayLiteral("secret")) {
        saveToHistory = false;
    }
    if (saveToHistory) {
        history()->insert(item);
    }

    return item;
}

void Klipper::newClipData(QClipboard::Mode mode)
{
    if ((mode == QClipboard::Clipboard && m_clipboardLocklevel) || (mode == QClipboard::Selection && m_selectionLocklevel)) {
        return;
    }

    if (mode == QClipboard::Selection && blockFetchingNewData())
        return;

    checkClipData(mode == QClipboard::Selection ? true : false);
}

void Klipper::slotHistoryChanged()
{
    if (history()->empty()) {
        slotClearClipboard();
    }
}

// Protection against too many clipboard data changes. Lyx responds to clipboard data
// requests with setting new clipboard data, so if Lyx takes over clipboard,
// Klipper notices, requests this data, this triggers "new" clipboard contents
// from Lyx, so Klipper notices again, requests this data, ... you get the idea.
const int MAX_CLIPBOARD_CHANGES = 10; // max changes per second

bool Klipper::blockFetchingNewData()
{
#if HAVE_X11
    // Hacks for #85198 and #80302.
    // #85198 - block fetching new clipboard contents if Shift is pressed and mouse is not,
    //   this may mean the user is doing selection using the keyboard, in which case
    //   it's possible the app sets new clipboard contents after every change - Klipper's
    //   history would list them all.
    // #80302 - OOo (v1.1.3 at least) has a bug that if Klipper requests its clipboard contents
    //   while the user is doing a selection using the mouse, OOo stops updating the clipboard
    //   contents, so in practice it's like the user has selected only the part which was
    //   selected when Klipper asked first.
    // Use XQueryPointer rather than QApplication::mouseButtons()/keyboardModifiers(), because
    //   Klipper needs the very current state.
    if (!KWindowSystem::isPlatformX11()) {
        return false;
    }
    xcb_connection_t *c = QX11Info::connection();
    const xcb_query_pointer_cookie_t cookie = xcb_query_pointer_unchecked(c, QX11Info::appRootWindow());
    UniqueCPointer<xcb_query_pointer_reply_t> queryPointer(xcb_query_pointer_reply(c, cookie, nullptr));
    if (!queryPointer) {
        return false;
    }
    if (((queryPointer->mask & (XCB_KEY_BUT_MASK_SHIFT | XCB_KEY_BUT_MASK_BUTTON_1)) == XCB_KEY_BUT_MASK_SHIFT) // BUG: 85198
        || ((queryPointer->mask & XCB_KEY_BUT_MASK_BUTTON_1) == XCB_KEY_BUT_MASK_BUTTON_1)) { // BUG: 80302
        m_pendingContentsCheck = true;
        m_pendingCheckTimer.start(100ms);
        return true;
    }
    m_pendingContentsCheck = false;
    if (m_overflowCounter == 0)
        m_overflowClearTimer.start(1s);
    if (++m_overflowCounter > MAX_CLIPBOARD_CHANGES)
        return true;
#endif
    return false;
}

void Klipper::slotCheckPending()
{
    if (!m_pendingContentsCheck)
        return;
    m_pendingContentsCheck = false; // blockFetchingNewData() will be called again
    updateTimestamp();
    newClipData(QClipboard::Selection); // always selection
}

void Klipper::checkClipData(bool selectionMode)
{
    if (ignoreClipboardChanges()) // internal to klipper, ignoring QSpinBox selections
    {
        // keep our old clipboard, thanks
        // This won't quite work, but it's close enough for now.
        // The trouble is that the top selection =! top clipboard
        // but we don't track that yet. We will....
        auto top = history()->first();
        if (top) {
            setClipboard(*top, selectionMode ? Selection : Clipboard);
        }
        return;
    }

    qCDebug(KLIPPER_LOG) << "Checking clip data";

    const QMimeData *data = m_clip->mimeData(selectionMode ? QClipboard::Selection : QClipboard::Clipboard);

    bool clipEmpty = false;
    bool changed = true; // ### FIXME (only relevant under polling, might be better to simply remove polling and rely on XFixes)
    if (!data) {
        clipEmpty = true;
    } else {
        clipEmpty = data->formats().isEmpty();
        if (clipEmpty) {
            // Might be a timeout. Try again
            clipEmpty = data->formats().isEmpty();
            qCDebug(KLIPPER_LOG) << "was empty. Retried, now " << (clipEmpty ? " still empty" : " no longer empty");
        }
    }

    if (changed && clipEmpty && m_bNoNullClipboard) {
        auto top = history()->first();
        if (top) {
            // keep old clipboard after someone set it to null
            qCDebug(KLIPPER_LOG) << "Resetting clipboard (Prevent empty clipboard)";
            setClipboard(*top, selectionMode ? Selection : Clipboard, ClipboardUpdateReason::PreventEmptyClipboard);
        }
        return;
    } else if (clipEmpty) {
        return;
    }

    // this must be below the "bNoNullClipboard" handling code!
    // XXX: I want a better handling of selection/clipboard in general.
    // XXX: Order sensitive code. Must die.
    if (selectionMode && m_bIgnoreSelection)
        return;

    if (selectionMode && m_bSelectionTextOnly && !data->hasText())
        return;

    if (data->hasUrls())
        ; // ok
    else if (data->hasText())
        ; // ok
    else if (data->hasImage()) {
        if (m_bIgnoreImages && !data->hasFormat(QStringLiteral("x-kde-force-image-copy")))
            return;
    } else // unknown, ignore
        return;

    HistoryItemPtr item = applyClipChanges(data, selectionMode);
    if (changed) {
        qCDebug(KLIPPER_LOG) << "Synchronize?" << m_bSynchronize;
        if (m_bSynchronize && item) {
            setClipboard(*item, selectionMode ? Clipboard : Selection);
        }
    }
    QString &lastURLGrabberText = selectionMode ? m_lastURLGrabberTextSelection : m_lastURLGrabberTextClipboard;
    if (m_bURLGrabber && item && data->hasText()) {
        m_myURLGrabber->checkNewData(qSharedPointerConstCast<const HistoryItem>(item));

        // Make sure URLGrabber doesn't repeat all the time if klipper reads the same
        // text all the time (e.g. because XFixes is not available and the application
        // has broken TIMESTAMP target). Using most recent history item may not always
        // work.
        if (item->text() != lastURLGrabberText) {
            lastURLGrabberText = item->text();
        }
    } else {
        lastURLGrabberText.clear();
    }
}

void Klipper::setClipboard(const HistoryItem &item, int mode, ClipboardUpdateReason updateReason)
{
    Ignore lock(mode == Selection ? m_selectionLocklevel : m_clipboardLocklevel);

    Q_ASSERT((mode & 1) == 0); // Warn if trying to pass a boolean as a mode.

    if (mode & Selection) {
        qCDebug(KLIPPER_LOG) << "Setting selection to <" << item.text() << ">";
        QMimeData *mimeData = item.mimeData();
        if (updateReason == ClipboardUpdateReason::PreventEmptyClipboard) {
            mimeData->setData(QStringLiteral("application/x-kde-onlyReplaceEmpty"), "1");
        }
        m_clip->setMimeData(mimeData, QClipboard::Selection);
    }
    if (mode & Clipboard) {
        qCDebug(KLIPPER_LOG) << "Setting clipboard to <" << item.text() << ">";
        QMimeData *mimeData = item.mimeData();
        if (updateReason == ClipboardUpdateReason::PreventEmptyClipboard) {
            mimeData->setData(QStringLiteral("application/x-kde-onlyReplaceEmpty"), "1");
        }
        m_clip->setMimeData(mimeData, QClipboard::Clipboard);
    }
}

void Klipper::slotClearOverflow()
{
    m_overflowClearTimer.stop();

    if (m_overflowCounter > MAX_CLIPBOARD_CHANGES) {
        qCDebug(KLIPPER_LOG) << "App owning the clipboard/selection is lame";
        // update to the latest data - this unfortunately may trigger the problem again
        newClipData(QClipboard::Selection); // Always the selection.
    }
    m_overflowCounter = 0;
}

QStringList Klipper::getClipboardHistoryMenu()
{
    QStringList menu;
    auto item = history()->first();
    if (item) {
        do {
            menu << item->text();
            item = history()->find(item->next_uuid());
        } while (item != history()->first());
    }

    return menu;
}

QString Klipper::getClipboardHistoryItem(int i)
{
    auto item = history()->first();
    if (item) {
        do {
            if (i-- == 0) {
                return item->text();
            }
            item = history()->find(item->next_uuid());
        } while (item != history()->first());
    }
    return QString();
}

//
// changing a spinbox in klipper's config-dialog causes the lineedit-contents
// of the spinbox to be selected and hence the clipboard changes. But we don't
// want all those items in klipper's history. See #41917
//
bool Klipper::ignoreClipboardChanges() const
{
    QWidget *focusWidget = qApp->focusWidget();
    if (focusWidget) {
        if (focusWidget->inherits("QSpinBox")
            || (focusWidget->parentWidget() && focusWidget->inherits("QLineEdit") && focusWidget->parentWidget()->inherits("QSpinWidget"))) {
            return true;
        }
    }

    return false;
}

void Klipper::updateTimestamp()
{
#if HAVE_X11
    if (KWindowSystem::isPlatformX11()) {
        QX11Info::setAppTime(QX11Info::getTimestamp());
    }
#endif
}

void Klipper::editData(const QSharedPointer<const HistoryItem> &item)
{
    QPointer<QDialog> dlg(new QDialog());
    dlg->setWindowTitle(i18n("Edit Contents"));
    QDialogButtonBox *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, dlg);
    connect(buttons, &QDialogButtonBox::accepted, dlg.data(), &QDialog::accept);
    connect(buttons, &QDialogButtonBox::rejected, dlg.data(), &QDialog::reject);
    connect(dlg.data(), &QDialog::finished, dlg.data(), [this, dlg, item](int result) {
        Q_EMIT editFinished(item, result);
        dlg->deleteLater();
    });

    ClipboardContentTextEdit *edit = new ClipboardContentTextEdit(dlg);
    edit->setAcceptRichText(false);
    if (item) {
        edit->setPlainText(item->text());
    }
    edit->setFocus();
    edit->setMinimumSize(300, 40);
    QVBoxLayout *layout = new QVBoxLayout(dlg);
    layout->addWidget(edit);
    layout->addWidget(buttons);
    dlg->adjustSize();

    connect(edit, &ClipboardContentTextEdit::done, dlg.data(), &QDialog::accept);
    connect(dlg.data(), &QDialog::accepted, this, [this, edit, item]() {
        QString text = edit->toPlainText();
        if (item) {
            m_history->remove(item);
        }
        m_history->insert(HistoryItemPtr(new HistoryStringItem(text)));
        if (m_myURLGrabber) {
            m_myURLGrabber->checkNewData(HistoryItemConstPtr(m_history->first()));
        }
    });

    if (m_mode == KlipperMode::Standalone) {
        dlg->setModal(true);
        dlg->exec();
    } else if (m_mode == KlipperMode::DataEngine) {
        dlg->open();
    }
}

class BarcodeLabel : public QLabel
{
public:
    BarcodeLabel(Prison::AbstractBarcode *barcode, QWidget *parent = nullptr)
        : QLabel(parent)
        , m_barcode(barcode)
    {
        setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding);
        setPixmap(QPixmap::fromImage(m_barcode->toImage(size())));
    }

protected:
    void resizeEvent(QResizeEvent *event) override
    {
        QLabel::resizeEvent(event);
        setPixmap(QPixmap::fromImage(m_barcode->toImage(event->size())));
    }

private:
    std::unique_ptr<Prison::AbstractBarcode> m_barcode;
};

void Klipper::showBarcode(const QSharedPointer<const HistoryItem> &item)
{
    using namespace Prison;
    QPointer<QDialog> dlg(new QDialog());
    dlg->setWindowTitle(i18n("Mobile Barcode"));
    QDialogButtonBox *buttons = new QDialogButtonBox(QDialogButtonBox::Ok, dlg);
    buttons->button(QDialogButtonBox::Ok)->setShortcut(Qt::CTRL | Qt::Key_Return);
    connect(buttons, &QDialogButtonBox::accepted, dlg.data(), &QDialog::accept);
    connect(dlg.data(), &QDialog::finished, dlg.data(), &QDialog::deleteLater);

    QWidget *mw = new QWidget(dlg);
    QHBoxLayout *layout = new QHBoxLayout(mw);

    {
        AbstractBarcode *qrCode = createBarcode(QRCode);
        if (qrCode) {
            if (item) {
                qrCode->setData(item->text());
            }
            BarcodeLabel *qrCodeLabel = new BarcodeLabel(qrCode, mw);
            layout->addWidget(qrCodeLabel);
        }
    }
    {
        AbstractBarcode *dataMatrix = createBarcode(DataMatrix);
        if (dataMatrix) {
            if (item) {
                dataMatrix->setData(item->text());
            }
            BarcodeLabel *dataMatrixLabel = new BarcodeLabel(dataMatrix, mw);
            layout->addWidget(dataMatrixLabel);
        }
    }

    mw->setFocus();
    QVBoxLayout *vBox = new QVBoxLayout(dlg);
    vBox->addWidget(mw);
    vBox->addWidget(buttons);
    dlg->adjustSize();

    if (m_mode == KlipperMode::Standalone) {
        dlg->setModal(true);
        dlg->exec();
    } else if (m_mode == KlipperMode::DataEngine) {
        dlg->open();
    }
}

void Klipper::slotAskClearHistory()
{
    int clearHist = KMessageBox::warningContinueCancel(nullptr,
                                                       i18n("Do you really want to clear and delete the entire clipboard history?"),
                                                       i18n("Clear Clipboard History"),
                                                       KStandardGuiItem::del(),
                                                       KStandardGuiItem::cancel(),
                                                       QStringLiteral("klipperClearHistoryAskAgain"),
                                                       KMessageBox::Dangerous);
    if (clearHist == KMessageBox::Continue) {
        history()->slotClear();
        saveHistory();
    }
}

void Klipper::slotCycleNext()
{
    // do cycle and show popup only if we have something in clipboard
    if (m_history->first()) {
        m_history->cycleNext();
        Q_EMIT passivePopup(i18n("Clipboard history"), cycleText());
    }
}

void Klipper::slotCyclePrev()
{
    // do cycle and show popup only if we have something in clipboard
    if (m_history->first()) {
        m_history->cyclePrev();
        Q_EMIT passivePopup(i18n("Clipboard history"), cycleText());
    }
}

QString Klipper::cycleText() const
{
    const int WIDTH_IN_PIXEL = 400;

    auto itemprev = m_history->prevInCycle();
    auto item = m_history->first();
    auto itemnext = m_history->nextInCycle();

    QFontMetrics font_metrics(m_popup->fontMetrics());
    QString result(QStringLiteral("<table>"));

    if (itemprev) {
        result += QLatin1String("<tr><td>");
        result += i18n("up");
        result += QLatin1String("</td><td>");
        result += font_metrics.elidedText(itemprev->text().simplified().toHtmlEscaped(), Qt::ElideMiddle, WIDTH_IN_PIXEL);
        result += QLatin1String("</td></tr>");
    }

    result += QLatin1String("<tr><td>");
    result += i18n("current");
    result += QLatin1String("</td><td><b>");
    result += font_metrics.elidedText(item->text().simplified().toHtmlEscaped(), Qt::ElideMiddle, WIDTH_IN_PIXEL);
    result += QLatin1String("</b></td></tr>");

    if (itemnext) {
        result += QLatin1String("<tr><td>");
        result += i18n("down");
        result += QLatin1String("</td><td>");
        result += font_metrics.elidedText(itemnext->text().simplified().toHtmlEscaped(), Qt::ElideMiddle, WIDTH_IN_PIXEL);
        result += QLatin1String("</td></tr>");
    }

    result += QLatin1String("</table>");
    return result;
}
