// SPDX-FileCopyrightText: 2023 g10 code Gmbh
// SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
// SPDX-License-Identifier: GPL-2.0-or-later

#include "composerwindow.h"

// Qt includes
#include <QApplication>
#include <QClipboard>
#include <QCloseEvent>
#include <QFileDialog>
#include <QFileInfo>
#include <QInputDialog>
#include <QLabel>
#include <QMenu>
#include <QMimeData>
#include <QPainter>
#include <QPrintDialog>
#include <QPrintPreviewDialog>
#include <QPrinter>
#include <QPushButton>
#include <QSplitter>
#include <QStatusBar>
#include <QTimer>
#include <QUrlQuery>
#include <QVBoxLayout>

// KDE includes
#include "identity/identity.h"
#include <KActionCollection>
#include <KCursorSaver>
#include <KEmailAddress>
#include <KIconUtils>
#include <KLineEdit>
#include <KLocalizedString>
#include <KMessageBox>
#include <KMime/Message>
#include <KPIMTextEdit/RichTextComposer>
#include <KPIMTextEdit/RichTextComposerActions>
#include <KPIMTextEdit/RichTextComposerControler>
#include <KPIMTextEdit/RichTextComposerImages>
#include <KPIMTextEdit/RichTextComposerWidget>
#include <KPIMTextEdit/RichTextExternalComposer>
#include <KToolBar>
#include <MimeTreeParserWidgets/MessageViewer>
#include <MimeTreeParserWidgets/MessageViewerDialog>
#include <Sonnet/DictionaryComboBox>
#include <TextCustomEditor/RichTextEditorWidget>

#include <Libkleo/ExpiryChecker>
#include <Libkleo/ExpiryCheckerSettings>
#include <Libkleo/KeyCache>
#include <Libkleo/KeyResolverCore>
#include <Libkleo/KeySelectionCombo>
#include <Libkleo/KeySelectionDialog>

// Gpgme includes
#include <QGpgME/Protocol>
#include <gpgme++/tofuinfo.h>

// App includes
#include "../identity/identitydialog.h"
#include "../identity/identitymanager.h"
#include "attachment/attachmentcontroller.h"
#include "attachment/attachmentmodel.h"
#include "attachment/attachmentview.h"
#include "composerviewbase.h"
#include "draft/draftmanager.h"
#include "ews/ewsmessagedispatcher.h"
#include "job/inserttextfilejob.h"
#include "job/saveasfilejob.h"
#include "kmcomposerglobalaction.h"
#include "mailtemplates.h"
#include "messagecomposersettings.h"
#include "nearexpirywarning.h"
#include "recipientseditor.h"
#include "richtextcomposerng.h"
#include "signaturecontroller.h"
#include "spellcheckerconfigdialog.h"
#include "websocketclient.h"

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

namespace
{

inline bool containsSMIME(unsigned int f)
{
    return f & (Kleo::SMIMEFormat | Kleo::SMIMEOpaqueFormat);
}

inline bool containsOpenPGP(unsigned int f)
{
    return f & (Kleo::OpenPGPMIMEFormat | Kleo::InlineOpenPGPFormat);
}

auto findSendersUid(const std::string &addrSpec, const std::vector<GpgME::UserID> &userIds)
{
    return std::find_if(userIds.cbegin(), userIds.cend(), [&addrSpec](const auto &uid) {
        return uid.addrSpec() == addrSpec || (uid.addrSpec().empty() && std::string(uid.email()) == addrSpec)
            || (uid.addrSpec().empty() && (!uid.email() || !*uid.email()) && uid.name() == addrSpec);
    });
}
}

