// SPDX-FileCopyrightText: 2025 g10 code GmbH
// SPDX-License-Identifier: LGPL-2.1-or-later

#include "bodytexteditor.h"
#include "composersignatures.h"
#include "part/textpart.h"

#include <KConfigGroup>
#include <KLocalizedString>
#include <KMessageBox>
#include <KSharedConfig>
#include <KStandardActions>
#include <Sonnet/BackgroundChecker>
#include <Sonnet/Dialog>
#include <Sonnet/Highlighter>
#include <Sonnet/Speller>

#include <QActionGroup>
#include <QMenu>
#include <QRegularExpression>
#include <QTextBlock>
#include <QTextDocumentFragment>

using namespace MessageComposer;
using namespace Qt::StringLiterals;

BodyTextEditor::BodyTextEditor(QWidget *parent)
    : QTextEdit(parent)
    , m_composerSignatures(new ComposerSignatures(this, this))
    , m_speller(std::make_unique<Sonnet::Speller>())
{
    setAcceptRichText(false);
    enableWordWrap(100);

    m_highlighter = new Sonnet::Highlighter(this);
    m_highlighter->setCurrentLanguage(spellCheckingLanguage());

    KSharedConfig::Ptr config = KSharedConfig::openConfig();
    if (config->hasGroup("Spelling"_L1)) {
        KConfigGroup group(config, "Spelling"_L1);
        m_checkSpellingEnabled = group.readEntry("checkerEnabledByDefault", false);
        setSpellCheckingLanguage(group.readEntry("Language", QString()));
    }
}

BodyTextEditor::~BodyTextEditor() = default;

MessageComposer::ComposerSignatures *BodyTextEditor::composerSignatures() const
{
    return m_composerSignatures;
}

int BodyTextEditor::quoteLength(const QString &line, bool oneQuote) const
{
    const auto quotePrefix = quotePrefixName();

    if (!quotePrefix.simplified().isEmpty()) {
        if (line.startsWith(quotePrefix)) {
            return quotePrefix.length();
        }
        return 0;
    }
    bool quoteFound = false;
    int startOfText = -1;
    const int lineLength(line.length());
    for (int i = 0; i < lineLength; ++i) {
        if (line[i] == QLatin1Char('>') || line[i] == QLatin1Char('|')) {
            if (quoteFound && oneQuote) {
                break;
            }
            quoteFound = true;
        } else if (line[i] != QLatin1Char(' ')) {
            startOfText = i;
            break;
        }
    }
    if (quoteFound) {
        // We found a quote but it's just quote element => 1 => remove 1 char.
        if (startOfText == -1) {
            startOfText = 1;
        }
        return startOfText;
    }
    return 0;
}

bool BodyTextEditor::isLineQuoted(const QString &line) const
{
    return quoteLength(line) > 0;
}

static bool isCursorAtEndOfLine(const QTextCursor &cursor)
{
    QTextCursor testCursor = cursor;
    testCursor.movePosition(QTextCursor::EndOfLine, QTextCursor::KeepAnchor);
    return !testCursor.hasSelection();
}

static void insertSignatureHelper(const QString &signature, BodyTextEditor *textEdit, KIdentityManagementCore::Signature::Placement placement, bool addNewlines)
{
    if (signature.isEmpty()) {
        return;
    }
    // Save the modified state of the document, as inserting a signature
    // shouldn't change this. Restore it at the end of this function.
    bool isModified = textEdit->document()->isModified();

    // Move to the desired position, where the signature should be inserted
    QTextCursor cursor = textEdit->textCursor();
    QTextCursor oldCursor = cursor;
    cursor.beginEditBlock();

    if (placement == KIdentityManagementCore::Signature::End) {
        cursor.movePosition(QTextCursor::End);
    } else if (placement == KIdentityManagementCore::Signature::Start) {
        cursor.movePosition(QTextCursor::Start);
    } else if (placement == KIdentityManagementCore::Signature::AtCursor) {
        cursor.movePosition(QTextCursor::StartOfLine);
    }
    textEdit->setTextCursor(cursor);

    QString lineSep;
    if (addNewlines) {
        lineSep = QLatin1Char('\n');
    }

    // Insert the signature and newlines depending on where it was inserted.
    int newCursorPos = -1;
    QString headSep;
    QString tailSep;

    if (placement == KIdentityManagementCore::Signature::End) {
        // There is one special case when re-setting the old cursor: The cursor
        // was at the end. In this case, QTextEdit has no way to know
        // if the signature was added before or after the cursor, and just
        // decides that it was added before (and the cursor moves to the end,
        // but it should not when appending a signature). See bug 167961
        if (oldCursor.position() == textEdit->toPlainText().length()) {
            newCursorPos = oldCursor.position();
        }
        headSep = lineSep;
    } else if (placement == KIdentityManagementCore::Signature::Start) {
        // When prepending signatures, add a couple of new lines before
        // the signature, and move the cursor to the beginning of the QTextEdit.
        // People tends to insert new text there.
        newCursorPos = 0;
        headSep = lineSep + lineSep;
        if (!isCursorAtEndOfLine(cursor)) {
            tailSep = lineSep;
        }
    } else if (placement == KIdentityManagementCore::Signature::AtCursor) {
        if (!isCursorAtEndOfLine(cursor)) {
            tailSep = lineSep;
        }
    }

    const QString full_signature = headSep + signature + tailSep;
    textEdit->insertPlainText(full_signature);

    cursor.endEditBlock();
    if (newCursorPos != -1) {
        oldCursor.setPosition(newCursorPos);
    }

    textEdit->setTextCursor(oldCursor);
    textEdit->ensureCursorVisible();

    textEdit->document()->setModified(isModified);
}

