/*
    This file is part of the KDE libraries
    SPDX-FileCopyrightText: 2020 Ahmad Samir <a.samirh78@gmail.com>

    SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/

#include "kmessagedialog.h"
#include "kmessagebox_p.h"

#include "loggingcategory.h"

#include <QApplication>
#include <QCheckBox>
#include <QDebug>
#include <QDialogButtonBox>
#include <QHBoxLayout>
#include <QLabel>
#include <QListWidget>
#include <QPushButton>
#include <QScreen>
#include <QScrollArea>
#include <QScrollBar>
#include <QShowEvent>
#include <QStyle>
#include <QStyleOption>
#include <QTextBrowser>
#include <QVBoxLayout>
#include <QWindow>

#include <KCollapsibleGroupBox>
#include <KSqueezedTextLabel>

static const Qt::TextInteractionFlags s_textFlags = Qt::TextSelectableByMouse | Qt::LinksAccessibleByMouse | Qt::LinksAccessibleByKeyboard;

// TODO KF6 remove QObject inheritance again
class KMessageDialogPrivate : public QObject
{
    Q_OBJECT

public:
    explicit KMessageDialogPrivate(KMessageDialog::Type type, KMessageDialog *qq)
        : m_type(type)
        , q(qq)
    {
    }

    KMessageDialog::Type m_type;
    KMessageDialog *const q;

    QVBoxLayout *m_topLayout = nullptr;
    QWidget *m_mainWidget = nullptr;
    QLabel *m_iconLabel = nullptr;
    QLabel *m_messageLabel = nullptr;
    QListWidget *m_listWidget = nullptr;
    QLabel *m_detailsLabel = nullptr;
    QTextBrowser *m_detailsTextEdit = nullptr;
    KCollapsibleGroupBox *m_detailsGroup = nullptr;
    QCheckBox *m_dontAskAgainCB = nullptr;
    QDialogButtonBox *m_buttonBox = nullptr;
    QMetaObject::Connection m_buttonBoxConnection;
    bool m_notifyEnabled = true;
};

KMessageDialog::KMessageDialog(KMessageDialog::Type type, const QString &text, QWidget *parent)
    : QDialog(parent)
    , d(new KMessageDialogPrivate(type, this))
{
    // Dialog top-level layout
    d->m_topLayout = new QVBoxLayout(this);
    d->m_topLayout->setSizeConstraint(QLayout::SetFixedSize);

    // Main widget
    d->m_mainWidget = new QWidget(this);
    d->m_topLayout->addWidget(d->m_mainWidget);

    // Layout for the main widget
    auto *mainLayout = new QVBoxLayout(d->m_mainWidget);
    QStyle *widgetStyle = d->m_mainWidget->style();
    // Provide extra spacing
    mainLayout->setSpacing(widgetStyle->pixelMetric(QStyle::PM_LayoutVerticalSpacing) * 2);
    mainLayout->setContentsMargins(0, 0, 0, 0);

    auto *hLayout = new QHBoxLayout{};
    mainLayout->addLayout(hLayout, 5);

    // Icon
    auto *iconLayout = new QVBoxLayout{};
    hLayout->addLayout(iconLayout, 0);

    d->m_iconLabel = new QLabel(d->m_mainWidget);
    d->m_iconLabel->setVisible(false);
    iconLayout->addWidget(d->m_iconLabel);
    hLayout->addSpacing(widgetStyle->pixelMetric(QStyle::PM_LayoutHorizontalSpacing));

    const QRect desktop = screen()->geometry();
    const auto desktopWidth = desktop.width();
    // Main message text
    d->m_messageLabel = new QLabel(text, d->m_mainWidget);
    if (d->m_messageLabel->sizeHint().width() > (desktopWidth * 0.5)) {
        // Enable automatic wrapping of messages which are longer than 50% of screen width
        d->m_messageLabel->setWordWrap(true);
        // Use a squeezed label if text is still too wide
        const bool usingSqueezedLabel = d->m_messageLabel->sizeHint().width() > (desktopWidth * 0.85);
        if (usingSqueezedLabel) {
            delete d->m_messageLabel;
            d->m_messageLabel = new KSqueezedTextLabel(text, d->m_mainWidget);
        }
    }

    d->m_messageLabel->setTextInteractionFlags(s_textFlags);

    const bool usingScrollArea = (desktop.height() / 3) < d->m_messageLabel->sizeHint().height();
    if (usingScrollArea) {
        QScrollArea *messageScrollArea = new QScrollArea(d->m_mainWidget);
        messageScrollArea->setWidget(d->m_messageLabel);
        messageScrollArea->setFrameShape(QFrame::NoFrame);
        messageScrollArea->setWidgetResizable(true);
        hLayout->addWidget(messageScrollArea, 5);
    } else {
        hLayout->addWidget(d->m_messageLabel, 5);
    }

    // List widget, will be populated by setListWidgetItems()
    d->m_listWidget = new QListWidget(d->m_mainWidget);
    mainLayout->addWidget(d->m_listWidget, usingScrollArea ? 10 : 50);
    d->m_listWidget->setVisible(false);

    // DontAskAgain checkbox, will be set up by setDontAskAgainText()
    d->m_dontAskAgainCB = new QCheckBox(d->m_mainWidget);
    mainLayout->addWidget(d->m_dontAskAgainCB);
    d->m_dontAskAgainCB->setVisible(false);

    // Details widget, text will be added by setDetails()
    auto *detailsHLayout = new QHBoxLayout{};
    d->m_topLayout->addLayout(detailsHLayout);

    d->m_detailsGroup = new KCollapsibleGroupBox();
    d->m_detailsGroup->setVisible(false);
    d->m_detailsGroup->setTitle(QApplication::translate("KMessageDialog", "Details"));
    QVBoxLayout *detailsLayout = new QVBoxLayout(d->m_detailsGroup);

    d->m_detailsLabel = new QLabel();
    d->m_detailsLabel->setTextInteractionFlags(s_textFlags);
    d->m_detailsLabel->setWordWrap(true);
    detailsLayout->addWidget(d->m_detailsLabel);

    d->m_detailsTextEdit = new QTextBrowser{};
    d->m_detailsTextEdit->setMinimumHeight(d->m_detailsTextEdit->fontMetrics().lineSpacing() * 11);
    detailsLayout->addWidget(d->m_detailsTextEdit, 50);

    detailsHLayout->addWidget(d->m_detailsGroup);

    // Button box
    d->m_buttonBox = new QDialogButtonBox(this);
    d->m_topLayout->addWidget(d->m_buttonBox);

    // Default buttons
    if ((d->m_type == KMessageDialog::Information) || (d->m_type != KMessageDialog::Error)) {
        // set Ok button
        setButtons();
    } else if (d->m_type == KMessageDialog::WarningContinueCancel) {
        // set Continue & Cancel buttons
        setButtons(KStandardGuiItem::cont(), KGuiItem(), KStandardGuiItem::cancel());
    }

    setNotifyEnabled(true);

    // If the dialog is rejected, e.g. by pressing Esc, done() signal connected to the button box
    // won't be emitted
    connect(this, &QDialog::rejected, this, [this]() {
        done(KMessageDialog::Cancel);
    });
}

// This method has been copied from KWindowSystem to avoid depending on it
static void setMainWindow(QDialog *dialog, WId mainWindowId)
{
#ifdef Q_OS_OSX
    if (!QWidget::find(mainWindowId)) {
        return;
    }
#endif
    // Set the WA_NativeWindow attribute to force the creation of the QWindow.
    // Without this QWidget::windowHandle() returns 0.
    dialog->setAttribute(Qt::WA_NativeWindow, true);
    QWindow *subWindow = dialog->windowHandle();
    Q_ASSERT(subWindow);

    QWindow *mainWindow = QWindow::fromWinId(mainWindowId);
    if (!mainWindow) {
        // foreign windows not supported on all platforms
        return;
    }
    // mainWindow is not the child of any object, so make sure it gets deleted at some point
    QObject::connect(dialog, &QObject::destroyed, mainWindow, &QObject::deleteLater);
    subWindow->setTransientParent(mainWindow);
}

KMessageDialog::KMessageDialog(KMessageDialog::Type type, const QString &text, WId parent_id)
    : KMessageDialog(type, text)
{
    QWidget *parent = QWidget::find(parent_id);
    setParent(parent);
    if (!parent && parent_id) {
        setMainWindow(this, parent_id);
    }
}

KMessageDialog::~KMessageDialog()
{
    removeEventFilter(d.get());
}

void KMessageDialog::setCaption(const QString &caption)
{
    if (!caption.isEmpty()) {
        setWindowTitle(caption);
        return;
    }

    QString title;
    switch (d->m_type) { // Get a title based on the dialog Type
    case KMessageDialog::QuestionTwoActions:
    case KMessageDialog::QuestionTwoActionsCancel:
        title = QApplication::translate("KMessageDialog", "Question");
        break;
    case KMessageDialog::WarningTwoActions:
    case KMessageDialog::WarningTwoActionsCancel:
    case KMessageDialog::WarningContinueCancel:
        title = QApplication::translate("KMessageDialog", "Warning");
        break;
    case KMessageDialog::Information:
        title = QApplication::translate("KMessageDialog", "Information");
        break;
    case KMessageDialog::Error: {
        title = QApplication::translate("KMessageDialog", "Error");
        break;
    }
    default:
        break;
    }

    setWindowTitle(title);
}

void KMessageDialog::setIcon(const QIcon &icon)
{
    QIcon effectiveIcon(icon);
    if (effectiveIcon.isNull()) { // Fallback to an icon based on the dialog Type
        QStyle *style = this->style();
        switch (d->m_type) {
        case KMessageDialog::QuestionTwoActions:
        case KMessageDialog::QuestionTwoActionsCancel:
            effectiveIcon = style->standardIcon(QStyle::SP_MessageBoxQuestion, nullptr, this);
            break;
        case KMessageDialog::WarningTwoActions:
        case KMessageDialog::WarningTwoActionsCancel:
        case KMessageDialog::WarningContinueCancel:
            effectiveIcon = style->standardIcon(QStyle::SP_MessageBoxWarning, nullptr, this);
            break;
        case KMessageDialog::Information:
            effectiveIcon = style->standardIcon(QStyle::SP_MessageBoxInformation, nullptr, this);
            break;
        case KMessageDialog::Error:
            effectiveIcon = style->standardIcon(QStyle::SP_MessageBoxCritical, nullptr, this);
            break;
        default:
            break;
        }
    }

    if (effectiveIcon.isNull()) {
        qCWarning(KWidgetsAddonsLog) << "Neither the requested icon nor a generic one based on the "
                                        "dialog type could be found.";
        return;
    }

    d->m_iconLabel->setVisible(true);

    QStyleOption option;
    option.initFrom(d->m_mainWidget);
    QStyle *widgetStyle = d->m_mainWidget->style();
    const int size = widgetStyle->pixelMetric(QStyle::PM_MessageBoxIconSize, &option, d->m_mainWidget);
    d->m_iconLabel->setPixmap(effectiveIcon.pixmap(size));
}

void KMessageDialog::setListWidgetItems(const QStringList &strlist)
{
    const bool isEmpty = strlist.isEmpty();
    d->m_listWidget->setVisible(!isEmpty);
    if (isEmpty) {
        return;
    }

    // Enable automatic wrapping since the listwidget already has a good initial width
    d->m_messageLabel->setWordWrap(true);
    d->m_listWidget->addItems(strlist);

    QStyleOptionViewItem styleOption;
    styleOption.initFrom(d->m_listWidget);
    QFontMetrics fm(styleOption.font);
    int listWidth = d->m_listWidget->width();
    for (const QString &str : strlist) {
        listWidth = qMax(listWidth, fm.boundingRect(str).width());
    }
    const int borderWidth = (d->m_listWidget->width() - d->m_listWidget->viewport()->width() //
                             + d->m_listWidget->verticalScrollBar()->height());
    listWidth += borderWidth;
    const auto deskWidthPortion = screen()->geometry().width() * 0.85;
    if (listWidth > deskWidthPortion) { // Limit the list widget size to 85% of screen width
        listWidth = qRound(deskWidthPortion);
    }
    d->m_listWidget->setMinimumWidth(listWidth);
    d->m_listWidget->setSelectionMode(QListWidget::NoSelection);
    d->m_messageLabel->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum);
}

void KMessageDialog::setDetails(const QString &details)
{
    d->m_detailsGroup->setVisible(!details.isEmpty());

    if (details.length() < 512) { // random number KMessageBox uses.
        d->m_detailsLabel->setText(details);
        d->m_detailsLabel->show();

        d->m_detailsTextEdit->setText(QString());
        d->m_detailsTextEdit->hide();
    } else {
        d->m_detailsLabel->setText(QString());
        d->m_detailsLabel->hide();

        d->m_detailsTextEdit->setText(details);
        d->m_detailsTextEdit->show();
    }
}

void KMessageDialog::setButtons(const KGuiItem &primaryAction, const KGuiItem &secondaryAction, const KGuiItem &cancelAction)
{
    switch (d->m_type) {
    case KMessageDialog::QuestionTwoActions: {
        d->m_buttonBox->setStandardButtons(QDialogButtonBox::Yes | QDialogButtonBox::No);
        auto *buttonYes = d->m_buttonBox->button(QDialogButtonBox::Yes);
        KGuiItem::assign(buttonYes, primaryAction);
        buttonYes->setFocus();
        KGuiItem::assign(d->m_buttonBox->button(QDialogButtonBox::No), secondaryAction);
        break;
    }
    case KMessageDialog::QuestionTwoActionsCancel: {
        d->m_buttonBox->setStandardButtons(QDialogButtonBox::Yes | QDialogButtonBox::No | QDialogButtonBox::Cancel);
        auto *buttonYes = d->m_buttonBox->button(QDialogButtonBox::Yes);
        KGuiItem::assign(buttonYes, primaryAction);
        buttonYes->setFocus();
        KGuiItem::assign(d->m_buttonBox->button(QDialogButtonBox::No), secondaryAction);
        KGuiItem::assign(d->m_buttonBox->button(QDialogButtonBox::Cancel), cancelAction);
        break;
    }
    case KMessageDialog::WarningTwoActions: {
        d->m_buttonBox->setStandardButtons(QDialogButtonBox::Yes | QDialogButtonBox::No);
        KGuiItem::assign(d->m_buttonBox->button(QDialogButtonBox::Yes), primaryAction);

        auto *noBtn = d->m_buttonBox->button(QDialogButtonBox::No);
        KGuiItem::assign(noBtn, secondaryAction);
        noBtn->setDefault(true);
        noBtn->setFocus();
        break;
    }
    case KMessageDialog::WarningTwoActionsCancel: {
        d->m_buttonBox->setStandardButtons(QDialogButtonBox::Yes | QDialogButtonBox::No | QDialogButtonBox::Cancel);
        KGuiItem::assign(d->m_buttonBox->button(QDialogButtonBox::Yes), primaryAction);
        KGuiItem::assign(d->m_buttonBox->button(QDialogButtonBox::No), secondaryAction);

        auto *cancelButton = d->m_buttonBox->button(QDialogButtonBox::Cancel);
        KGuiItem::assign(cancelButton, cancelAction);
        cancelButton->setDefault(true);
        cancelButton->setFocus();
        break;
    }
    case KMessageDialog::WarningContinueCancel: {
        d->m_buttonBox->setStandardButtons(QDialogButtonBox::Yes | QDialogButtonBox::Cancel);

        KGuiItem::assign(d->m_buttonBox->button(QDialogButtonBox::Yes), primaryAction);

        auto *cancelButton = d->m_buttonBox->button(QDialogButtonBox::Cancel);
        KGuiItem::assign(cancelButton, cancelAction);
        cancelButton->setDefault(true);
        cancelButton->setFocus();
        break;
    }
    case KMessageDialog::Information:
    case KMessageDialog::Error: {
        d->m_buttonBox->setStandardButtons(QDialogButtonBox::Ok);
        auto *okButton = d->m_buttonBox->button(QDialogButtonBox::Ok);
        KGuiItem::assign(okButton, KStandardGuiItem::ok());
        okButton->setFocus();
        break;
    }
    default:
        break;
    }

    // Button connections
    if (!d->m_buttonBoxConnection) {
        d->m_buttonBoxConnection = connect(d->m_buttonBox, &QDialogButtonBox::clicked, this, [this](QAbstractButton *button) {
            QDialogButtonBox::StandardButton code = d->m_buttonBox->standardButton(button);
            const int result = (code == QDialogButtonBox::Ok) ? KMessageDialog::Ok
                : (code == QDialogButtonBox::Cancel)          ? KMessageDialog::Cancel
                : (code == QDialogButtonBox::Yes)             ? KMessageDialog::PrimaryAction
                : (code == QDialogButtonBox::No)              ? KMessageDialog::SecondaryAction
                                                              :
                                                 /* else */ -1;
            if (result != -1) {
                done(result);
            }
        });
    }
}