ComposerWindow::ComposerWindow(const QString &from, const QString &name, const QByteArray &bearerToken, QWidget *parent)
    : KXmlGuiWindow(parent)
    , mFrom(from)
    , mMainWidget(new QWidget(this))
    , mComposerBase(new MessageComposer::ComposerViewBase(this))
    , mHeadersToEditorSplitter(new QSplitter(Qt::Vertical, mMainWidget))
    , mHeadersArea(new QWidget(mHeadersToEditorSplitter))
    , mGrid(new QGridLayout(mHeadersArea))
    , mLblFrom(new QLabel(i18nc("sender address field", "From:"), mHeadersArea))
    , mButtonFrom(new QPushButton(mHeadersArea))
    , mRecipientEditor(new RecipientsEditor(mHeadersArea))
    , mLblSubject(new QLabel(i18nc("@label:textbox Subject of email.", "Subject:"), mHeadersArea))
    , mEdtSubject(new QLineEdit(mHeadersArea))
    , mRichTextComposer(new MessageComposer::RichTextComposerNg(this))
    , mRichTextEditorWidget(new TextCustomEditor::RichTextEditorWidget(mRichTextComposer, mMainWidget))
    , mNearExpiryWarning(new NearExpiryWarning(this))
    , mGlobalAction(new KMComposerGlobalAction(this, this))
    , mMessageDispatcher(new EWSMessageDispatcher(this))
    , mKeyCache(Kleo::KeyCache::mutableInstance())
{
    connect(mKeyCache.get(), &Kleo::KeyCache::keyListingDone, this, [this](const GpgME::KeyListResult &result) {
        Q_UNUSED(result);
        mRunKeyResolverTimer->start();
    });

    bool isNew = false;
    mIdentity = IdentityManager::self().fromEmail(from, isNew);
    mEdtFrom = new QLabel(mHeadersArea);

    if (isNew) {
        // fill the idenity with default fields
        auto dlg = new KMail::IdentityDialog;
        mIdentity.setFullName(name);
        mIdentity.setWarnNotEncrypt(true);

        dlg->setIdentity(mIdentity);
        connect(dlg, &KMail::IdentityDialog::keyListingFinished, this, [this, dlg]() {
            dlg->updateIdentity(mIdentity);
            IdentityManager::self().updateIdentity(mIdentity);
        });
    }
    mMessageDispatcher->setBearerToken(bearerToken);
    mComposerBase->setMessageDispatcher(mMessageDispatcher);

    mMainWidget->resize(800, 600);
    setCentralWidget(mMainWidget);
    setWindowTitle(i18nc("@title:window", "Composer"));
    setMinimumSize(200, 200);

    mHeadersToEditorSplitter->setObjectName(QStringLiteral("mHeadersToEditorSplitter"));
    mHeadersToEditorSplitter->setChildrenCollapsible(false);

    auto v = new QVBoxLayout(mMainWidget);
    v->setContentsMargins({});
    v->addWidget(mNearExpiryWarning);
    v->addWidget(mHeadersToEditorSplitter);

    mHeadersArea->setSizePolicy(mHeadersToEditorSplitter->sizePolicy().horizontalPolicy(), QSizePolicy::Expanding);
    mHeadersToEditorSplitter->addWidget(mHeadersArea);

    const QList<int> defaultSizes{0};
    mHeadersToEditorSplitter->setSizes(defaultSizes);

    mGrid->setColumnStretch(0, 1);
    mGrid->setColumnStretch(1, 100);
    mGrid->setRowStretch(3 + 1, 100);
    int row = 0;

    mRunKeyResolverTimer = new QTimer(this);
    mRunKeyResolverTimer->setSingleShot(true);
    mRunKeyResolverTimer->setInterval(500ms);
    connect(mRunKeyResolverTimer, &QTimer::timeout, this, &ComposerWindow::runKeyResolver);

    // From
    mLblFrom->setObjectName(QStringLiteral("fromLineLabel"));
    mLblFrom->setFixedWidth(mRecipientEditor->setFirstColumnWidth(0));
    mLblFrom->setBuddy(mEdtFrom);

    auto fromWrapper = new QWidget(mHeadersArea);
    auto fromWrapperLayout = new QHBoxLayout(fromWrapper);
    fromWrapperLayout->setContentsMargins({});

    mEdtFrom->installEventFilter(this);
    mEdtFrom->setText(mFrom);
    mEdtFrom->setObjectName(QStringLiteral("fromLine"));
    fromWrapperLayout->addWidget(mEdtFrom);
    mComposerBase->setIdentity(mIdentity);

    mButtonFrom->setText(i18nc("@action:button", "Configure"));
    mButtonFrom->setIcon(QIcon::fromTheme(u"configure-symbolic"_s));
    mButtonFrom->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred);
    connect(mButtonFrom, &QPushButton::clicked, this, &ComposerWindow::slotEditIdentity);
    fromWrapperLayout->addWidget(mButtonFrom);

    mGrid->addWidget(mLblFrom, row, 0);
    mGrid->addWidget(fromWrapper, row, 1);
    row++;

    // Recipients
    mGrid->addWidget(mRecipientEditor, row, 0, 1, 2);
    mComposerBase->setRecipientsEditor(mRecipientEditor);
    mRecipientEditor->setCompletionMode(KCompletion::CompletionPopup);
    connect(mRecipientEditor, &RecipientsEditor::lineAdded, this, [this](KPIM::MultiplyingLine *line) {
        slotRecipientEditorLineAdded(qobject_cast<RecipientLineNG *>(line));
    });
    row++;

    // Subject
    mEdtSubject->setObjectName(u"subjectLine"_s);
    mLblSubject->setObjectName(u"subjectLineLabel"_s);
    mLblSubject->setBuddy(mEdtSubject);
    mGrid->addWidget(mLblSubject, row, 0);
    mGrid->addWidget(mEdtSubject, row, 1);
    row++;

    auto editorWidget = new QWidget();
    auto vLayout = new QVBoxLayout(editorWidget);
    vLayout->setContentsMargins({});
    vLayout->setSpacing(0);
    mHeadersToEditorSplitter->addWidget(editorWidget);

    // Message widget
    auto connectionLossWidget = new KMessageWidget(this);
    connectionLossWidget->hide();
    connectionLossWidget->setWordWrap(true);
    connectionLossWidget->setPosition(KMessageWidget::Position::Header);
    vLayout->addWidget(connectionLossWidget);
    auto &websocketClient = WebsocketClient::self();
    connect(&websocketClient, &WebsocketClient::stateChanged, this, [&websocketClient, connectionLossWidget]() {
        if (websocketClient.state() == WebsocketClient::State::Connected) {
            connectionLossWidget->hide();
        } else {
            connectionLossWidget->setText(websocketClient.stateDisplay());
            connectionLossWidget->show();
        }
    });
    connect(&websocketClient, &WebsocketClient::emailSentSuccessfully, this, [this](const QString &id) {
        if (id == mComposerBase->mailId()) {
            auto &draftManager = DraftManager::self();
            draftManager.remove(draftManager.draftById(id.toUtf8()));
            hide();
        }
    });

    // Rich text editor
    mRichTextComposer->setProperty("_breeze_borders_sides", QVariant::fromValue(QFlags{Qt::TopEdge}));
    mRichTextComposer->setProperty("_breeze_force_frame", true);
    mComposerBase->setEditor(mRichTextComposer);
    vLayout->addWidget(mRichTextEditorWidget);

    auto attachmentModel = new MessageComposer::AttachmentModel(this);
    auto attachmentView = new AttachmentView(attachmentModel, mHeadersToEditorSplitter);
    attachmentView->hideIfEmpty();
    connect(attachmentView, &AttachmentView::modified, this, &ComposerWindow::setModified);
    connect(attachmentView, &AttachmentView::errorOccurred, this, &ComposerWindow::slotHandleError);

    auto attachmentController = new AttachmentController(attachmentModel, attachmentView, this);

    mComposerBase->setAttachmentController(attachmentController);
    mComposerBase->setAttachmentModel(attachmentModel);

    auto signatureController = new MessageComposer::SignatureController(this);
    connect(signatureController, &MessageComposer::SignatureController::enableHtml, this, &ComposerWindow::enableHtml);
    signatureController->setIdentity(mIdentity);
    signatureController->setEditor(mComposerBase->editor());
    mComposerBase->setSignatureController(signatureController);

    connect(signatureController,
            &MessageComposer::SignatureController::signatureAdded,
            mComposerBase->editor()->externalComposer(),
            &KPIMTextEdit::RichTextExternalComposer::startExternalEditor);

    setupStatusBar(attachmentView->widget());
    setupActions();

    setStandardToolBarMenuEnabled(true);

    toolBar(u"mainToolBar"_s)->show();

    connect(expiryChecker().get(),
            &Kleo::ExpiryChecker::expiryMessage,
            this,
            [&](const GpgME::Key &key, QString msg, Kleo::ExpiryChecker::ExpiryInformation info, bool isNewMessage) {
                Q_UNUSED(isNewMessage);
                if (info == Kleo::ExpiryChecker::OwnKeyExpired || info == Kleo::ExpiryChecker::OwnKeyNearExpiry) {
                    const auto plainMsg = msg.replace(QStringLiteral("<p>"), QStringLiteral(" "))
                                              .replace(QStringLiteral("</p>"), QStringLiteral(" "))
                                              .replace(QStringLiteral("<p align=center>"), QStringLiteral(" "));
                    mNearExpiryWarning->addInfo(plainMsg);
                    mNearExpiryWarning->setWarning(info == Kleo::ExpiryChecker::OwnKeyExpired);
                    mNearExpiryWarning->animatedShow();
                }
                const QList<KPIM::MultiplyingLine *> lstLines = mRecipientEditor->lines();
                for (KPIM::MultiplyingLine *line : lstLines) {
                    auto recipient = line->data().dynamicCast<Recipient>();
                    if (recipient->key().primaryFingerprint() == key.primaryFingerprint()) {
                        auto recipientLine = qobject_cast<RecipientLineNG *>(line);
                        QString iconName = QStringLiteral("emblem-warning");
                        if (info == Kleo::ExpiryChecker::OtherKeyExpired) {
                            mAcceptedSolution = false;
                            iconName = QStringLiteral("emblem-error");
                            const auto showCryptoIndicator = true;
                            const auto encrypt = mEncryptAction->isChecked();

                            const bool showAllIcons = showCryptoIndicator && encrypt;
                            if (!showAllIcons) {
                                recipientLine->setIcon(QIcon(), msg);
                                return;
                            }
                        }

                        recipientLine->setIcon(QIcon::fromTheme(iconName), msg);
                        return;
                    }
                }
            });

    // TODO make it possible to show this
    auto dictionaryComboBox = new Sonnet::DictionaryComboBox(this);
    dictionaryComboBox->hide();
    mComposerBase->setDictionary(dictionaryComboBox);

    slotIdentityChanged();
    runKeyResolver();

    connect(mMessageDispatcher, &MessageDispatcher::errorOccurred, this, &ComposerWindow::slotHandleError);
    connect(mComposerBase, &MessageComposer::ComposerViewBase::failed, this, &ComposerWindow::slotHandleError);
    connect(mComposerBase, &MessageComposer::ComposerViewBase::closeWindow, this, &ComposerWindow::slotCloseWindow);
    connect(mMessageDispatcher, &MessageDispatcher::sentSuccessfully, this, &ComposerWindow::slotCloseWindow);
}

void ComposerWindow::slotHandleError(const QString &errorMessage)
{
    KMessageBox::error(this, errorMessage);
}

void ComposerWindow::reset(const QString &fromAddress, const QString &name, const QByteArray &bearerToken)
{
    mFrom = fromAddress;
    bool isNew = false;
    mIdentity = IdentityManager::self().fromEmail(fromAddress, isNew);
    if (isNew) {
        // fill the idenity with default fields
        auto dlg = new KMail::IdentityDialog;
        mIdentity.setFullName(name);
        dlg->setIdentity(mIdentity);
        connect(dlg, &KMail::IdentityDialog::keyListingFinished, this, [this, dlg]() {
            dlg->updateIdentity(mIdentity);
            IdentityManager::self().updateIdentity(mIdentity);
        });
    }
    mMessageDispatcher->setBearerToken(bearerToken);
    mEdtSubject->setText(QString());
    mRecipientEditor->clear();
    mComposerBase->editor()->setText(QString{});
    mComposerBase->attachmentController()->clear();

    const QList<KPIM::MultiplyingLine *> lstLines = mRecipientEditor->lines();
    for (KPIM::MultiplyingLine *line : lstLines) {
        slotRecipientEditorLineAdded(qobject_cast<RecipientLineNG *>(line));
    }
}