void BodyTextEditor::insertSignature(const KIdentityManagementCore::Signature &signature,
                                     KIdentityManagementCore::Signature::Placement placement,
                                     KIdentityManagementCore::Signature::AddedText addedText)
{
    if (!signature.isEnabledSignature()) {
        return;
    }
    QString signatureStr;
    bool ok = false;
    QString errorMessage;
    if (addedText & KIdentityManagementCore::Signature::AddSeparator) {
        signatureStr = signature.withSeparator(&ok, &errorMessage);
    } else {
        signatureStr = signature.rawText(&ok, &errorMessage);
    }

    if (!ok && !errorMessage.isEmpty()) {
        KMessageBox::error(nullptr, errorMessage);
    }

    insertSignatureHelper(signatureStr, this, placement, (addedText & KIdentityManagementCore::Signature::AddNewLines));
}

void BodyTextEditor::enableWordWrap(int wrapColumn)
{
    setWordWrapMode(QTextOption::WordWrap);
    setLineWrapMode(QTextEdit::FixedColumnWidth);
    setLineWrapColumnOrWidth(wrapColumn);
}

void BodyTextEditor::disableWordWrap()
{
    setLineWrapMode(QTextEdit::WidgetWidth);
}

QString BodyTextEditor::spellCheckingLanguage() const
{
    return m_spellCheckingLanguage;
}

Sonnet::Highlighter *BodyTextEditor::highlighter() const
{
    return m_highlighter;
}

void BodyTextEditor::setSpellCheckingLanguage(const QString &lang)
{
    if (highlighter()) {
        highlighter()->setCurrentLanguage(lang);
        highlighter()->rehighlight();
    }

    if (lang == m_spellCheckingLanguage) {
        return;
    }
    m_spellCheckingLanguage = lang;
    KSharedConfig::Ptr config = KSharedConfig::openConfig();
    KConfigGroup group(config, "Spelling"_L1);
    group.writeEntry("Language", m_spellCheckingLanguage);
    setCheckSpellingEnabled(checkSpellingEnabled());

    Q_EMIT languageChanged(lang);
}

int BodyTextEditor::linePosition() const
{
    const QTextCursor cursor = textCursor();
    const QTextDocument *doc = document();
    QTextBlock block = doc->begin();
    int lineCount = 0;

    // Simply using cursor.block.blockNumber() would not work since that does not
    // take word-wrapping into account, i.e. it is possible to have more than one
    // line in a block.
    //
    // What we have to do therefore is to iterate over the blocks and count the
    // lines in them. Once we have reached the block where the cursor is, we have
    // to iterate over each line in it, to find the exact line in the block where
    // the cursor is.
    while (block.isValid()) {
        const QTextLayout *layout = block.layout();

        // If the current block has the cursor in it, iterate over all its lines
        if (block == cursor.block()) {
            // Special case: Cursor at end of single non-wrapped line, exit early
            // in this case as the logic below can't handle it
            if (block.lineCount() == layout->lineCount()) {
                return lineCount;
            }

            const int cursorBasePosition = cursor.position() - block.position();
            const int numberOfLine(layout->lineCount());
            for (int i = 0; i < numberOfLine; ++i) {
                QTextLine line = layout->lineAt(i);
                if (cursorBasePosition >= line.textStart() && cursorBasePosition < line.textStart() + line.textLength()) {
                    break;
                }
                lineCount++;
            }
            return lineCount;
        } else {
            // No, cursor is not in the current block
            lineCount += layout->lineCount();
        }

        block = block.next();
    }

    // Only gets here if the cursor block can't be found, shouldn't happen except
    // for an empty document maybe
    return lineCount;
}

int BodyTextEditor::columnNumber() const
{
    const QTextCursor cursor = textCursor();
    return cursor.columnNumber();
}

QString BodyTextEditor::quotePrefixName() const
{
    return QStringLiteral(">");
}