void KMessageDialog::setDontAskAgainText(const QString &dontAskAgainText)
{
    d->m_dontAskAgainCB->setVisible(!dontAskAgainText.isEmpty());
    d->m_dontAskAgainCB->setText(dontAskAgainText);
}

void KMessageDialog::setDontAskAgainChecked(bool isChecked)
{
    if (d->m_dontAskAgainCB->text().isEmpty()) {
        qCWarning(KWidgetsAddonsLog) << "setDontAskAgainChecked() method was called on a dialog that doesn't "
                                        "appear to have a checkbox; you need to use setDontAskAgainText() "
                                        "to add a checkbox to the dialog first.";
        return;
    }

    d->m_dontAskAgainCB->setChecked(isChecked);
}

bool KMessageDialog::isDontAskAgainChecked() const
{
    if (d->m_dontAskAgainCB->text().isEmpty()) {
        qCWarning(KWidgetsAddonsLog) << "isDontAskAgainChecked() method was called on a dialog that doesn't "
                                        "appear to have a checkbox; you need to use setDontAskAgainText() "
                                        "to add a checkbox to the dialog first.";
        return false;
    }

    return d->m_dontAskAgainCB->isChecked();
}

void KMessageDialog::setOpenExternalLinks(bool isAllowed)
{
    d->m_messageLabel->setOpenExternalLinks(isAllowed);
    d->m_detailsLabel->setOpenExternalLinks(isAllowed);
    d->m_detailsTextEdit->setOpenExternalLinks(isAllowed);
}