void ComposerWindow::setupActions()
{
    // Save as draft
    auto action = new QAction(QIcon::fromTheme(QStringLiteral("document-save")), i18n("Save as Encrypted &Draft"), this);
    actionCollection()->addAction(QStringLiteral("save_in_drafts"), action);
    actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::CTRL | Qt::Key_S));
    connect(action, &QAction::triggered, this, &ComposerWindow::slotSaveDraft);

    // Save as file
    action = new QAction(QIcon::fromTheme(QStringLiteral("document-save")), i18n("Save as &File"), this);
    actionCollection()->addAction(QStringLiteral("save_as_file"), action);
    connect(action, &QAction::triggered, this, &ComposerWindow::slotSaveAsFile);

    // Insert file
    action = new QAction(QIcon::fromTheme(QStringLiteral("document-open")), i18n("&Insert Text File..."), this);
    actionCollection()->addAction(QStringLiteral("insert_file"), action);
    connect(action, &QAction::triggered, this, &ComposerWindow::slotInsertFile);

    // Spellchecking
    mAutoSpellCheckingAction = new KToggleAction(QIcon::fromTheme(QStringLiteral("tools-check-spelling")), i18n("&Automatic Spellchecking"), this);
    actionCollection()->addAction(QStringLiteral("options_auto_spellchecking"), mAutoSpellCheckingAction);
    const bool spellChecking = MessageComposer::MessageComposerSettings::self()->autoSpellChecking();
    mAutoSpellCheckingAction->setChecked(spellChecking);
    slotAutoSpellCheckingToggled(spellChecking);
    connect(mAutoSpellCheckingAction, &KToggleAction::toggled, this, &ComposerWindow::slotAutoSpellCheckingToggled);
    connect(mComposerBase->editor(), &TextCustomEditor::RichTextEditor::checkSpellingChanged, this, &ComposerWindow::slotAutoSpellCheckingToggled);

    action = new QAction(QIcon::fromTheme(QStringLiteral("tools-check-spelling")), i18n("&Spellchecker..."), this);
    action->setIconText(i18n("Spellchecker"));
    actionCollection()->addAction(QStringLiteral("setup_spellchecker"), action);
    connect(action, &QAction::triggered, this, &ComposerWindow::slotSpellcheckConfig);

    // Recent actions
    mRecentAction = new KRecentFilesAction(QIcon::fromTheme(QStringLiteral("document-open")), i18n("&Insert Recent Text File"), this);
    actionCollection()->addAction(QStringLiteral("insert_file_recent"), mRecentAction);
    connect(mRecentAction, &KRecentFilesAction::urlSelected, this, &ComposerWindow::slotInsertRecentFile);
    connect(mRecentAction, &KRecentFilesAction::recentListCleared, this, &ComposerWindow::slotRecentListFileClear);

    const QStringList urls = MessageComposer::MessageComposerSettings::self()->recentUrls();
    for (const QString &url : urls) {
        mRecentAction->addUrl(QUrl(url));
    }

    // print
    KStandardAction::print(this, &ComposerWindow::slotPrint, actionCollection());
    KStandardAction::printPreview(this, &ComposerWindow::slotPrintPreview, actionCollection());

    // Send email action
    action = new QAction(QIcon::fromTheme(QStringLiteral("mail-send")), i18n("&Send Mail"), this);
    actionCollection()->addAction(QStringLiteral("mail_send"), action);
    actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::CTRL | Qt::Key_Return));
    connect(action, &QAction::triggered, this, &ComposerWindow::slotSend);

    // Toggle rich text
    mMarkupAction = new KToggleAction(i18n("Rich Text Editing"), this);
    mMarkupAction->setIcon(QIcon::fromTheme(QStringLiteral("preferences-desktop-font")));
    mMarkupAction->setIconText(i18n("Rich Text"));
    mMarkupAction->setToolTip(i18n("Toggle rich text editing mode"));
    actionCollection()->addAction(QStringLiteral("html"), mMarkupAction);
    connect(mMarkupAction, &KToggleAction::triggered, this, &ComposerWindow::slotToggleMarkup);

    mWordWrapAction = new KToggleAction(i18n("&Wordwrap"), this);
    actionCollection()->addAction(QStringLiteral("wordwrap"), mWordWrapAction);
    mWordWrapAction->setChecked(MessageComposer::MessageComposerSettings::self()->wordWrap());
    connect(mWordWrapAction, &KToggleAction::toggled, this, &ComposerWindow::slotWordWrapToggled);

    // Encryption action
    mEncryptAction = new KToggleAction(QIcon::fromTheme(QStringLiteral("document-encrypt")), i18n("&Encrypt Message"), this);
    mEncryptAction->setIconText(i18n("Encrypt"));
    actionCollection()->addAction(QStringLiteral("encrypt_message"), mEncryptAction);
    connect(mEncryptAction, &KToggleAction::toggled, this, &ComposerWindow::slotEncryptionButtonIconUpdate);

    // Signing action
    mSignAction = new KToggleAction(QIcon::fromTheme(QStringLiteral("document-sign")), i18n("&Sign Message"), this);
    mSignAction->setIconText(i18n("Sign"));
    actionCollection()->addAction(QStringLiteral("sign_message"), mSignAction);

    connect(mSignAction, &KToggleAction::triggered, this, &ComposerWindow::slotSignToggled);

    // Append signature
    mAppendSignature = new QAction(i18n("Append S&ignature"), this);
    actionCollection()->addAction(QStringLiteral("append_signature"), mAppendSignature);
    connect(mAppendSignature, &QAction::triggered, mComposerBase->signatureController(), &MessageComposer::SignatureController::appendSignature);

    // Prepend signature
    mPrependSignature = new QAction(i18n("Pr&epend Signature"), this);
    actionCollection()->addAction(QStringLiteral("prepend_signature"), mPrependSignature);
    connect(mPrependSignature, &QAction::triggered, mComposerBase->signatureController(), &MessageComposer::SignatureController::prependSignature);

    mInsertSignatureAtCursorPosition = new QAction(i18n("Insert Signature At C&ursor Position"), this);
    actionCollection()->addAction(QStringLiteral("insert_signature_at_cursor_position"), mInsertSignatureAtCursorPosition);
    connect(mInsertSignatureAtCursorPosition,
            &QAction::triggered,
            mComposerBase->signatureController(),
            &MessageComposer::SignatureController::insertSignatureAtCursor);

    action = new QAction(i18n("Paste as Attac&hment"), this);
    actionCollection()->addAction(QStringLiteral("paste_att"), action);
    connect(action, &QAction::triggered, this, &ComposerWindow::slotPasteAsAttachment);

    action = new QAction(i18n("Cl&ean Spaces"), this);
    actionCollection()->addAction(QStringLiteral("clean_spaces"), action);
    connect(action, &QAction::triggered, mComposerBase->signatureController(), &MessageComposer::SignatureController::cleanSpace);

    mRichTextComposer->composerActions()->createActions(actionCollection());

    KStandardAction::close(this, &ComposerWindow::close, actionCollection());

    KStandardAction::undo(mGlobalAction, &KMComposerGlobalAction::slotUndo, actionCollection());
    KStandardAction::redo(mGlobalAction, &KMComposerGlobalAction::slotRedo, actionCollection());
    KStandardAction::cut(mGlobalAction, &KMComposerGlobalAction::slotCut, actionCollection());
    KStandardAction::copy(mGlobalAction, &KMComposerGlobalAction::slotCopy, actionCollection());
    KStandardAction::paste(mGlobalAction, &KMComposerGlobalAction::slotPaste, actionCollection());
    mSelectAll = KStandardAction::selectAll(mGlobalAction, &KMComposerGlobalAction::slotMarkAll, actionCollection());

    mFindText = KStandardAction::find(mRichTextEditorWidget, &TextCustomEditor::RichTextEditorWidget::slotFind, actionCollection());
    mFindNextText = KStandardAction::findNext(mRichTextEditorWidget, &TextCustomEditor::RichTextEditorWidget::slotFindNext, actionCollection());

    mReplaceText = KStandardAction::replace(mRichTextEditorWidget, &TextCustomEditor::RichTextEditorWidget::slotReplace, actionCollection());

    mComposerBase->attachmentController()->createActions();

    createGUI(u"composerui.rc"_s);

    connect(toolBar(QStringLiteral("htmlToolBar"))->toggleViewAction(), &QAction::toggled, this, &ComposerWindow::htmlToolBarVisibilityChanged);

    const QList<KPIM::MultiplyingLine *> lstLines = mRecipientEditor->lines();
    for (KPIM::MultiplyingLine *line : lstLines) {
        slotRecipientEditorLineAdded(qobject_cast<RecipientLineNG *>(line));
    }
}

void ComposerWindow::setupStatusBar(QWidget *w)
{
    statusBar()->addWidget(w);
    mStatusbarLabel = new QLabel(this);
    mStatusbarLabel->setAlignment(Qt::AlignLeft | Qt::AlignVCenter);
    statusBar()->addPermanentWidget(mStatusbarLabel);

    mCursorLineLabel = new QLabel(this);
    mCursorLineLabel->setTextFormat(Qt::PlainText);
    mCursorLineLabel->setText(i18nc("Shows the linenumber of the cursor position.", " Line: %1 ", QStringLiteral("     ")));
    statusBar()->addPermanentWidget(mCursorLineLabel);

    mCursorColumnLabel = new QLabel(i18n(" Column: %1 ", QStringLiteral("     ")));
    mCursorColumnLabel->setTextFormat(Qt::PlainText);
    statusBar()->addPermanentWidget(mCursorColumnLabel);

    connect(mComposerBase->editor(), &QTextEdit::cursorPositionChanged, this, &ComposerWindow::slotCursorPositionChanged);
    slotCursorPositionChanged();
}

void ComposerWindow::reply(const KMime::Message::Ptr &originalMessage)
{
    MailTemplates::reply(originalMessage, [this](const KMime::Message::Ptr &message) {
        setMessage(message);

        Q_EMIT initialized();
    });
}

void ComposerWindow::forward(const KMime::Message::Ptr &originalMessage)
{
    MailTemplates::forward(originalMessage, [this](const KMime::Message::Ptr &message) {
        setMessage(message);

        Q_EMIT initialized();
    });
}

void ComposerWindow::setMessage(const KMime::Message::Ptr &msg)
{
    mEdtSubject->setText(msg->subject()->asUnicodeString());
    mComposerBase->setMessage(msg);
}

bool ComposerWindow::isComposerModified() const
{
    return mComposerBase->editor()->document()->isModified() || mComposerBase->recipientsEditor()->isModified() || mEdtSubject->isModified();
}