void static fixupTextEditString(QString &text)
{
    // Remove line separators. Normal \n chars are still there, so no linebreaks get lost here
    text.remove(QChar::LineSeparator);

    // Get rid of embedded images, see QTextImageFormat documentation:
    // "Inline images are represented by an object replacement character (0xFFFC in Unicode) "
    text.remove(QChar(0xFFFC));

    // In plaintext mode, each space is non-breaking.
    text.replace(QChar::Nbsp, QChar::fromLatin1(' '));
}

void BodyTextEditor::fillComposerTextPart(MessageComposer::TextPart *textPart)
{
    auto plain = toPlainText();
    fixupTextEditString(plain);
    textPart->setCleanPlainText(plain);
    textPart->setWrappedPlainText(toWrappedPlainText());
    textPart->setWordWrappingEnabled(lineWrapMode() == QTextEdit::FixedColumnWidth);
}

QString BodyTextEditor::toWrappedPlainText() const
{
    QString temp;
    static const QRegularExpression rx(QStringLiteral("(http|ftp|ldap)s?\\S+-$"));
    QTextBlock block = document()->begin();
    while (block.isValid()) {
        QTextLayout *layout = block.layout();
        const int numberOfLine(layout->lineCount());
        bool urlStart = false;
        for (int i = 0; i < numberOfLine; ++i) {
            const QTextLine line = layout->lineAt(i);
            const QString lineText = block.text().mid(line.textStart(), line.textLength());

            if (lineText.contains(rx) || (urlStart && !lineText.contains(QLatin1Char(' ')) && lineText.endsWith(QLatin1Char('-')))) {
                // don't insert line break in URL
                temp += lineText;
                urlStart = true;
            } else {
                temp += lineText + QLatin1Char('\n');
            }
        }
        block = block.next();
    }

    // Remove the last superfluous newline added above
    if (temp.endsWith(QLatin1Char('\n'))) {
        temp.chop(1);
    }
    fixupTextEditString(temp);
    return temp;
}

bool BodyTextEditor::checkSpellingEnabled() const
{
    return m_checkSpellingEnabled;
}

void BodyTextEditor::setCheckSpellingEnabled(bool check)
{
    m_checkSpellingEnabled = check;
}

void BodyTextEditor::contextMenuEvent(QContextMenuEvent *event)
{
    auto popup = std::unique_ptr<QMenu>(createStandardContextMenu());
    if (!popup) {
        return;
    }

    const bool emptyDocument = document()->isEmpty();
    const QList<QAction *> actionList = popup->actions();
    enum {
        UndoAct,
        RedoAct,
        CutAct,
        CopyAct,
        PasteAct,
        ClearAct,
        SelectAllAct,
        NCountActs
    };
    QAction *separatorAction = nullptr;
    const int idx = actionList.indexOf(actionList[SelectAllAct]) + 1;
    if (idx < actionList.count()) {
        separatorAction = actionList.at(idx);
    }
    if (separatorAction) {
        if (!emptyDocument) {
            auto clearAllAction = KStandardActions::clear(this, &BodyTextEditor::slotUndoableClear, popup.get());
            popup->insertAction(separatorAction, clearAllAction);
        }
    }

    if (!m_speller->availableBackends().isEmpty()) {
        if (!emptyDocument) {
            popup->addAction(QIcon::fromTheme(QStringLiteral("tools-check-spelling")),
                             i18nc("@action:inmenu", "Check Spelling…"),
                             this,
                             &BodyTextEditor::slotCheckSpelling);
            popup->addSeparator();
        }
        QAction *autoSpellCheckAction = popup->addAction(i18nc("@action:inmenu", "Auto Spell Check"), this, &BodyTextEditor::slotToggleAutoSpellCheck);
        autoSpellCheckAction->setCheckable(true);
        autoSpellCheckAction->setChecked(checkSpellingEnabled());
        popup->addAction(autoSpellCheckAction);

        if (checkSpellingEnabled() && m_activateLanguageMenu) {
            auto languagesMenu = new QMenu(i18nc("@title:menu", "Spell Checking Language"), popup.get());
            auto languagesGroup = new QActionGroup(languagesMenu);
            languagesGroup->setExclusive(true);

            QString defaultSpellcheckingLanguage = spellCheckingLanguage();
            if (defaultSpellcheckingLanguage.isEmpty()) {
                // TODO fix default value
                defaultSpellcheckingLanguage = m_speller->defaultLanguage();
            }

            QMapIterator<QString, QString> i(m_speller->availableDictionaries());
            while (i.hasNext()) {
                i.next();
                QAction *languageAction = languagesMenu->addAction(i.key());
                languageAction->setCheckable(true);
                languageAction->setChecked(defaultSpellcheckingLanguage == i.value());
                languageAction->setData(i.value());
                languageAction->setActionGroup(languagesGroup);
                connect(languageAction, &QAction::triggered, this, &BodyTextEditor::slotLanguageSelected);
            }
            popup->addMenu(languagesMenu);
        }
        popup->addSeparator();
    }
    popup->exec(event->globalPos());
}

