/*
  This file is part of libkdepim.

  SPDX-FileCopyrightText: 2002 Helge Deller <deller@gmx.de>
  SPDX-FileCopyrightText: 2002 Lubos Lunak <llunak@suse.cz>
  SPDX-FileCopyrightText: 2001, 2003 Carsten Pfeiffer <pfeiffer@kde.org>
  SPDX-FileCopyrightText: 2001 Waldo Bastian <bastian@kde.org>
  SPDX-FileCopyrightText: 2004 Daniel Molkentin <danimo@klaralvdalens-datakonsult.se>
  SPDX-FileCopyrightText: 2004 Karl-Heinz Zimmer <khz@klaralvdalens-datakonsult.se>
  SPDX-FileCopyrightText: 2017-2024 Laurent Montel <montel@kde.org>

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

#include "addresseelineedit.h"
#include "addresseelineeditmanager.h"
#include "kmailcompletion.h"
#include <KCodecs>
#include <KCompletionBox>
#include <KContacts/Addressee>
#include <KContacts/ContactGroup>
#include <KContacts/VCardConverter>
#include <KContacts/VCardDrag>
#include <KEmailAddress>
#include <KLocalizedString>
#include <KMessageBox>
#include <KStandardShortcut>
#include <Libkleo/KeyCache>
#include <QApplication>
#include <QClipboard>
#include <QDropEvent>
#include <QMenu>
#include <QMimeData>
#include <QRegularExpression>
#include <QTimer>
#include <gpgme++/key.h>

using namespace std::chrono_literals;
using namespace Qt::Literals::StringLiterals;

static constexpr QLatin1StringView s_completionItemIndentString("     ");

inline bool itemIsHeader(const QListWidgetItem *item)
{
    return item && !item->text().startsWith(s_completionItemIndentString);
}

QString adaptPasteMails(const QString &str)
{
    QString newText = str;
    // remove newlines in the to-be-pasted string
    static QRegularExpression reg2(QStringLiteral("\r?\n"));
    QStringList lines = newText.split(reg2, Qt::SkipEmptyParts);
    QStringList::iterator end(lines.end());
    for (QStringList::iterator it = lines.begin(); it != end; ++it) {
        // remove trailing commas and whitespace
        static QRegularExpression reg1(QRegularExpression(QStringLiteral(",?\\s*$")));
        it->remove(reg1);
    }
    newText = lines.join(QLatin1StringView(", "));

    if (newText.startsWith(QLatin1StringView("mailto:"))) {
        const QUrl url(newText);
        newText = url.path();
    } else if (newText.contains(QLatin1StringView(" at "))) {
        // Anti-spam stuff
        newText.replace(QStringLiteral(" at "), QStringLiteral("@"));
        newText.replace(QStringLiteral(" dot "), QStringLiteral("."));
    } else if (newText.contains(QLatin1StringView("(at)"))) {
        static QRegularExpression reg((QStringLiteral("\\s*\\(at\\)\\s*")));
        newText.replace(reg, QStringLiteral("@"));
    }
    return newText;
}

class SourceWithWeight
{
public:
    int weight; // the weight of the source
    int index; // index into AddresseeLineEditStatic::self()->completionSources
    QString sourceName; // the name of the source, e.g. "LDAP Server"

    bool operator<(const SourceWithWeight &other) const
    {
        if (weight > other.weight) {
            return true;
        }

        if (weight < other.weight) {
            return false;
        }

        return sourceName < other.sourceName;
    }
};

class AddresseeLineEditPrivate : public QObject
{
    Q_OBJECT

public:
    AddresseeLineEditPrivate(AddresseeLineEdit *qq);

    void setCompletedItems(const QStringList &items, bool autoSuggest);
    void addCompletionItem(const QString &string, int weight, int completionItemSource, const QStringList *keyWords = nullptr);
    bool smartPaste() const;
    void setSmartPaste(bool smartPaste);
    bool completionInitialized() const;
    bool useCompletion() const;
    void setUseCompletion(bool useCompletion);

    const QStringList adjustedCompletionItems(bool fullSearch);
    void updateSearchString();
    void startSearches();
    void slotCompletion();
    void doCompletion(bool ctrlT);

    void searchInGnupg();

    void slotTriggerDelayedQueries();

    [[nodiscard]] QString searchString() const;
    void setSearchString(const QString &searchString);

    [[nodiscard]] bool searchExtended() const;
    void setSearchExtended(bool searchExtended);

    [[nodiscard]] bool useSemicolonAsSeparator() const;
    void setUseSemicolonAsSeparator(bool useSemicolonAsSeparator);

    void slotReturnPressed(const QString &);
    void slotPopupCompletion(const QString &completion);
    void slotUserCancelled(const QString &cancelText);

private:
    AddresseeLineEdit *const q;
    QString mSearchString;
    QString mPreviousAddresses;
    bool mSmartPaste;
    bool mUseCompletion = true;
    bool mCompletionInitialized = false;
    bool mLastSearchMode;
    bool mSearchExtended = false;
    bool mUseSemicolonAsSeparator = false;

    QTimer *mDelayedQueryTimer;
};

AddresseeLineEditPrivate::AddresseeLineEditPrivate(AddresseeLineEdit *qq)
    : QObject(qq)
    , q(qq)
    , mDelayedQueryTimer(new QTimer(this))
{
    mDelayedQueryTimer->setSingleShot(true);
    connect(mDelayedQueryTimer, &QTimer::timeout, this, &AddresseeLineEditPrivate::slotTriggerDelayedQueries);

    if (!mCompletionInitialized) {
        q->setCompletionObject(AddresseeLineEditManager::self()->completion(), false);
        connect(q, &KLineEdit::completion, this, &AddresseeLineEditPrivate::slotCompletion);
        connect(q, &AddresseeLineEdit::returnKeyPressed, this, &AddresseeLineEditPrivate::slotReturnPressed);

        KCompletionBox *box = q->completionBox();
        connect(box, &KCompletionBox::textActivated, this, &AddresseeLineEditPrivate::slotPopupCompletion);
        connect(box, &KCompletionBox::userCancelled, this, &AddresseeLineEditPrivate::slotUserCancelled);

        mCompletionInitialized = true;
    }

    connect(q, &AddresseeLineEdit::textCompleted, q, &AddresseeLineEdit::slotEditingFinished);
    connect(q, &AddresseeLineEdit::editingFinished, q, &AddresseeLineEdit::slotEditingFinished);

    AddresseeLineEditManager::self()->addCompletionSource(i18n("GPG keychain"), 100);
}

void AddresseeLineEditPrivate::slotUserCancelled(const QString &cancelText)
{
    q->callUserCancelled(mPreviousAddresses + cancelText); // in KLineEdit
}

void AddresseeLineEditPrivate::slotPopupCompletion(const QString &completion)
{
    QString c = completion.trimmed();
    if (c.endsWith(QLatin1Char(')'))) {
        c = completion.mid(0, completion.lastIndexOf(QLatin1StringView(" ("))).trimmed();
    }
    q->setText(mPreviousAddresses + c);
    q->cursorAtEnd();
    updateSearchString();
    q->emitTextCompleted();
}

void AddresseeLineEditPrivate::slotReturnPressed(const QString &)
{
    if (!q->completionBox()->selectedItems().isEmpty()) {
        slotPopupCompletion(q->completionBox()->selectedItems().constFirst()->text());
    }
}

bool AddresseeLineEditPrivate::useSemicolonAsSeparator() const
{
    return mUseSemicolonAsSeparator;
}

void AddresseeLineEditPrivate::setUseSemicolonAsSeparator(bool useSemicolonAsSeparator)
{
    mUseSemicolonAsSeparator = useSemicolonAsSeparator;
}

const QStringList AddresseeLineEditPrivate::adjustedCompletionItems(bool fullSearch)
{
    QStringList items = fullSearch ? AddresseeLineEditManager::self()->completion()->allMatches(mSearchString)
                                   : AddresseeLineEditManager::self()->completion()->substringCompletion(mSearchString);

    // force items to be sorted by email
    items.sort();

    // For weighted mode, the algorithm is the following:
    // In the first loop, we add each item to its section (there is one section per completion source)
    // We also add spaces in front of the items.
    // The sections are appended to the items list.
    // In the second loop, we then walk through the sections and add all the items in there to the
    // sorted item list, which is the final result.
    //
    // The algo for non-weighted mode is different.

    int lastSourceIndex = -1;

    // Maps indices of the items list, which are section headers/source items,
    // to a QStringList which are the items of that section/source.
    QMap<int, QStringList> sections;
    QStringList sortedItems;
    for (QStringList::Iterator it = items.begin(); it != items.end(); ++it) {
        auto cit = AddresseeLineEditManager::self()->completionItemsMap.constFind(*it);
        if (cit == AddresseeLineEditManager::self()->completionItemsMap.constEnd()) {
            continue;
        }

        const int index = (*cit).second;

        if (AddresseeLineEditManager::self()->completion()->order() == KCompletion::Weighted) {
            if (lastSourceIndex == -1 || lastSourceIndex != index) {
                const QString sourceLabel(AddresseeLineEditManager::self()->completionSources.at(index));
                if (sections.find(index) == sections.end()) {
                    it = items.insert(it, sourceLabel);
                    ++it; // skip new item
                }
                lastSourceIndex = index;
            }

            it->prepend(s_completionItemIndentString);
            // remove preferred email sort <blank> added in  addContact()
            it->replace(QLatin1StringView("  <"), QStringLiteral(" <"));
        }
        sections[index].append(*it);

        if (AddresseeLineEditManager::self()->completion()->order() == KCompletion::Sorted) {
            sortedItems.append(*it);
        }
    }

    if (AddresseeLineEditManager::self()->completion()->order() == KCompletion::Weighted) {
        // Sort the sections
        QList<SourceWithWeight> sourcesAndWeights;
        const int numberOfCompletionSources(AddresseeLineEditManager::self()->completionSources.count());
        sourcesAndWeights.reserve(numberOfCompletionSources);
        for (int i = 0; i < numberOfCompletionSources; ++i) {
            SourceWithWeight sww;
            sww.sourceName = AddresseeLineEditManager::self()->completionSources.at(i);
            sww.weight = AddresseeLineEditManager::self()->completionSourceWeights[sww.sourceName];
            sww.index = i;
            sourcesAndWeights.append(sww);
        }

        std::sort(sourcesAndWeights.begin(), sourcesAndWeights.end());
        // Add the sections and their items to the final sortedItems result list
        const int numberOfSources(sourcesAndWeights.size());
        for (int i = 0; i < numberOfSources; ++i) {
            const SourceWithWeight source = sourcesAndWeights.at(i);
            const QStringList sectionItems = sections[source.index];
            if (!sectionItems.isEmpty()) {
                sortedItems.append(source.sourceName);
                for (const QString &itemInSection : sectionItems) {
                    sortedItems.append(itemInSection);
                }
            }
        }
    } else {
        sortedItems.sort();
    }

    return sortedItems;
}

void AddresseeLineEditPrivate::doCompletion(bool ctrlT)
{
    mLastSearchMode = ctrlT;

    const KCompletion::CompletionMode mode = q->completionMode();

    if (mode == KCompletion::CompletionNone) {
        return;
    }

    AddresseeLineEditManager::self()->completion()->setOrder(KCompletion::Weighted);

    // cursor at end of string - or Ctrl+T pressed for substring completion?
    if (ctrlT) {
        const QStringList completions = adjustedCompletionItems(false);

        if (completions.count() == 1) {
            q->setText(mPreviousAddresses + completions.first().trimmed());
        }

        // Make sure the completion popup is closed if no matching items were found
        setCompletedItems(completions, true);

        q->cursorAtEnd();
        q->setCompletionMode(mode); // set back to previous mode
        return;
    }

    switch (mode) {
    case KCompletion::CompletionPopupAuto:
        if (mSearchString.isEmpty()) {
            break;
        }
        // else: fall-through to the CompletionPopup case
        [[fallthrough]];

    case KCompletion::CompletionPopup: {
        const QStringList items = adjustedCompletionItems(false);
        setCompletedItems(items, false);
        break;
    }

    case KCompletion::CompletionShell: {
        const QString match = AddresseeLineEditManager::self()->completion()->makeCompletion(mSearchString);
        if (!match.isNull() && match != mSearchString) {
            q->setText(mPreviousAddresses + match);
            q->setModified(true);
            q->cursorAtEnd();
        }
        break;
    }

    case KCompletion::CompletionMan: // Short-Auto in fact
    case KCompletion::CompletionAuto:
        // force autoSuggest in KLineEdit::keyPressed or setCompletedText will have no effect
        q->setCompletionMode(q->completionMode());

        if (!mSearchString.isEmpty()) {
            // if only our \" is left, remove it since user has not typed it either
            if (mSearchExtended && mSearchString == QLatin1StringView("\"")) {
                mSearchExtended = false;
                mSearchString.clear();
                q->setText(mPreviousAddresses);
                break;
            }

            QString match = AddresseeLineEditManager::self()->completion()->makeCompletion(mSearchString);

            if (!match.isEmpty()) {
                if (match != mSearchString) {
                    const QString adds = mPreviousAddresses + match;
                    q->callSetCompletedText(adds);
                }
            } else {
                if (!mSearchString.startsWith(QLatin1Char('\"'))) {
                    // try with quoted text, if user has not type one already
                    match = AddresseeLineEditManager::self()->completion()->makeCompletion(QLatin1StringView("\"") + mSearchString);
                    if (!match.isEmpty() && match != mSearchString) {
                        mSearchString = QLatin1StringView("\"") + mSearchString;
                        mSearchExtended = true;
                        q->setText(mPreviousAddresses + mSearchString);
                        q->callSetCompletedText(mPreviousAddresses + match);
                    }
                } else if (mSearchExtended) {
                    // our added \" does not work anymore, remove it
                    mSearchString.remove(0, 1);
                    mSearchExtended = false;
                    q->setText(mPreviousAddresses + mSearchString);
                    // now try again
                    match = AddresseeLineEditManager::self()->completion()->makeCompletion(mSearchString);
                    if (!match.isEmpty() && match != mSearchString) {
                        const QString adds = mPreviousAddresses + match;
                        q->setCompletedText(adds);
                    }
                }
            }
        }
        break;

    case KCompletion::CompletionNone:
        break;
    }
}

void AddresseeLineEditPrivate::setCompletedItems(const QStringList &items, bool autoSuggest)
{
    KCompletionBox *completionBox = q->completionBox();

    if (!items.isEmpty() && !(items.count() == 1 && mSearchString == items.first())) {
        completionBox->clear();
        const int numberOfItems(items.count());
        for (int i = 0; i < numberOfItems; ++i) {
            auto item = new QListWidgetItem(items.at(i), completionBox);
            if (!items.at(i).startsWith(s_completionItemIndentString)) {
                item->setFlags(item->flags() & ~Qt::ItemIsSelectable);
                item->setBackground(AddresseeLineEditManager::self()->alternateColor());
            }
            completionBox->addItem(item);
        }
        if (!completionBox->isVisible()) {
            if (!mSearchString.isEmpty()) {
                completionBox->setCancelledText(mSearchString);
            }
            completionBox->popup();
            // we have to install the event filter after popup(), since that
            // calls show(), and that's where KCompletionBox installs its filter.
            // We want to be first, though, so do it now.
            if (AddresseeLineEditManager::self()->completion()->order() == KCompletion::Weighted) {
                qApp->installEventFilter(q);
            }
        }

        QListWidgetItem *item = completionBox->item(1);
        if (item) {
            completionBox->blockSignals(true);
            completionBox->setCurrentItem(item);
            item->setSelected(true);
            completionBox->blockSignals(false);
        }

        if (autoSuggest) {
            const int index = items.first().indexOf(mSearchString);
            const QString newText = items.first().mid(index);
            q->callSetUserSelection(false);
            q->callSetCompletedText(newText, true);
        }
    } else {
        if (completionBox && completionBox->isVisible()) {
            completionBox->hide();
            completionBox->setItems(QStringList());
        }
    }
}

void AddresseeLineEditPrivate::addCompletionItem(const QString &string, int weight, int completionItemSource, const QStringList *keyWords)
{
    // Check if there is an exact match for item already, and use the
    // maximum weight if so. Since there's no way to get the information
    // from KCompletion, we have to keep our own QMap.
    // We also update the source since the item should always be shown from the source with the highest weight

    auto manager = AddresseeLineEditManager::self();
    auto it = manager->completionItemsMap.find(string);
    if (it != manager->completionItemsMap.end()) {
        weight = qMax((*it).first, weight);
        (*it).first = weight;
        (*it).second = completionItemSource;
    } else {
        manager->completionItemsMap.insert(string, qMakePair(weight, completionItemSource));
    }

    manager->completion()->addItem(string, weight);
    if (keyWords && !keyWords->isEmpty()) {
        manager->completion()->addItemWithKeys(string, weight, keyWords); // see kmailcompletion.cpp
    }
}

bool AddresseeLineEditPrivate::smartPaste() const
{
    return mSmartPaste;
}

void AddresseeLineEditPrivate::setSmartPaste(bool smartPaste)
{
    mSmartPaste = smartPaste;
}

bool AddresseeLineEditPrivate::completionInitialized() const
{
    return mCompletionInitialized;
}

bool AddresseeLineEditPrivate::useCompletion() const
{
    return mUseCompletion;
}

void AddresseeLineEditPrivate::setUseCompletion(bool useCompletion)
{
    mUseCompletion = useCompletion;
}

AddresseeLineEdit::AddresseeLineEdit(QWidget *parent)
    : KLineEdit(parent)
    , d(std::make_unique<AddresseeLineEditPrivate>(this))
{
}

AddresseeLineEdit::~AddresseeLineEdit() = default;

void AddresseeLineEdit::addContactGroup(const KContacts::ContactGroup &group, int weight, int source)
{
    d->addCompletionItem(group.name(), weight, source);
}

void AddresseeLineEdit::addContact(const QStringList &emails, const KContacts::Addressee &addr, int weight, int source, QString append)
{
    int isPrefEmail = 1; // first in list is preferredEmail
    for (const QString &email : emails) {
        // TODO: highlight preferredEmail
        const QString givenName = addr.givenName();
        const QString familyName = addr.familyName();
        const QString nickName = addr.nickName();
        const QString fullEmail = addr.fullEmail(email);

        QString appendix;

        if (!append.isEmpty()) {
            appendix = QStringLiteral(" (%1)");
            append.replace(QLatin1Char('('), QStringLiteral("["));
            append.replace(QLatin1Char(')'), QStringLiteral("]"));
            appendix = appendix.arg(append);
        }

        // Prepare "givenName" + ' ' + "familyName"
        QString fullName = givenName;
        if (!familyName.isEmpty()) {
            if (!fullName.isEmpty()) {
                fullName += QLatin1Char(' ');
            }
            fullName += familyName;
        }

        // Finally, we can add the completion items
        if (!fullName.isEmpty()) {
            const QString address = KEmailAddress::normalizedAddress(fullName, email, QString());
            if (fullEmail != address) {
                // This happens when fullEmail contains a middle name, while our own fullName+email only has "first last".
                // Let's offer both, the fullEmail with 3 parts, looks a tad formal.
                d->addCompletionItem(address + appendix, weight + isPrefEmail, source);
            }
        }

        QStringList keyWords;
        if (!nickName.isEmpty()) {
            keyWords.append(nickName);
        }

        d->addCompletionItem(fullEmail + appendix, weight + isPrefEmail, source, &keyWords);

        isPrefEmail = 0;
    }
}

void AddresseeLineEdit::addContact(const KContacts::Addressee &addr, int weight, int source, const QString &append)
{
    const QStringList emails = addr.emails();
    if (emails.isEmpty()) {
        return;
    }
    addContact(emails, addr, weight, source, append);
}

void AddresseeLineEdit::dropEvent(QDropEvent *event)
{
    const QMimeData *md = event->mimeData();
    // Case one: The user dropped a text/directory (i.e. vcard), so decode its
    //           contents
    if (KContacts::VCardDrag::canDecode(md)) {
        KContacts::Addressee::List list;
        KContacts::VCardDrag::fromMimeData(md, list);

        for (const KContacts::Addressee &addr : std::as_const(list)) {
            insertEmails(addr.emails());
        }
    }
    // Case two: The user dropped a list or Urls.
    // Iterate over that list. For mailto: Urls, just add the addressee to the list,
    // and for other Urls, download the Url and assume it points to a vCard
    else if (md->hasUrls()) {
        const QList<QUrl> urls = md->urls();
        KContacts::Addressee::List list;

        for (const QUrl &url : urls) {
            // First, let's deal with mailto Urls. The path() part contains the
            // email-address.
            if (url.scheme() == QLatin1StringView("mailto")) {
                KContacts::Addressee addressee;
                KContacts::Email email(KEmailAddress::decodeMailtoUrl(url));
                email.setPreferred(true);
                addressee.addEmail(email);
                list += addressee;
            }
        }

        // Now, let the user choose which addressee to add.
        for (const KContacts::Addressee &addressee : std::as_const(list)) {
            insertEmails(addressee.emails());
        }
    }
    // Case three: Let AddresseeLineEdit deal with the rest
    else {
        if (!isReadOnly()) {
            const QList<QUrl> uriList = event->mimeData()->urls();
            if (!uriList.isEmpty()) {
                QString contents = text();
                // remove trailing white space and comma
                int eot = contents.length();
                while ((eot > 0) && contents.at(eot - 1).isSpace()) {
                    --eot;
                }
                if (eot == 0) {
                    contents.clear();
                } else if (contents.at(eot - 1) == QLatin1Char(',')) {
                    --eot;
                    contents.truncate(eot);
                }
                bool mailtoURL = false;
                // append the mailto URLs
                for (const QUrl &url : uriList) {
                    if (url.scheme() == QLatin1StringView("mailto")) {
                        mailtoURL = true;
                        QString address;
                        address = QUrl::fromPercentEncoding(url.path().toLatin1());
                        address = KCodecs::decodeRFC2047String(address);
                        if (!contents.isEmpty()) {
                            contents.append(QLatin1StringView(", "));
                        }
                        contents.append(address);
                    }
                }
                if (mailtoURL) {
                    setText(contents);
                    setModified(true);
                    return;
                }
            } else {
                // Let's see if this drop contains a comma separated list of emails
                if (md->hasText()) {
                    const QString dropData = md->text();
                    const QStringList addrs = KEmailAddress::splitAddressList(dropData);
                    if (!addrs.isEmpty()) {
                        if (addrs.count() == 1) {
                            QUrl url(dropData);
                            if (url.scheme() == QLatin1StringView("mailto")) {
                                KContacts::Addressee addressee;
                                KContacts::Email email(KEmailAddress::decodeMailtoUrl(url));
                                email.setPreferred(true);
                                addressee.addEmail(email);
                                insertEmails(addressee.emails());
                            } else {
                                setText(KEmailAddress::normalizeAddressesAndDecodeIdn(dropData));
                            }
                        } else {
                            setText(KEmailAddress::normalizeAddressesAndDecodeIdn(dropData));
                        }
                        setModified(true);
                        return;
                    }
                }
            }
        }

        if (d->useCompletion()) {
            d->setSmartPaste(true);
        }

        QLineEdit::dropEvent(event);
        d->setSmartPaste(false);
    }
}

void AddresseeLineEdit::insertEmails(const QStringList &emails)
{
    if (emails.empty()) {
        return;
    }

    QString contents = text();
    if (!contents.isEmpty()) {
        contents += QLatin1Char(',');
    }
    // only one address, don't need kpopup to choose
    if (emails.size() == 1) {
        setText(contents + emails.front());
        return;
    }
    // multiple emails, let the user choose one
    QMenu menu(this);
    menu.setTitle(i18n("Select email from contact"));
    menu.setObjectName(QLatin1StringView("Addresschooser"));
    for (const QString &email : emails) {
        menu.addAction(email);
    }
    const QAction *result = menu.exec(QCursor::pos());
    if (!result) {
        return;
    }
    setText(contents + KLocalizedString::removeAcceleratorMarker(result->text()));
}

void AddresseeLineEdit::cursorAtEnd()
{
    setCursorPosition(text().length());
}

void AddresseeLineEdit::enableCompletion(bool enable)
{
    d->setUseCompletion(enable);
}

bool AddresseeLineEdit::isCompletionEnabled() const
{
    return d->useCompletion();
}

bool AddresseeLineEdit::eventFilter(QObject *object, QEvent *event)
{
    if (d->completionInitialized() && (object == completionBox() || completionBox()->findChild<QWidget *>(object->objectName()) == object)) {
        if (event->type() == QEvent::MouseButtonPress || event->type() == QEvent::MouseMove || event->type() == QEvent::MouseButtonRelease
            || event->type() == QEvent::MouseButtonDblClick) {
            const QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);
            // find list box item at the event position
            QListWidgetItem *item = completionBox()->itemAt(mouseEvent->pos());
            if (!item) {
                // In the case of a mouse move outside of the box we don't want
                // the parent to fuzzy select a header by mistake.
                const bool eat = event->type() == QEvent::MouseMove;
                return eat;
            }
            // avoid selection of headers on button press, or move or release while
            // a button is pressed
            const Qt::MouseButtons buttons = mouseEvent->buttons();
            if (event->type() == QEvent::MouseButtonPress || event->type() == QEvent::MouseButtonDblClick || buttons & Qt::LeftButton
                || buttons & Qt::MiddleButton || buttons & Qt::RightButton) {
                if (itemIsHeader(item)) {
                    return true; // eat the event, we don't want anything to happen
                } else {
                    // if we are not on one of the group heading, make sure the item
                    // below or above is selected, not the heading, inadvertedly, due
                    // to fuzzy auto-selection from QListBox
                    completionBox()->setCurrentItem(item);
                    item->setSelected(true);
                    if (event->type() == QEvent::MouseMove) {
                        return true; // avoid fuzzy selection behavior
                    }
                }
            }
        }
    }

    if ((object == this) && (event->type() == QEvent::ShortcutOverride)) {
        auto keyEvent = static_cast<QKeyEvent *>(event);
        if (keyEvent->key() == Qt::Key_Up || keyEvent->key() == Qt::Key_Down || keyEvent->key() == Qt::Key_Tab) {
            keyEvent->accept();
            return true;
        }
    }

    if ((object == this) && (event->type() == QEvent::KeyPress || event->type() == QEvent::KeyRelease) && completionBox()->isVisible()) {
        const QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
        int currentIndex = completionBox()->currentRow();
        if (currentIndex < 0) {
            return true;
        }
        if (keyEvent->key() == Qt::Key_Up) {
            // qCDebug(PIMCOMMONAKONADI_LOG) <<"EVENTFILTER: Qt::Key_Up currentIndex=" << currentIndex;
            // figure out if the item we would be moving to is one we want
            // to ignore. If so, go one further
            const QListWidgetItem *itemAbove = completionBox()->item(currentIndex);
            if (itemAbove && itemIsHeader(itemAbove)) {
                // there is a header above is, check if there is even further up
                // and if so go one up, so it'll be selected
                if (currentIndex > 0 && completionBox()->item(currentIndex - 1)) {
                    // qCDebug(PIMCOMMONAKONADI_LOG) <<"EVENTFILTER: Qt::Key_Up -> skipping" << currentIndex - 1;
                    completionBox()->setCurrentRow(currentIndex - 1);
                    completionBox()->item(currentIndex - 1)->setSelected(true);
                } else if (currentIndex == 0) {
                    // nothing to skip to, let's stay where we are, but make sure the
                    // first header becomes visible, if we are the first real entry
                    completionBox()->scrollToItem(completionBox()->item(0));
                    QListWidgetItem *item = completionBox()->item(currentIndex);
                    if (item) {
                        if (itemIsHeader(item)) {
                            currentIndex++;
                            item = completionBox()->item(currentIndex);
                        }
                        completionBox()->setCurrentItem(item);
                        item->setSelected(true);
                    }
                }

                return true;
            }
        } else if (keyEvent->key() == Qt::Key_Down) {
            // same strategy for downwards
            // qCDebug(PIMCOMMONAKONADI_LOG) <<"EVENTFILTER: Qt::Key_Down. currentIndex=" << currentIndex;
            const QListWidgetItem *itemBelow = completionBox()->item(currentIndex);
            if (itemBelow && itemIsHeader(itemBelow)) {
                if (completionBox()->item(currentIndex + 1)) {
                    // qCDebug(PIMCOMMONAKONADI_LOG) <<"EVENTFILTER: Qt::Key_Down -> skipping" << currentIndex+1;
                    completionBox()->setCurrentRow(currentIndex + 1);
                    completionBox()->item(currentIndex + 1)->setSelected(true);
                } else {
                    // nothing to skip to, let's stay where we are
                    QListWidgetItem *item = completionBox()->item(currentIndex);
                    if (item) {
                        completionBox()->setCurrentItem(item);
                        item->setSelected(true);
                    }
                }

                return true;
            }
            // special case of the initial selection, which is unfortunately a header.
            // Setting it to selected tricks KCompletionBox into not treating is special
            // and selecting making it current, instead of the one below.
            QListWidgetItem *item = completionBox()->item(currentIndex);
            if (item && itemIsHeader(item)) {
                completionBox()->setCurrentItem(item);
                item->setSelected(true);
            }
        } else if (event->type() == QEvent::KeyRelease && (keyEvent->key() == Qt::Key_Tab || keyEvent->key() == Qt::Key_Backtab)) {
            /// first, find the header of the current section
            QListWidgetItem *myHeader = nullptr;
            int myHeaderIndex = -1;
            const int iterationStep = keyEvent->key() == Qt::Key_Tab ? 1 : -1;
            int index = qMin(qMax(currentIndex - iterationStep, 0), completionBox()->count() - 1);
            while (index >= 0) {
                if (itemIsHeader(completionBox()->item(index))) {
                    myHeader = completionBox()->item(index);
                    myHeaderIndex = index;
                    break;
                }

                index--;
            }
            Q_ASSERT(myHeader); // we should always be able to find a header

            // find the next header (searching backwards, for Qt::Key_Backtab)
            QListWidgetItem *nextHeader = nullptr;

            // when iterating forward, start at the currentindex, when backwards,
            // one up from our header, or at the end
            int j;
            if (keyEvent->key() == Qt::Key_Tab) {
                j = currentIndex;
            } else {
                index = myHeaderIndex;
                if (index == 0) {
                    j = completionBox()->count() - 1;
                } else {
                    j = (index - 1) % completionBox()->count();
                }
            }
            while ((nextHeader = completionBox()->item(j)) && nextHeader != myHeader) {
                if (itemIsHeader(nextHeader)) {
                    break;
                }
                j = (j + iterationStep) % completionBox()->count();
            }

            if (nextHeader && nextHeader != myHeader) {
                QListWidgetItem *item = completionBox()->item(j + 1);
                if (item && !itemIsHeader(item)) {
                    completionBox()->setCurrentItem(item);
                    item->setSelected(true);
                }
            }

            return true;
        }
    }

    return KLineEdit::eventFilter(object, event);
}

void AddresseeLineEdit::setText(const QString &text)
{
    const int cursorPos = cursorPosition();
    KLineEdit::setText(text.trimmed());
    setCursorPosition(cursorPos);
}

void AddresseeLineEdit::insert(const QString &t)
{
    if (!d->smartPaste()) {
        KLineEdit::insert(t);
        return;
    }

    QString newText = t.trimmed();
    if (newText.isEmpty()) {
        return;
    }

    newText = adaptPasteMails(newText);

    QString contents = text();
    int pos = cursorPosition();

    if (hasSelectedText()) {
        // Cut away the selection.
        int start_sel = selectionStart();
        pos = start_sel;
        contents = contents.left(start_sel) + contents.mid(start_sel + selectedText().length());
    }

    int eot = contents.length();
    while ((eot > 0) && contents.at(eot - 1).isSpace()) {
        --eot;
    }
    if (eot == 0) {
        contents.clear();
    } else if (pos >= eot) {
        if (contents.at(eot - 1) == QLatin1Char(',')) {
            --eot;
        }
        contents.truncate(eot);
        contents += QStringLiteral(", ");
        pos = eot + 2;
    }

    contents = contents.left(pos) + newText + contents.mid(pos);
    setText(contents);
    setModified(true);
    setCursorPosition(pos + newText.length());
}

void AddresseeLineEdit::paste()
{
    if (d->useCompletion()) {
        d->setSmartPaste(true);
    }

    KLineEdit::paste();
    d->setSmartPaste(false);
}

void AddresseeLineEdit::keyPressEvent(QKeyEvent *event)
{
    bool accept = false;

    const int key = event->key() | event->modifiers();

    if (KStandardShortcut::shortcut(KStandardShortcut::SubstringCompletion).contains(key)) {
        // TODO: add LDAP substring lookup, when it becomes available in KPIM::LDAPSearch
        d->updateSearchString();
        d->startSearches();
        d->doCompletion(true);
        accept = true;
    } else if (KStandardShortcut::shortcut(KStandardShortcut::TextCompletion).contains(key)) {
        const int len = text().length();

        if (len == cursorPosition()) { // at End?
            d->updateSearchString();
            d->startSearches();
            d->doCompletion(true);
            accept = true;
        }
    }

    const QString oldContent = text();
    if (!accept) {
        KLineEdit::keyPressEvent(event);
    }

    // if the text didn't change (eg. because a cursor navigation key was pressed)
    // we don't need to trigger a new search
    if (oldContent == text()) {
        return;
    }

    if (event->isAccepted()) {
        d->updateSearchString();

        QString searchString(d->searchString());
        // LDAP does not know about our string manipulation, remove it
        if (d->searchExtended()) {
            searchString = d->searchString().mid(1);
        }
    }
}

QString AddresseeLineEditPrivate::searchString() const
{
    return mSearchString;
}

void AddresseeLineEditPrivate::setSearchString(const QString &searchString)
{
    mSearchString = searchString;
}

bool AddresseeLineEditPrivate::searchExtended() const
{
    return mSearchExtended;
}

void AddresseeLineEditPrivate::setSearchExtended(bool searchExtended)
{
    mSearchExtended = searchExtended;
}

void AddresseeLineEdit::callUserCancelled(const QString &str)
{
    userCancelled(str);
}

void AddresseeLineEdit::callSetCompletedText(const QString &text, bool marked)
{
    setCompletedText(text, marked);
}

void AddresseeLineEdit::callSetCompletedText(const QString &text)
{
    setCompletedText(text);
}

void AddresseeLineEdit::callSetUserSelection(bool b)
{
    setUserSelection(b);
}

void AddresseeLineEdit::mouseReleaseEvent(QMouseEvent *event)
{
    // reimplemented from QLineEdit::mouseReleaseEvent()
    if (d->useCompletion() && QApplication::clipboard()->supportsSelection() && !isReadOnly() && event->button() == Qt::MiddleButton) {
        d->setSmartPaste(true);
    }

    KLineEdit::mouseReleaseEvent(event);
    d->setSmartPaste(false);
}

void AddresseeLineEditPrivate::updateSearchString()
{
    mSearchString = q->text();

    int n = -1;
    bool inQuote = false;
    const int searchStringLength = mSearchString.length();
    for (int i = 0; i < searchStringLength; ++i) {
        const QChar searchChar = mSearchString.at(i);
        if (searchChar == QLatin1Char('"')) {
            inQuote = !inQuote;
        }

        if (searchChar == QLatin1Char('\\') && (i + 1) < searchStringLength && mSearchString.at(i + 1) == QLatin1Char('"')) {
            ++i;
        }

        if (inQuote) {
            continue;
        }

        if (i < searchStringLength && (searchChar == QLatin1Char(',') || (mUseSemicolonAsSeparator && searchChar == QLatin1Char(';')))) {
            n = i;
        }
    }

    if (n >= 0) {
        ++n; // Go past the ","

        const int len = mSearchString.length();

        // Increment past any whitespace...
        while (n < len && mSearchString.at(n).isSpace()) {
            ++n;
        }

        mPreviousAddresses = mSearchString.left(n);
        mSearchString = mSearchString.mid(n).trimmed();
    } else {
        mPreviousAddresses.clear();
    }
}

void AddresseeLineEditPrivate::startSearches()
{
    if (!mDelayedQueryTimer->isActive()) {
        mDelayedQueryTimer->start(50ms);
    }
}

void AddresseeLineEditPrivate::slotCompletion()
{
    // Called by KLineEdit's keyPressEvent for CompletionModes
    // Auto,Popup -> new text, update search string.
    // not called for CompletionShell, this is been taken care of
    // in AddresseeLineEdit::keyPressEvent

    updateSearchString();
    if (q->completionBox()) {
        q->completionBox()->setCancelledText(mSearchString);
    }

    startSearches();
    doCompletion(false);
}

void AddresseeLineEditPrivate::slotTriggerDelayedQueries()
{
    const QString strSearch = mSearchString.trimmed();
    if (strSearch.size() <= 2) {
        return;
    }

    searchInGnupg();
}

void AddresseeLineEditPrivate::searchInGnupg()
{
    const auto keys = Kleo::KeyCache::instance()->keys();
    const QString trimmedString = mSearchString.trimmed();

    for (const auto &key : keys) {
        for (int i = 0, count = key.numUserIDs(); i < count; i++) {
            auto email = QString::fromLatin1(key.userID(i).email());
            if (email.startsWith(u'<')) {
                email.remove(0, 1);
            }
            if (email.endsWith(u'>')) {
                email.chop(1);
            }

            const auto name = QLatin1StringView(key.userID(i).name());
            if (email.contains(trimmedString) || name.contains(trimmedString)) {
                if (name.trimmed().isEmpty()) {
                    addCompletionItem(email, 1, 0);
                } else {
                    addCompletionItem(name + u" <"_s + email + u'>', 1, 0);
                }
            }
        }
    }
}

void AddresseeLineEdit::slotEditingFinished()
{
    // const QList<KJob *> listJob = d->mightBeGroupJobs();
    // for (KJob *job : listJob) {
    //     disconnect(job);
    //     job->deleteLater();
    // }

    // d->mightBeGroupJobsClear();
    // d->groupsClear();

    // if (!text().trimmed().isEmpty() && enableAkonadiSearch()) {
    //     const QStringList addresses = KEmailAddress::splitAddressList(text());
    //     for (const QString &address : addresses) {
    //         auto job = new Akonadi::ContactGroupSearchJob();
    //         connect(job, &Akonadi::ContactGroupSearchJob::result, this, &AddresseeLineEdit::slotGroupSearchResult);
    //         d->mightBeGroupJobsAdd(job);
    //         job->setQuery(Akonadi::ContactGroupSearchJob::Name, address);
    //     }
    // }
}

void AddresseeLineEdit::emitTextCompleted()
{
    Q_EMIT textCompleted();
}

#include "addresseelineedit.moc"