void ComposerWindow::setModified(bool isModified)
{
    mIsModified = isModified;

    mComposerBase->editor()->document()->setModified(isModified);
    if (!isModified) {
        mComposerBase->recipientsEditor()->clearModified();
        mEdtSubject->setModified(false);
    }
}

bool ComposerWindow::isModified() const
{
    return mIsModified || isComposerModified();
}

void ComposerWindow::setSigning(bool sign, bool setByUser)
{
    const bool wasModified = isModified();
    if (setByUser) {
        setModified(true);
    }
    if (!mSignAction->isEnabled()) {
        sign = false;
    }

    // check if the user defined a signing key for the current identity
    if (sign && !mLastIdentityHasSigningKey) {
        if (setByUser) {
            KMessageBox::error(this,
                               i18n("<qt><p>In order to be able to sign "
                                    "this message you first have to "
                                    "define the (OpenPGP or S/MIME) signing key "
                                    "to use.</p>"
                                    "<p>Please select the key to use "
                                    "in the identity configuration.</p>"
                                    "</qt>"),
                               i18nc("@title:window", "Undefined Signing Key"));
            setModified(wasModified);
        }
        sign = false;
    }

    // make sure the mSignAction is in the right state
    mSignAction->setChecked(sign);

    // mark the attachments for (no) signing
    // if (canSignEncryptAttachments()) {
    //    mComposerBase->attachmentModel()->setSignSelected(sign);
    //}
}

std::unique_ptr<Kleo::KeyResolverCore> ComposerWindow::fillKeyResolver()
{
    auto keyResolverCore = std::make_unique<Kleo::KeyResolverCore>(true, sign());

    keyResolverCore->setMinimumValidity(GpgME::UserID::Unknown);

    QStringList signingKeys, encryptionKeys;

    if (cryptoMessageFormat() & Kleo::AnyOpenPGP) {
        if (!mIdentity.pgpSigningKey().isEmpty()) {
            signingKeys.push_back(QLatin1String(mIdentity.pgpSigningKey()));
        }
        if (!mIdentity.pgpEncryptionKey().isEmpty()) {
            encryptionKeys.push_back(QLatin1String(mIdentity.pgpEncryptionKey()));
        }
    }

    if (cryptoMessageFormat() & Kleo::AnySMIME) {
        if (!mIdentity.smimeSigningKey().isEmpty()) {
            signingKeys.push_back(QLatin1String(mIdentity.smimeSigningKey()));
        }
        if (!mIdentity.smimeEncryptionKey().isEmpty()) {
            encryptionKeys.push_back(QLatin1String(mIdentity.smimeEncryptionKey()));
        }
    }

    keyResolverCore->setSender(mIdentity.fullEmailAddr());
    keyResolverCore->setSigningKeys(signingKeys);
    keyResolverCore->setOverrideKeys({{GpgME::UnknownProtocol, {{keyResolverCore->normalizedSender(), encryptionKeys}}}});

    QStringList recipients;
    const auto lst = mRecipientEditor->lines();
    for (auto line : lst) {
        auto recipient = line->data().dynamicCast<Recipient>();
        recipients.push_back(recipient->email());
    }

    keyResolverCore->setRecipients(recipients);

    qWarning() << recipients;

    return keyResolverCore;
}

void ComposerWindow::slotEncryptionButtonIconUpdate()
{
    const auto state = mEncryptAction->isChecked();

    auto icon = QIcon::fromTheme(QStringLiteral("document-encrypt"));
    QString tooltip;
    if (state) {
        tooltip = i18nc("@info:tooltip", "Encrypt");
    } else {
        tooltip = i18nc("@info:tooltip", "Not Encrypt");
        icon = QIcon::fromTheme(QStringLiteral("document-decrypt"));
    }

    if (mAcceptedSolution) {
        auto overlay = QIcon::fromTheme(QStringLiteral("emblem-added"));
        if (state) {
            overlay = QIcon::fromTheme(QStringLiteral("emblem-checked"));
        }
        icon = KIconUtils::addOverlay(icon, overlay, Qt::BottomRightCorner);
    } else {
        const auto lst = mRecipientEditor->lines();
        bool empty = false;

        if (lst.size() == 1) {
            const auto line = qobject_cast<RecipientLineNG *>(lst.first());
            if (line->recipientsCount() == 0) {
                empty = true;
            }
        }
        if (state && !empty) {
            auto overlay = QIcon::fromTheme(QStringLiteral("emblem-warning"));
            icon = KIconUtils::addOverlay(icon, overlay, Qt::BottomRightCorner);
        }
    }
    mEncryptAction->setIcon(icon);
    mEncryptAction->setToolTip(tooltip);
}

void ComposerWindow::runKeyResolver()
{
    auto keyResolverCore = fillKeyResolver();
    auto result = keyResolverCore->resolve();

    const auto lst = mRecipientEditor->lines();

    if (lst.size() == 1) {
        const auto line = qobject_cast<RecipientLineNG *>(lst.first());
        if (line->recipientsCount() == 0) {
            mAcceptedSolution = false;
            slotEncryptionButtonIconUpdate();
            return;
        }
    }

    mAcceptedSolution = result.flags & Kleo::KeyResolverCore::AllResolved;

    for (auto line_ : lst) {
        auto line = qobject_cast<RecipientLineNG *>(line_);
        Q_ASSERT(line);
        auto recipient = line->data().dynamicCast<Recipient>();

        QString dummy;
        QString addrSpec;
        if (KEmailAddress::splitAddress(recipient->email(), dummy, addrSpec, dummy) != KEmailAddress::AddressOk) {
            addrSpec = recipient->email();
        }

        auto resolvedKeys = result.solution.encryptionKeys[addrSpec];
        GpgME::Key key;
        if (resolvedKeys.size() == 0) { // no key found for recipient
            // Search for any key, also for not accepted ons, to at least give the user more info.
            key = Kleo::KeyCache::instance()->findBestByMailBox(addrSpec.toUtf8().constData(), GpgME::UnknownProtocol, Kleo::KeyCache::KeyUsage::Encrypt);
            key.update(); // We need tofu information for key.
            recipient->setKey(key);
        } else { // A key was found for recipient
            key = resolvedKeys.front();
            if (recipient->key().primaryFingerprint() != key.primaryFingerprint()) {
                key.update(); // We need tofu information for key.
                recipient->setKey(key);
            }
        }

        annotateRecipientEditorLineWithCryptoInfo(line);

        if (!key.isNull()) {
            mExpiryChecker->checkKey(key, Kleo::ExpiryChecker::EncryptionKey);
        }
    }

    slotEncryptionButtonIconUpdate();
}