void BodyTextEditor::slotLanguageSelected()
{
    auto languageAction = static_cast<QAction *>(QObject::sender());
    setSpellCheckingLanguage(languageAction->data().toString());
}

void BodyTextEditor::slotUndoableClear()
{
    QTextCursor cursor = textCursor();
    cursor.beginEditBlock();
    cursor.movePosition(QTextCursor::Start);
    cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor);
    cursor.removeSelectedText();
    cursor.endEditBlock();
}

void BodyTextEditor::slotCheckSpelling()
{
    if (document()->isEmpty()) {
        // slotDisplayMessageIndicator(i18n("Nothing to spell check."));
        return;
    }
    auto backgroundSpellCheck = new Sonnet::BackgroundChecker;
    if (backgroundSpellCheck->speller().availableBackends().isEmpty()) {
        // slotDisplayMessageIndicator(i18n("No backend available for spell checking."));
        delete backgroundSpellCheck;
        return;
    }
    if (!m_spellCheckingLanguage.isEmpty()) {
        backgroundSpellCheck->changeLanguage(m_spellCheckingLanguage);
    }
    if (!m_ignoreSpellCheckingWords.isEmpty()) {
        for (const QString &word : std::as_const(m_ignoreSpellCheckingWords)) {
            backgroundSpellCheck->speller().addToSession(word);
        }
    }
    auto spellDialog = new Sonnet::Dialog(backgroundSpellCheck, nullptr);
    backgroundSpellCheck->setParent(spellDialog);
    spellDialog->setAttribute(Qt::WA_DeleteOnClose, true);
    connect(spellDialog, &Sonnet::Dialog::replace, this, &BodyTextEditor::slotSpellCheckerCorrected);
    connect(spellDialog, &Sonnet::Dialog::misspelling, this, &BodyTextEditor::slotSpellCheckerMisspelling);
    connect(spellDialog, &Sonnet::Dialog::autoCorrect, this, &BodyTextEditor::slotSpellCheckerAutoCorrect);
    connect(spellDialog, &Sonnet::Dialog::spellCheckDone, this, &BodyTextEditor::slotSpellCheckerFinished);
    connect(spellDialog, &Sonnet::Dialog::cancel, this, &BodyTextEditor::slotSpellCheckerCanceled);
    connect(spellDialog, &Sonnet::Dialog::spellCheckStatus, this, &BodyTextEditor::spellCheckStatus);
    connect(spellDialog, &Sonnet::Dialog::languageChanged, this, &BodyTextEditor::languageChanged);
    m_originalDoc = QTextDocumentFragment(document());
    spellDialog->setBuffer(toPlainText());
    spellDialog->show();
}

void BodyTextEditor::slotSpellCheckerCanceled()
{
    QTextDocument *doc = document();
    doc->clear();
    QTextCursor cursor(doc);
    cursor.insertFragment(m_originalDoc);
    slotSpellCheckerFinished();
}

void BodyTextEditor::slotSpellCheckerAutoCorrect(const QString &currentWord, const QString &autoCorrectWord)
{
    Q_EMIT spellCheckerAutoCorrect(currentWord, autoCorrectWord);
}

void BodyTextEditor::slotSpellCheckerMisspelling(const QString &text, int pos)
{
    highlightWord(text.length(), pos);
}

void BodyTextEditor::slotSpellCheckerCorrected(const QString &oldWord, int pos, const QString &newWord)
{
    if (oldWord != newWord) {
        QTextCursor cursor(document());
        cursor.setPosition(pos);
        cursor.setPosition(pos + oldWord.length(), QTextCursor::KeepAnchor);
        cursor.insertText(newWord);
    }
}

void BodyTextEditor::slotSpellCheckerFinished()
{
    QTextCursor cursor(document());
    cursor.clearSelection();
    setTextCursor(cursor);
}

void BodyTextEditor::highlightWord(int length, int pos)
{
    QTextCursor cursor(document());
    cursor.setPosition(pos);
    cursor.setPosition(pos + length, QTextCursor::KeepAnchor);
    setTextCursor(cursor);
    ensureCursorVisible();
}

void BodyTextEditor::slotToggleAutoSpellCheck()
{
    setCheckSpellingEnabled(!checkSpellingEnabled());
    KSharedConfig::Ptr config = KSharedConfig::openConfig();
    KConfigGroup group(config, "Spelling"_L1);
    group.writeEntry("checkerEnabledByDefault", m_checkSpellingEnabled);
}