bool KMessageDialog::isNotifyEnabled() const
{
    return d->m_notifyEnabled;
}

void KMessageDialog::setNotifyEnabled(bool enable)
{
    d->m_notifyEnabled = enable;
}

void KMessageDialog::showEvent(QShowEvent *event)
{
    if (d->m_notifyEnabled && !event->spontaneous()) {
        // TODO include m_listWidget items
        beep(d->m_type, d->m_messageLabel->text(), topLevelWidget());
    }
    QDialog::showEvent(event);
}

void KMessageDialog::beep(Type type, const QString &text, QWidget *widget)
{
#ifndef Q_OS_WIN // FIXME problems with KNotify on Windows
    QMessageBox::Icon notifyType = QMessageBox::NoIcon;
    switch (type) {
    case KMessageDialog::QuestionTwoActions:
    case KMessageDialog::QuestionTwoActionsCancel:
        notifyType = QMessageBox::Question;
        break;
    case KMessageDialog::WarningTwoActions:
    case KMessageDialog::WarningTwoActionsCancel:
    case KMessageDialog::WarningContinueCancel:
        notifyType = QMessageBox::Warning;
        break;
    case KMessageDialog::Information:
        notifyType = QMessageBox::Information;
        break;
    case KMessageDialog::Error:
        notifyType = QMessageBox::Critical;
        break;
    }

    KMessageBox::notifyInterface()->sendNotification(notifyType, text, widget);
#endif
}

#include "kmessagedialog.moc"
#include "moc_kmessagedialog.cpp"