void ComposerWindow::annotateRecipientEditorLineWithCryptoInfo(RecipientLineNG *line)
{
    auto recipient = line->data().dynamicCast<Recipient>();
    const auto key = recipient->key();

    const auto showCryptoIndicator = true;
    const auto encrypt = mEncryptAction->isChecked();

    const bool showPositiveIcons = showCryptoIndicator && encrypt;
    const bool showAllIcons = showCryptoIndicator && encrypt;

    QString dummy;
    QString addrSpec;
    bool invalidEmail = false;
    if (KEmailAddress::splitAddress(recipient->email(), dummy, addrSpec, dummy) != KEmailAddress::AddressOk) {
        invalidEmail = true;
        addrSpec = recipient->email();
    }

    if (key.isNull()) {
        recipient->setEncryptionAction(Kleo::Impossible);
        if (showAllIcons && !invalidEmail) {
            const auto icon = QIcon::fromTheme(QStringLiteral("emblem-error"));
            line->setIcon(icon, i18nc("@info:tooltip", "No key found for the recipient."));
        } else {
            line->setIcon(QIcon());
        }
        line->setProperty("keyStatus", invalidEmail ? InProgress : NoKey);
        return;
    }

    CryptoKeyState keyState = KeyOk;

    if (recipient->encryptionAction() != Kleo::DoIt) {
        recipient->setEncryptionAction(Kleo::DoIt);
    }

    QString tooltip;

    const auto uids = key.userIDs();
    const auto _uid = findSendersUid(addrSpec.toStdString(), uids);
    GpgME::UserID uid;
    if (_uid == uids.cend()) {
        uid = key.userID(0);
    } else {
        uid = *_uid;
    }

    const auto trustLevel = Kleo::trustLevel(uid);
    switch (trustLevel) {
    case Kleo::Level0:
        if (uid.tofuInfo().isNull()) {
            tooltip = i18nc("@info:tooltip",
                            "The encryption key is not trusted. It hasn't enough validity. "
                            "You can sign the key, if you communicated the fingerprint by another channel. "
                            "Click the icon for details.");
            keyState = NoTrusted;
        } else {
            switch (uid.tofuInfo().validity()) {
            case GpgME::TofuInfo::NoHistory:
                tooltip = i18nc("@info:tooltip",
                                "The encryption key is not trusted. "
                                "It hasn't been used anywhere to guarantee it belongs to the stated person. "
                                "By using the key will be trusted more. "
                                "Or you can sign the key, if you communicated the fingerprint by another channel. "
                                "Click the icon for details.");
                break;
            case GpgME::TofuInfo::Conflict:
                tooltip = i18nc("@info:tooltip",
                                "The encryption key is not trusted. It has conflicting TOFU data. "
                                "Click the icon for details.");
                keyState = NoKey;
                break;
            case GpgME::TofuInfo::ValidityUnknown:
                tooltip = i18nc("@info:tooltip",
                                "The encryption key is not trusted. It has unknown validity in TOFU data. "
                                "Click the icon for details.");
                keyState = NoKey;
                break;
            default:
                tooltip = i18nc("@info:tooltip",
                                "The encryption key is not trusted. The key is marked as bad. "
                                "Click the icon for details.");
                keyState = NoKey;
            }
        }
        break;
    case Kleo::Level1:
        tooltip = i18nc("@info:tooltip",
                        "The encryption key is only marginally trusted and hasn't been used enough time to guarantee it belongs to the stated person. "
                        "By using the key will be trusted more. "
                        "Or you can sign the key, if you communicated the fingerprint by another channel. "
                        "Click the icon for details.");
        break;
    case Kleo::Level2:
        if (uid.tofuInfo().isNull()) {
            tooltip = i18nc("@info:tooltip",
                            "The encryption key is only marginally trusted. "
                            "You can sign the key, if you communicated the fingerprint by another channel. "
                            "Click the icon for details.");
        } else {
            tooltip = i18nc("@info:tooltip",
                            "The encryption key is only marginally trusted, but has been used enough times to be very likely controlled by the stated person. "
                            "By using the key will be trusted more. "
                            "Or you can sign the key, if you communicated the fingerprint by another channel. "
                            "Click the icon for details.");
        }
        break;
    case Kleo::Level3:
        tooltip = i18nc("@info:tooltip",
                        "The encryption key is fully trusted. You can raise the security level, by signing the key. "
                        "Click the icon for details.");
        break;
    case Kleo::Level4:
        tooltip = i18nc("@info:tooltip",
                        "The encryption key is ultimately trusted or is signed by another ultimately trusted key. "
                        "Click the icon for details.");
        break;
    default:
        Q_UNREACHABLE();
    }

    // Ensure the tooltips are word wrapped
    tooltip = u"<div>"_s + tooltip + u"</div>"_s;

    if (keyState == NoKey) {
        mAcceptedSolution = false;
        if (showAllIcons) {
            line->setIcon(QIcon::fromTheme(QStringLiteral("emblem-error")), tooltip);
        } else {
            line->setIcon(QIcon());
        }
    } else if (trustLevel == Kleo::Level0 && encrypt) {
        if (keyState == NoTrusted) {
            line->setIcon(QIcon::fromTheme(QStringLiteral("emblem-question")), tooltip);
        } else {
            line->setIcon(QIcon::fromTheme(QStringLiteral("emblem-warning")), tooltip);
        }
    } else if (showPositiveIcons) {
        // Magically, the icon name maps precisely to each trust level
        // line->setIcon(QIcon::fromTheme(QStringLiteral("gpg-key-trust-level-%1").arg(trustLevel)), tooltip);
        line->setIcon(QIcon::fromTheme(QStringLiteral("emblem-success")), tooltip);
    } else {
        line->setIcon(QIcon());
    }

    if (line->property("keyStatus") != keyState) {
        line->setProperty("keyStatus", keyState);
    }
}

void ComposerWindow::slotSignToggled(bool on)
{
    setSigning(on, true);
}

bool ComposerWindow::sign() const
{
    return mSignAction->isChecked();
}

void ComposerWindow::slotSend()
{
    mComposerBase->setFrom(mFrom);
    mComposerBase->setSubject(mEdtSubject->text());

    if (mComposerBase->to().isEmpty()) {
        if (mComposerBase->cc().isEmpty() && mComposerBase->bcc().isEmpty()) {
            KMessageBox::information(this,
                                     i18n("You must specify at least one receiver, "
                                          "either in the To: field or as CC or as BCC."));

            return;
        } else {
            const int rc = KMessageBox::questionTwoActions(this,
                                                           i18n("To: field is empty. "
                                                                "Send message anyway?"),
                                                           i18nc("@title:window", "No To: specified"),
                                                           KGuiItem(i18n("S&end as Is"), QLatin1String("mail-send")),
                                                           KGuiItem(i18n("&Specify the To field"), QLatin1String("edit-rename")));
            if (rc == KMessageBox::ButtonCode::SecondaryAction) {
                return;
            }
        }
    }

    if (mComposerBase->subject().isEmpty()) {
        mEdtSubject->setFocus();
        const int rc = KMessageBox::questionTwoActions(this,
                                                       i18n("You did not specify a subject. "
                                                            "Send message anyway?"),
                                                       i18nc("@title:window", "No Subject Specified"),
                                                       KGuiItem(i18n("S&end as Is"), QStringLiteral("mail-send")),
                                                       KGuiItem(i18n("&Specify the Subject"), QStringLiteral("edit-rename")));
        if (rc == KMessageBox::ButtonCode::SecondaryAction) {
            return;
        }
    }

    KCursorSaver saver(Qt::WaitCursor);
    const bool encrypt = mEncryptAction->isChecked();

    mComposerBase->setCryptoOptions(sign(), encrypt, cryptoMessageFormat());

    mComposerBase->send();
}

void ComposerWindow::changeCryptoAction()
{
    if (!QGpgME::openpgp() && !QGpgME::smime()) {
        // no crypto whatsoever
        mEncryptAction->setEnabled(false);
        mSignAction->setEnabled(false);
        setSigning(false);
    } else {
        setSigning(true);
        mEncryptAction->setChecked(true);
    }
}

void ComposerWindow::slotToggleMarkup()
{
    htmlToolBarVisibilityChanged(mMarkupAction->isChecked());
}

void ComposerWindow::htmlToolBarVisibilityChanged(bool visible)
{
    if (visible) {
        enableHtml();
    } else {
        disableHtml(LetUserConfirm);
    }
}

void ComposerWindow::enableHtml()
{
    if (mForceDisableHtml) {
        disableHtml(NoConfirmationNeeded);
        return;
    }

    mRichTextComposer->activateRichText();
    if (!toolBar(QStringLiteral("htmlToolBar"))->isVisible()) {
        // Use singleshot, as we we might actually be called from a slot that wanted to disable the
        // toolbar (but the messagebox in disableHtml() prevented that and called us).
        // The toolbar can't correctly deal with being enabled right in a slot called from the "disabled"
        // signal, so wait one event loop run for that.
        QTimer::singleShot(0, toolBar(QStringLiteral("htmlToolBar")), &QWidget::show);
    }
    if (!mMarkupAction->isChecked()) {
        mMarkupAction->setChecked(true);
    }

    mRichTextComposer->composerActions()->updateActionStates();
    mRichTextComposer->composerActions()->setActionsEnabled(true);
}

void ComposerWindow::disableHtml(Confirmation confirmation)
{
    bool forcePlainTextMarkup = false;
    if (confirmation == LetUserConfirm && mRichTextComposer->composerControler()->isFormattingUsed()) {
        int choice = KMessageBox::warningTwoActionsCancel(this,
                                                          i18n("Turning HTML mode off "
                                                               "will cause the text to lose the formatting. Are you sure?"),
                                                          i18n("Lose the formatting?"),
                                                          KGuiItem(i18n("Lose Formatting")),
                                                          KGuiItem(i18n("Add Markup Plain Text")),
                                                          KStandardGuiItem::cancel(),
                                                          QStringLiteral("LoseFormattingWarning"));

        switch (choice) {
        case KMessageBox::Cancel:
            enableHtml();
            return;
        case KMessageBox::ButtonCode::SecondaryAction:
            forcePlainTextMarkup = true;
            break;
        case KMessageBox::ButtonCode::PrimaryAction:
            break;
        }
    }

    mRichTextComposer->forcePlainTextMarkup(forcePlainTextMarkup);
    mRichTextComposer->switchToPlainText();
    mRichTextComposer->composerActions()->setActionsEnabled(false);

    if (toolBar(QStringLiteral("htmlToolBar"))->isVisible()) {
        // See the comment in enableHtml() why we use a singleshot timer, similar situation here.
        QTimer::singleShot(0, toolBar(QStringLiteral("htmlToolBar")), &QWidget::hide);
    }
    if (mMarkupAction->isChecked()) {
        mMarkupAction->setChecked(false);
    }
}

inline Kleo::chrono::days encryptOwnKeyNearExpiryWarningThresholdInDays()
{
    const int num = 30;
    return Kleo::chrono::days{qMax(1, num)};
}

inline Kleo::chrono::days encryptKeyNearExpiryWarningThresholdInDays()
{
    const int num = 14;
    return Kleo::chrono::days{qMax(1, num)};
}

inline Kleo::chrono::days encryptRootCertNearExpiryWarningThresholdInDays()
{
    const int num = 14;
    return Kleo::chrono::days{qMax(1, num)};
}

inline Kleo::chrono::days encryptChainCertNearExpiryWarningThresholdInDays()
{
    const int num = 14;
    return Kleo::chrono::days{qMax(1, num)};
}

std::shared_ptr<Kleo::ExpiryChecker> ComposerWindow::expiryChecker()
{
    if (!mExpiryChecker) {
        mExpiryChecker.reset(new Kleo::ExpiryChecker{Kleo::ExpiryCheckerSettings{encryptOwnKeyNearExpiryWarningThresholdInDays(),
                                                                                 encryptKeyNearExpiryWarningThresholdInDays(),
                                                                                 encryptRootCertNearExpiryWarningThresholdInDays(),
                                                                                 encryptChainCertNearExpiryWarningThresholdInDays()}});
    }
    return mExpiryChecker;
}

Kleo::CryptoMessageFormat ComposerWindow::cryptoMessageFormat() const
{
    return Kleo::AutoFormat;
}

void ComposerWindow::slotEditIdentity()
{
    QPointer<KMail::IdentityDialog> dlg = new KMail::IdentityDialog();
    dlg->setAttribute(Qt::WA_DeleteOnClose);
    dlg->setIdentity(mIdentity);
    dlg->open();

    connect(dlg, &KMail::IdentityDialog::accepted, this, [dlg, this]() {
        dlg->updateIdentity(mIdentity);
        IdentityManager::self().updateIdentity(mIdentity);
        IdentityManager::self().writeConfig();
        slotIdentityChanged();
    });
}

void ComposerWindow::slotIdentityChanged()
{
    mComposerBase->setIdentity(mIdentity);
    mLastIdentityHasSigningKey = !mIdentity.pgpSigningKey().isEmpty() || !mIdentity.smimeSigningKey().isEmpty();
    mLastIdentityHasEncryptionKey = !mIdentity.pgpEncryptionKey().isEmpty() || !mIdentity.smimeEncryptionKey().isEmpty();
    mComposerBase->signatureController()->setIdentity(mIdentity);
    mComposerBase->editor()->setAutocorrectionLanguage(mIdentity.autocorrectionLanguage());
    mComposerBase->dictionary()->setCurrentByDictionaryName(mIdentity.dictionary());
    mComposerBase->editor()->setSpellCheckingLanguage(mComposerBase->dictionary()->currentDictionary());

    bool bPGPEncryptionKey = !mIdentity.pgpEncryptionKey().isEmpty();
    bool bPGPSigningKey = !mIdentity.pgpSigningKey().isEmpty();
    bool bSMIMEEncryptionKey = !mIdentity.smimeEncryptionKey().isEmpty();
    bool bSMIMESigningKey = !mIdentity.smimeSigningKey().isEmpty();
    if (cryptoMessageFormat() & Kleo::AnyOpenPGP) {
        if (bPGPEncryptionKey) {
            auto const key = mKeyCache->findByKeyIDOrFingerprint(mIdentity.pgpEncryptionKey().constData());
            if (key.isNull() || !key.canEncrypt()) {
                bPGPEncryptionKey = false;
            }
        }
        if (bPGPSigningKey) {
            auto const key = mKeyCache->findByKeyIDOrFingerprint(mIdentity.pgpSigningKey().constData());
            if (key.isNull() || !key.canSign()) {
                bPGPSigningKey = false;
            }
        }
    } else {
        bPGPEncryptionKey = false;
        bPGPSigningKey = false;
    }

    if (cryptoMessageFormat() & Kleo::AnySMIME) {
        if (bSMIMEEncryptionKey) {
            auto const key = mKeyCache->findByKeyIDOrFingerprint(mIdentity.smimeEncryptionKey().constData());
            if (key.isNull() || !key.canEncrypt()) {
                bSMIMEEncryptionKey = false;
            }
        }
        if (bSMIMESigningKey) {
            auto const key = mKeyCache->findByKeyIDOrFingerprint(mIdentity.smimeSigningKey().constData());
            if (key.isNull() || !key.canSign()) {
                bSMIMESigningKey = false;
            }
        }
    } else {
        bSMIMEEncryptionKey = false;
        bSMIMESigningKey = false;
    }

    bool bNewIdentityHasSigningKey = bPGPSigningKey || bSMIMESigningKey;
    bool bNewIdentityHasEncryptionKey = bPGPEncryptionKey || bSMIMEEncryptionKey;

    if (!mKeyCache->initialized()) {
        // We need to start key listing on our own othweise KMail will crash and we want to wait till the cache is populated.
        mKeyCache->startKeyListing();
        connect(mKeyCache.get(), &Kleo::KeyCache::keyListingDone, this, [this]() {
            checkOwnKeyExpiry(mIdentity);
            runKeyResolver();
        });
    } else {
        checkOwnKeyExpiry(mIdentity);
    }

    // save the state of the sign and encrypt button
    if (!bNewIdentityHasEncryptionKey && mLastIdentityHasEncryptionKey) {
        mLastEncryptActionState = mEncryptAction->isChecked();
    }

    mSignAction->setEnabled(bNewIdentityHasSigningKey);

    if (!bNewIdentityHasSigningKey && mLastIdentityHasSigningKey) {
        mLastSignActionState = sign();
        setSigning(false);
    }
    // restore the last state of the sign and encrypt button
    if (bNewIdentityHasSigningKey && !mLastIdentityHasSigningKey) {
        setSigning(mLastSignActionState);
    }

    mLastIdentityHasSigningKey = bNewIdentityHasSigningKey;
    mLastIdentityHasEncryptionKey = bNewIdentityHasEncryptionKey;
    const KIdentityManagementCore::Signature sig = const_cast<KIdentityManagementCore::Identity &>(mIdentity).signature();
    bool isEnabledSignature = sig.isEnabledSignature();
    mAppendSignature->setEnabled(isEnabledSignature);
    mPrependSignature->setEnabled(isEnabledSignature);
    mInsertSignatureAtCursorPosition->setEnabled(isEnabledSignature);

    changeCryptoAction();

    Q_EMIT identityChanged();
}

void ComposerWindow::checkOwnKeyExpiry(const KIdentityManagementCore::Identity &ident)
{
    mNearExpiryWarning->clearInfo();
    mNearExpiryWarning->hide();

    if (cryptoMessageFormat() & Kleo::AnyOpenPGP) {
        if (!ident.pgpEncryptionKey().isEmpty()) {
            auto const key = mKeyCache->findByKeyIDOrFingerprint(ident.pgpEncryptionKey().constData());
            if (key.isNull() || !key.canEncrypt()) {
                mNearExpiryWarning->addInfo(i18nc("The argument is as PGP fingerprint",
                                                  "Your selected PGP key (%1) doesn't exist in your keyring or is not suitable for encryption.",
                                                  QString::fromUtf8(ident.pgpEncryptionKey())));
                mNearExpiryWarning->setWarning(true);
                mNearExpiryWarning->show();
            } else {
                mComposerBase->expiryChecker()->checkKey(key, Kleo::ExpiryChecker::OwnEncryptionKey);
            }
        }
        if (!ident.pgpSigningKey().isEmpty()) {
            if (ident.pgpSigningKey() != ident.pgpEncryptionKey()) {
                auto const key = mKeyCache->findByKeyIDOrFingerprint(ident.pgpSigningKey().constData());
                if (key.isNull() || !key.canSign()) {
                    mNearExpiryWarning->addInfo(i18nc("The argument is as PGP fingerprint",
                                                      "Your selected PGP signing key (%1) doesn't exist in your keyring or is not suitable for signing.",
                                                      QString::fromUtf8(ident.pgpSigningKey())));
                    mNearExpiryWarning->setWarning(true);
                    mNearExpiryWarning->show();
                } else {
                    mComposerBase->expiryChecker()->checkKey(key, Kleo::ExpiryChecker::OwnSigningKey);
                }
            }
        }
    }

    if (cryptoMessageFormat() & Kleo::AnySMIME) {
        if (!ident.smimeEncryptionKey().isEmpty()) {
            auto const key = mKeyCache->findByKeyIDOrFingerprint(ident.smimeEncryptionKey().constData());
            if (key.isNull() || !key.canEncrypt()) {
                mNearExpiryWarning->addInfo(i18nc("The argument is as SMIME fingerprint",
                                                  "Your selected SMIME key (%1) doesn't exist in your keyring or is not suitable for encryption.",
                                                  QString::fromUtf8(ident.smimeEncryptionKey())));
                mNearExpiryWarning->setWarning(true);
                mNearExpiryWarning->show();
            } else {
                mComposerBase->expiryChecker()->checkKey(key, Kleo::ExpiryChecker::OwnEncryptionKey);
            }
        }
        if (!ident.smimeSigningKey().isEmpty()) {
            if (ident.smimeSigningKey() != ident.smimeEncryptionKey()) {
                auto const key = mKeyCache->findByKeyIDOrFingerprint(ident.smimeSigningKey().constData());
                if (key.isNull() || !key.canSign()) {
                    mNearExpiryWarning->addInfo(i18nc("The argument is as SMIME fingerprint",
                                                      "Your selected SMIME signing key (%1) doesn't exist in your keyring or is not suitable for signing.",
                                                      QString::fromUtf8(ident.smimeSigningKey())));
                    mNearExpiryWarning->setWarning(true);
                    mNearExpiryWarning->show();
                } else {
                    mComposerBase->expiryChecker()->checkKey(key, Kleo::ExpiryChecker::OwnSigningKey);
                }
            }
        }
    }
}

void ComposerWindow::slotCursorPositionChanged()
{
    // Change Line/Column info in status bar
    const int line = mComposerBase->editor()->linePosition() + 1;
    const int col = mComposerBase->editor()->columnNumber() + 1;
    QString temp = i18nc("Shows the linenumber of the cursor position.", " Line: %1 ", line);
    mCursorLineLabel->setText(temp);
    temp = i18n(" Column: %1 ", col);
    mCursorColumnLabel->setText(temp);

    // Show link target in status bar
    if (mComposerBase->editor()->textCursor().charFormat().isAnchor()) {
        const QString text = mComposerBase->editor()->composerControler()->currentLinkText() + QLatin1String(" -> ")
            + mComposerBase->editor()->composerControler()->currentLinkUrl();
        mStatusbarLabel->setText(text);
    } else {
        mStatusbarLabel->clear();
    }
}

KIdentityManagementCore::Identity ComposerWindow::identity() const
{
    return mIdentity;
}

QString ComposerWindow::subject() const
{
    return mEdtSubject->text();
}

QString ComposerWindow::content() const
{
    return mComposerBase->editor()->toCleanHtml();
}

RecipientsEditor *ComposerWindow::recipientsEditor() const
{
    return mRecipientEditor;
}

void ComposerWindow::addAttachment(const QList<AttachmentInfo> &infos, bool showWarning)
{
    QStringList lst;
    for (const AttachmentInfo &info : infos) {
        if (showWarning) {
            lst.append(info.url.toDisplayString());
        }
        mComposerBase->addAttachment(info.url, info.comment, false);
    }
    if (showWarning) {
        // TODO
        // mAttachmentFromExternalMissing->setAttachmentNames(lst);
        // mAttachmentFromExternalMissing->animatedShow();
    }
}

void ComposerWindow::addAttachment(const QString &name, KMime::Headers::contentEncoding cte, const QByteArray &data, const QByteArray &mimeType)
{
    Q_UNUSED(cte)
    mComposerBase->addAttachment(name, name, data, mimeType);
}

void ComposerWindow::insertUrls(const QMimeData *source, const QList<QUrl> &urlList)
{
    QStringList urlAdded;
    for (const QUrl &url : urlList) {
        QString urlStr;
        if (url.scheme() == QLatin1String("mailto")) {
            urlStr = KEmailAddress::decodeMailtoUrl(url);
        } else {
            urlStr = url.toDisplayString();
            // Workaround #346370
            if (urlStr.isEmpty()) {
                urlStr = source->text();
            }
        }
        if (!urlAdded.contains(urlStr)) {
            mComposerBase->editor()->composerControler()->insertLink(urlStr);
            urlAdded.append(urlStr);
        }
    }
}

bool ComposerWindow::insertFromMimeData(const QMimeData *source, bool forceAttachment)
{
    // If this is a PNG image, either add it as an attachment or as an inline image
    if (source->hasHtml() && mComposerBase->editor()->textMode() == MessageComposer::RichTextComposerNg::Rich) {
        const QString html = QString::fromUtf8(source->data(QStringLiteral("text/html")));
        mComposerBase->editor()->insertHtml(html);
        return true;
    } else if (source->hasHtml() && (mComposerBase->editor()->textMode() == MessageComposer::RichTextComposerNg::Plain) && source->hasText()
               && !forceAttachment) {
        mComposerBase->editor()->insertPlainText(source->text());
        return true;
    } else if (source->hasImage() && source->hasFormat(QStringLiteral("image/png"))) {
        // Get the image data before showing the dialog, since that processes events which can delete
        // the QMimeData object behind our back
        const QByteArray imageData = source->data(QStringLiteral("image/png"));
        if (imageData.isEmpty()) {
            return true;
        }
        if (!forceAttachment) {
            if (mComposerBase->editor()->textMode()
                == MessageComposer::RichTextComposerNg::Rich /*&& mComposerBase->editor()->isEnableImageActions() Necessary ?*/) {
                auto image = qvariant_cast<QImage>(source->imageData());
                QFileInfo fi(source->text());

                QMenu menu(this);
                const QAction *addAsInlineImageAction = menu.addAction(i18n("Add as &Inline Image"));
                menu.addAction(i18n("Add as &Attachment"));
                const QAction *selectedAction = menu.exec(QCursor::pos());
                if (selectedAction == addAsInlineImageAction) {
                    // Let the textedit from kdepimlibs handle inline images
                    mComposerBase->editor()->composerControler()->composerImages()->insertImage(image, fi);
                    return true;
                } else if (!selectedAction) {
                    return true;
                }
                // else fall through
            }
        }
        // Ok, when we reached this point, the user wants to add the image as an attachment.
        // Ask for the filename first.
        bool ok;
        QString attName = QInputDialog::getText(this, i18n("KMail"), i18n("Name of the attachment:"), QLineEdit::Normal, QString(), &ok);
        if (!ok) {
            return true;
        }
        attName = attName.trimmed();
        if (attName.isEmpty()) {
            KMessageBox::error(this, i18n("Attachment name can't be empty"), i18nc("@title:window", "Invalid Attachment Name"));

            return true;
        }
        addAttachment(attName, KMime::Headers::CEbase64, imageData, "image/png");
        return true;
    }

    // If this is a URL list, add those files as attachments or text
    // but do not offer this if we are pasting plain text containing an url, e.g. from a browser
    const QList<QUrl> urlList = source->urls();
    if (!urlList.isEmpty()) {
        // Search if it's message items.
        bool allLocalURLs = true;

        for (const QUrl &url : urlList) {
            if (!url.isLocalFile()) {
                allLocalURLs = false;
            }
        }

        if (allLocalURLs || forceAttachment) {
            QList<AttachmentInfo> infoList;
            infoList.reserve(urlList.count());
            for (const QUrl &url : urlList) {
                AttachmentInfo info;
                info.url = url;
                infoList.append(info);
            }
            addAttachment(infoList, false);
        } else {
            QMenu p;
            const int sizeUrl(urlList.size());
            const QAction *addAsTextAction = p.addAction(i18np("Add URL into Message", "Add URLs into Message", sizeUrl));
            const QAction *addAsAttachmentAction = p.addAction(i18np("Add File as &Attachment", "Add Files as &Attachment", sizeUrl));
            const QAction *selectedAction = p.exec(QCursor::pos());

            if (selectedAction == addAsTextAction) {
                insertUrls(source, urlList);
            } else if (selectedAction == addAsAttachmentAction) {
                QList<AttachmentInfo> infoList;
                for (const QUrl &url : urlList) {
                    if (url.isValid()) {
                        AttachmentInfo info;
                        info.url = url;
                        infoList.append(info);
                    }
                }
                addAttachment(infoList, false);
            }
        }
        return true;
    }
    return false;
}

void ComposerWindow::slotSaveDraft()
{
    mComposerBase->slotSaveDraft();
}

void ComposerWindow::slotSaveAsFile()
{
    auto job = new SaveAsFileJob(this);
    job->setParentWidget(this);
    job->setHtmlMode(mComposerBase->editor()->textMode() == MessageComposer::RichTextComposerNg::Rich);
    job->setTextDocument(mComposerBase->editor()->document());
    job->start();
}

QUrl ComposerWindow::insertFile()
{
    const auto fileName = QFileDialog::getOpenFileName(this, i18nc("@title:window", "Insert File"));
    return QUrl::fromUserInput(fileName);
}

void ComposerWindow::slotInsertFile()
{
    const QUrl u = insertFile();
    if (u.isEmpty()) {
        return;
    }

    mRecentAction->addUrl(u);
    // Prevent race condition updating list when multiple composers are open
    {
        QUrlQuery query(u);
        QStringList urls = MessageComposer::MessageComposerSettings::self()->recentUrls();
        // Prevent config file from growing without bound
        // Would be nicer to get this constant from KRecentFilesAction
        const int mMaxRecentFiles = 30;
        while (urls.count() > mMaxRecentFiles) {
            urls.removeLast();
        }
        urls.prepend(u.toDisplayString());
        MessageComposer::MessageComposerSettings::self()->setRecentUrls(urls);
        MessageComposer::MessageComposerSettings::self()->save();
    }
    slotInsertRecentFile(u);
}

void ComposerWindow::slotRecentListFileClear()
{
    MessageComposer::MessageComposerSettings::self()->setRecentUrls({});
    MessageComposer::MessageComposerSettings::self()->save();
}

void ComposerWindow::slotInsertRecentFile(const QUrl &u)
{
    if (u.fileName().isEmpty()) {
        return;
    }

    auto job = new MessageComposer::InsertTextFileJob(mComposerBase->editor(), u);
    job->start();
}

void ComposerWindow::slotPrint()
{
    QPrinter printer;
    QPrintDialog dialog(&printer, this);
    dialog.setWindowTitle(i18nc("@title:window", "Print Document"));
    if (dialog.exec() != QDialog::Accepted)
        return;

    printInternal(&printer);
}

void ComposerWindow::slotPrintPreview()
{
    auto dialog = new QPrintPreviewDialog(this);
    dialog->setAttribute(Qt::WA_DeleteOnClose);
    dialog->resize(800, 750);
    dialog->setWindowTitle(i18nc("@title:window", "Print Document"));
    QObject::connect(dialog, &QPrintPreviewDialog::paintRequested, this, [this](QPrinter *printer) {
        printInternal(printer);
    });
    dialog->open();
}

void ComposerWindow::printInternal(QPrinter *printer)
{
    mComposerBase->setFrom(mFrom);
    mComposerBase->setSubject(mEdtSubject->text());
    mComposerBase->generateMessage([printer](const QList<KMime::Message::Ptr> &messages) {
        if (messages.isEmpty()) {
            return;
        }

        MimeTreeParser::Widgets::MessageViewer messageViewer;
        messageViewer.setMessage(messages[0]);
        QPainter painter;
        painter.begin(printer);
        const auto pageLayout = printer->pageLayout();
        const auto pageRect = pageLayout.paintRectPixels(printer->resolution());
        const double xscale = pageRect.width() / double(messageViewer.width());
        const double yscale = pageRect.height() / double(messageViewer.height());
        const double scale = qMin(qMin(xscale, yscale), 1.);
        painter.translate(pageRect.x(), pageRect.y());
        painter.scale(scale, scale);
        messageViewer.print(&painter, pageRect.width());
    });
}

void ComposerWindow::slotPasteAsAttachment()
{
    const QMimeData *mimeData = QApplication::clipboard()->mimeData();
    if (!mimeData) {
        return;
    }
    if (insertFromMimeData(mimeData, true)) {
        return;
    }
    if (mimeData->hasText()) {
        bool ok;
        const QString attName =
            QInputDialog::getText(this, i18n("Insert clipboard text as attachment"), i18n("Name of the attachment:"), QLineEdit::Normal, QString(), &ok);
        if (ok) {
            mComposerBase->addAttachment(attName, attName, QApplication::clipboard()->text().toUtf8(), "text/plain");
        }
        return;
    }
}

void ComposerWindow::slotWordWrapToggled(bool on)
{
    if (on) {
        mComposerBase->editor()->enableWordWrap(validateLineWrapWidth());
    } else {
        disableWordWrap();
    }
}

int ComposerWindow::validateLineWrapWidth() const
{
    int lineWrap = MessageComposer::MessageComposerSettings::self()->lineWrapWidth();
    if ((lineWrap == 0) || (lineWrap > 78)) {
        lineWrap = 78;
    } else if (lineWrap < 30) {
        lineWrap = 30;
    }
    return lineWrap;
}

void ComposerWindow::disableWordWrap()
{
    mComposerBase->editor()->disableWordWrap();
}

void ComposerWindow::slotAutoSpellCheckingToggled(bool enabled)
{
    mAutoSpellCheckingAction->setChecked(enabled);
    if (mComposerBase->editor()->checkSpellingEnabled() != enabled) {
        mComposerBase->editor()->setCheckSpellingEnabled(enabled);
    }
    // mStatusBarLabelSpellCheckingChangeMode->setToggleMode(enabled);
}

void ComposerWindow::slotSpellcheckConfig()
{
    QPointer<SpellCheckerConfigDialog> dialog = new SpellCheckerConfigDialog(this);
    if (!mComposerBase->editor()->spellCheckingLanguage().isEmpty()) {
        dialog->setLanguage(mComposerBase->editor()->spellCheckingLanguage());
    }

    if (dialog->exec()) {
        mComposerBase->editor()->setSpellCheckingLanguage(dialog->language());
    }
    delete dialog;
}

void ComposerWindow::closeEvent(QCloseEvent *event)
{
    event->ignore();
    ComposerWindowFactory::self().clear(this);
}

bool ComposerWindow::queryClose()
{
    if (isModified()) {
        const QString savebut = i18n("&Save as Draft");
        const QString savetext = i18n("Save this message encrypted in your drafts folder. It can then be edited and sent at a later time.");

        const int rc = KMessageBox::warningTwoActionsCancel(this,
                                                            i18n("Do you want to save the message for later or discard it?"),
                                                            i18nc("@title:window", "Close Composer"),
                                                            KGuiItem(savebut, QStringLiteral("document-save"), QString(), savetext),
                                                            KStandardGuiItem::discard(),
                                                            KStandardGuiItem::cancel());
        if (rc == KMessageBox::Cancel) {
            return false;
        } else if (rc == KMessageBox::ButtonCode::PrimaryAction) {
            // doSend will close the window. Just return false from this method
            slotSaveDraft();
            return false;
        }
        // else fall through: return true
    }

    mComposerBase->cleanupAutoSave();
    return true;
}

void ComposerWindow::slotRecipientEditorLineAdded(RecipientLineNG *line)
{
    connect(line, &RecipientLineNG::countChanged, this, [this, line]() {
        slotRecipientAdded(line);
    });
    connect(line, &RecipientLineNG::iconClicked, this, [this, line]() {
        slotRecipientLineIconClicked(line);
    });
    connect(line, &RecipientLineNG::destroyed, this, &ComposerWindow::slotRecipientEditorFocusChanged, Qt::QueuedConnection);
    connect(
        line,
        &RecipientLineNG::activeChanged,
        this,
        [this, line]() {
            slotRecipientFocusLost(line);
        },
        Qt::QueuedConnection);

    slotRecipientEditorFocusChanged();
}

void ComposerWindow::slotRecipientLineIconClicked(RecipientLineNG *line)
{
    const auto recipient = line->data().dynamicCast<Recipient>();

    if (!recipient->key().isNull()) {
        const QString exec = QStandardPaths::findExecutable(QStringLiteral("kleopatra"));
        if (exec.isEmpty()
            || !QProcess::startDetached(exec,
                                        {QStringLiteral("--query"),
                                         QString::fromLatin1(recipient->key().primaryFingerprint()),
                                         QStringLiteral("--parent-windowid"),
                                         QString::number(winId())})) {
            qCWarning(EDITOR_LOG) << "Unable to execute kleopatra";
        }
        return;
    }

    const auto msg = i18nc(
        "if in your language something like "
        "'certificate(s)' is not possible please "
        "use the plural in the translation",
        "<qt>No valid and trusted encryption certificate was "
        "found for \"%1\".<br/><br/>"
        "Select the certificate(s) which should "
        "be used for this recipient. If there is no suitable certificate in the list "
        "you can also search for external certificates by clicking the button: "
        "search for external certificates.</qt>",
        recipient->name().isEmpty() ? recipient->email() : recipient->name());

    const bool opgp = containsOpenPGP(cryptoMessageFormat());
    const bool x509 = containsSMIME(cryptoMessageFormat());

    QPointer<Kleo::KeySelectionDialog> dlg = new Kleo::KeySelectionDialog(
        i18n("Encryption Key Selection"),
        msg,
        recipient->email(),
        {},
        Kleo::KeySelectionDialog::ValidEncryptionKeys | (opgp ? Kleo::KeySelectionDialog::OpenPGPKeys : 0) | (x509 ? Kleo::KeySelectionDialog::SMIMEKeys : 0),
        false, // multi-selection
        false); // "remember choice" box;

    dlg->open();

    connect(dlg, &QDialog::accepted, this, [dlg, recipient, line, this]() {
        auto key = dlg->selectedKey();
        key.update(); // We need tofu information for key.
        recipient->setKey(key);
        annotateRecipientEditorLineWithCryptoInfo(line);
    });
}

void ComposerWindow::slotRecipientEditorFocusChanged()
{
    if (!mEncryptAction->isChecked()) {
        return;
    }

    if (mKeyCache->initialized()) {
        mRunKeyResolverTimer->stop();
        runKeyResolver();
    }
}

void ComposerWindow::slotRecipientAdded(RecipientLineNG *line)
{
    if (line->recipientsCount() == 0) {
        return;
    }

    if (!mKeyCache->initialized()) {
        if (line->property("keyLookupJob").toBool()) {
            return;
        }

        line->setProperty("keyLookupJob", true);
        // We need to start key listing on our own othweise KMail will crash and we want to wait till the cache is populated.
        connect(mKeyCache.get(), &Kleo::KeyCache::keyListingDone, this, [this, line]() {
            slotRecipientAdded(line);
        });
        return;
    }

    if (mKeyCache->initialized()) {
        mRunKeyResolverTimer->start();
    }
}

void ComposerWindow::slotRecipientFocusLost(RecipientLineNG *line)
{
    if (line->recipientsCount() == 0) {
        return;
    }

    if (mKeyCache->initialized()) {
        mRunKeyResolverTimer->start();
    }
}

void ComposerWindow::slotCloseWindow()
{
    setModified(false);
    mComposerBase->cleanupAutoSave();
    close();
}
