
/*
   SPDX-FileCopyrightText: 2003-2007 Clarence Dang <dang@kde.org>

   SPDX-License-Identifier: BSD-2-Clause
*/

#define DEBUG_KP_DOCUMENT_SAVE_OPTIONS_WIDGET 0

#include "widgets/kpDocumentSaveOptionsWidget.h"

#include "dialogs/imagelib/transforms/kpTransformPreviewDialog.h"
#include "dialogs/kpDocumentSaveOptionsPreviewDialog.h"
#include "document/kpDocument.h"
#include "generic/kpWidgetMapper.h"
#include "generic/widgets/kpResizeSignallingLabel.h"
#include "kpDefs.h"
#include "pixmapfx/kpPixmapFX.h"

#include "kpLogCategories.h"
#include <KConfigGroup>
#include <KLocalization>
#include <KSharedConfig>

#include <QApplication>
#include <QBuffer>
#include <QComboBox>
#include <QHBoxLayout>
#include <QImage>
#include <QLabel>
#include <QPushButton>
#include <QSpinBox>
#include <QTimer>

kpDocumentSaveOptionsWidget::kpDocumentSaveOptionsWidget(const QImage &docPixmap,
                                                         const kpDocumentSaveOptions &saveOptions,
                                                         const kpDocumentMetaInfo &metaInfo,
                                                         QWidget *parent)
    : QWidget(parent)
    , m_visualParent(parent)
{
    init();
    setDocumentSaveOptions(saveOptions);
    setDocumentPixmap(docPixmap);
    setDocumentMetaInfo(metaInfo);
}

kpDocumentSaveOptionsWidget::kpDocumentSaveOptionsWidget(QWidget *parent)
    : QWidget(parent)
    , m_visualParent(parent)
{
    init();
}

// private
void kpDocumentSaveOptionsWidget::init()
{
    m_documentPixmap = nullptr;
    m_previewDialog = nullptr;
    m_visualParent = nullptr;

    m_colorDepthLabel = new QLabel(i18n("Convert &to:"), this);
    m_colorDepthCombo = new QComboBox(this);

    m_colorDepthSpaceWidget = new QWidget(this);

    m_qualityLabel = new QLabel(i18n("Quali&ty:"), this);
    m_qualityInput = new QSpinBox(this);
    // Note that we set min to 1 not 0 since "0 Quality" is a bit misleading
    // and 101 quality settings would be weird.  So we lose 1 quality setting
    // according to QImage::save().
    // TODO: 100 quality is also misleading since that implies perfect quality.
    m_qualityInput->setRange(1, 100);
    KLocalization::setupSpinBoxFormatString(m_qualityInput, ki18nc("@label:spinbox", "%v%"));

    m_previewButton = new QPushButton(i18n("&Preview"), this);
    m_previewButton->setCheckable(true);

    m_colorDepthLabel->setBuddy(m_colorDepthCombo);

    m_qualityLabel->setBuddy(m_qualityInput);

    auto *lay = new QHBoxLayout(this);
    lay->setContentsMargins(0, 0, 0, 0);

    lay->addWidget(m_colorDepthLabel, 0 /*stretch*/, Qt::AlignLeft);
    lay->addWidget(m_colorDepthCombo, 0 /*stretch*/);

    lay->addWidget(m_colorDepthSpaceWidget, 1 /*stretch*/);

    lay->addWidget(m_qualityLabel, 0 /*stretch*/, Qt::AlignLeft);
    lay->addWidget(m_qualityInput, 2 /*stretch*/);

    lay->addWidget(m_previewButton, 0 /*stretch*/, Qt::AlignRight);

    connect(m_colorDepthCombo, static_cast<void (QComboBox::*)(int)>(&QComboBox::activated), this, &kpDocumentSaveOptionsWidget::slotColorDepthSelected);

    connect(m_colorDepthCombo, static_cast<void (QComboBox::*)(int)>(&QComboBox::activated), this, &kpDocumentSaveOptionsWidget::updatePreview);

    connect(m_qualityInput, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, &kpDocumentSaveOptionsWidget::updatePreviewDelayed);

    connect(m_previewButton, &QPushButton::toggled, this, &kpDocumentSaveOptionsWidget::showPreview);

    m_updatePreviewDelay = 200 /*ms*/;

    m_updatePreviewTimer = new QTimer(this);
    m_updatePreviewTimer->setSingleShot(true);
    connect(m_updatePreviewTimer, &QTimer::timeout, this, &kpDocumentSaveOptionsWidget::updatePreview);

    m_updatePreviewDialogLastRelativeGeometryTimer = new QTimer(this);
    connect(m_updatePreviewDialogLastRelativeGeometryTimer, &QTimer::timeout, this, &kpDocumentSaveOptionsWidget::updatePreviewDialogLastRelativeGeometry);

    setMode(None);

    slotColorDepthSelected();
}

kpDocumentSaveOptionsWidget::~kpDocumentSaveOptionsWidget()
{
#if DEBUG_KP_DOCUMENT_SAVE_OPTIONS_WIDGET
    qCDebug(kpLogWidgets) << "kpDocumentSaveOptionsWidget::<dtor>()";
#endif
    hidePreview();

    delete m_documentPixmap;
}

// public
void kpDocumentSaveOptionsWidget::setVisualParent(QWidget *visualParent)
{
#if DEBUG_KP_DOCUMENT_SAVE_OPTIONS_WIDGET
    qCDebug(kpLogWidgets) << "kpDocumentSaveOptionsWidget::setVisualParent(" << visualParent << ")" << endl;
#endif

    m_visualParent = visualParent;
}

// protected
bool kpDocumentSaveOptionsWidget::mimeTypeHasConfigurableColorDepth() const
{
    return kpDocumentSaveOptions::mimeTypeHasConfigurableColorDepth(mimeType());
}

// protected
bool kpDocumentSaveOptionsWidget::mimeTypeHasConfigurableQuality() const
{
    return kpDocumentSaveOptions::mimeTypeHasConfigurableQuality(mimeType());
}

// public
QString kpDocumentSaveOptionsWidget::mimeType() const
{
    return m_baseDocumentSaveOptions.mimeType();
}

// public slots
void kpDocumentSaveOptionsWidget::setMimeType(const QString &string)
{
#if DEBUG_KP_DOCUMENT_SAVE_OPTIONS_WIDGET
    qCDebug(kpLogWidgets) << "kpDocumentSaveOptionsWidget::setMimeType(" << string
                          << ") maxColorDepth=" << kpDocumentSaveOptions::mimeTypeMaximumColorDepth(string) << endl;
#endif

    const int newMimeTypeMaxDepth = kpDocumentSaveOptions::mimeTypeMaximumColorDepth(string);

#if DEBUG_KP_DOCUMENT_SAVE_OPTIONS_WIDGET
    qCDebug(kpLogWidgets) << "\toldMimeType=" << mimeType() << " maxColorDepth=" << kpDocumentSaveOptions::mimeTypeMaximumColorDepth(mimeType()) << endl;
#endif

    if (mimeType().isEmpty() || kpDocumentSaveOptions::mimeTypeMaximumColorDepth(mimeType()) != newMimeTypeMaxDepth) {
        m_colorDepthCombo->clear();

        m_colorDepthCombo->insertItem(0, i18n("Monochrome"));
        m_colorDepthCombo->insertItem(1, i18n("Monochrome (Dithered)"));

        if (newMimeTypeMaxDepth >= 8) {
            m_colorDepthCombo->insertItem(2, i18n("256 Color"));
            m_colorDepthCombo->insertItem(3, i18n("256 Color (Dithered)"));
        }

        if (newMimeTypeMaxDepth >= 24) {
            m_colorDepthCombo->insertItem(4, i18n("24-bit Color"));
        }

        if (m_colorDepthComboLastSelectedItem >= 0 && m_colorDepthComboLastSelectedItem < m_colorDepthCombo->count()) {
#if DEBUG_KP_DOCUMENT_SAVE_OPTIONS_WIDGET
            qCDebug(kpLogWidgets) << "\tsetting colorDepthCombo to " << m_colorDepthComboLastSelectedItem << endl;
#endif

            m_colorDepthCombo->setCurrentIndex(m_colorDepthComboLastSelectedItem);
        } else {
#if DEBUG_KP_DOCUMENT_SAVE_OPTIONS_WIDGET
            qCDebug(kpLogWidgets) << "\tsetting colorDepthCombo to max item since"
                                  << " m_colorDepthComboLastSelectedItem=" << m_colorDepthComboLastSelectedItem << " out of range" << endl;
#endif

            m_colorDepthCombo->setCurrentIndex(m_colorDepthCombo->count() - 1);
        }
    }

    m_baseDocumentSaveOptions.setMimeType(string);

    if (mimeTypeHasConfigurableColorDepth()) {
        setMode(ColorDepth);
    } else if (mimeTypeHasConfigurableQuality()) {
        setMode(Quality);
    } else {
        setMode(None);
    }

    updatePreview();
}

// public
int kpDocumentSaveOptionsWidget::colorDepth() const
{
    if (mode() & ColorDepth) {
        // The returned values match QImage's supported depths.
        switch (m_colorDepthCombo->currentIndex()) {
        case 0:
        case 1:
            return 1;

        case 2:
        case 3:
            return 8;

        case 4:
            // 24-bit is known as 32-bit with QImage.
            return 32;

        default:
            return kpDocumentSaveOptions::invalidColorDepth();
        }
    } else {
        return m_baseDocumentSaveOptions.colorDepth();
    }
}

// public
bool kpDocumentSaveOptionsWidget::dither() const
{
    if (mode() & ColorDepth) {
        return (m_colorDepthCombo->currentIndex() == 1 || m_colorDepthCombo->currentIndex() == 3);
    }

    return m_baseDocumentSaveOptions.dither();
}

// protected static
int kpDocumentSaveOptionsWidget::colorDepthComboItemFromColorDepthAndDither(int depth, bool dither)
{
    switch (depth) {
    case 1:
        if (!dither) {
            return 0;
        }
        return 1;

    case 8:
        if (!dither) {
            return 2;
        }
        return 3;

    case 32:
        return 4;

    default:
        return -1;
    }
}

// public slots
void kpDocumentSaveOptionsWidget::setColorDepthDither(int newDepth, bool newDither)
{
#if DEBUG_KP_DOCUMENT_SAVE_OPTIONS_WIDGET
    qCDebug(kpLogWidgets) << "kpDocumentSaveOptionsWidget::setColorDepthDither("
                          << "depth=" << newDepth << ",dither=" << newDither << ")" << endl;
#endif

    m_baseDocumentSaveOptions.setColorDepth(newDepth);
    m_baseDocumentSaveOptions.setDither(newDither);

    const int comboItem = colorDepthComboItemFromColorDepthAndDither(newDepth, newDither);
    // TODO: Ignoring when comboItem >= m_colorDepthCombo->count() is wrong.
    //       This happens if this mimeType has configurable color depth
    //       and an incorrect maximum color depth (less than a QImage of
    //       this mimeType, opened by kpDocument).
    if (comboItem >= 0 && comboItem < m_colorDepthCombo->count()) {
        m_colorDepthCombo->setCurrentIndex(comboItem);
    }

    slotColorDepthSelected();
}

// protected slot
void kpDocumentSaveOptionsWidget::slotColorDepthSelected()
{
    if (mode() & ColorDepth) {
        m_colorDepthComboLastSelectedItem = m_colorDepthCombo->currentIndex();
    } else {
        m_colorDepthComboLastSelectedItem =
            colorDepthComboItemFromColorDepthAndDither(m_baseDocumentSaveOptions.colorDepth(), m_baseDocumentSaveOptions.dither());
    }

#if DEBUG_KP_DOCUMENT_SAVE_OPTIONS_WIDGET
    qCDebug(kpLogWidgets) << "kpDocumentSaveOptionsWidget::slotColorDepthSelected()"
                          << " mode&ColorDepth=" << (mode() & ColorDepth) << " colorDepthComboLastSelectedItem=" << m_colorDepthComboLastSelectedItem << endl;
#endif
}

// public
int kpDocumentSaveOptionsWidget::quality() const
{
    if (mode() & Quality) {
        return m_qualityInput->value();
    }

    return m_baseDocumentSaveOptions.quality();
}

// public
void kpDocumentSaveOptionsWidget::setQuality(int newQuality)
{
#if DEBUG_KP_DOCUMENT_SAVE_OPTIONS_WIDGET
    qCDebug(kpLogWidgets) << "kpDocumentSaveOptionsWidget::setQuality(" << newQuality << ")" << endl;
#endif

    m_baseDocumentSaveOptions.setQuality(newQuality);
    m_qualityInput->setValue(newQuality == -1 /*QImage::save() default*/ ? 75 : newQuality);
}

// public
kpDocumentSaveOptions kpDocumentSaveOptionsWidget::documentSaveOptions() const
{
    return kpDocumentSaveOptions(mimeType(), colorDepth(), dither(), quality());
}

// public
void kpDocumentSaveOptionsWidget::setDocumentSaveOptions(const kpDocumentSaveOptions &saveOptions)
{
    setMimeType(saveOptions.mimeType());
    setColorDepthDither(saveOptions.colorDepth(), saveOptions.dither());
    setQuality(saveOptions.quality());
}

// public
void kpDocumentSaveOptionsWidget::setDocumentPixmap(const QImage &documentPixmap)
{
    delete m_documentPixmap;
    m_documentPixmap = new QImage(documentPixmap);

    updatePreview();
}

// public
void kpDocumentSaveOptionsWidget::setDocumentMetaInfo(const kpDocumentMetaInfo &metaInfo)
{
    m_documentMetaInfo = metaInfo;

    updatePreview();
}

// public
kpDocumentSaveOptionsWidget::Mode kpDocumentSaveOptionsWidget::mode() const
{
    return m_mode;
}

// public
void kpDocumentSaveOptionsWidget::setMode(Mode mode)
{
    m_mode = mode;

    // If mode == None, we show still show the Color Depth widgets but disabled
    m_colorDepthLabel->setVisible(mode != Quality);
    m_colorDepthCombo->setVisible(mode != Quality);
    m_colorDepthSpaceWidget->setVisible(mode != Quality);

    m_qualityLabel->setVisible(mode == Quality);
    m_qualityInput->setVisible(mode == Quality);

    m_colorDepthLabel->setEnabled(mode == ColorDepth);
    m_colorDepthCombo->setEnabled(mode == ColorDepth);

    m_qualityLabel->setEnabled(mode == Quality);
    m_qualityInput->setEnabled(mode == Quality);

    // SYNC: HACK: When changing between color depth and quality widgets,
    //       we change the height of "this", causing the text on the labels
    //       to move but the first instance of the text doesn't get erased.
    //       Qt bug.
    QTimer::singleShot(0, this, &kpDocumentSaveOptionsWidget::repaintLabels);
}

// protected slot
void kpDocumentSaveOptionsWidget::repaintLabels()
{
    if (mode() != Quality) {
        m_colorDepthLabel->repaint();
    }
    if (mode() == Quality) {
        m_qualityLabel->repaint();
    }
}

// protected slot
void kpDocumentSaveOptionsWidget::showPreview(bool yes)
{
#if DEBUG_KP_DOCUMENT_SAVE_OPTIONS_WIDGET
    qCDebug(kpLogWidgets) << "kpDocumentSaveOptionsWidget::showPreview(" << yes << ")"
                          << " m_previewDialog=" << bool(m_previewDialog) << endl;
#endif

    if (yes == bool(m_previewDialog)) {
        return;
    }

    if (!m_visualParent) {
        return;
    }

    if (yes) {
        m_previewDialog = new kpDocumentSaveOptionsPreviewDialog(m_visualParent);
        m_previewDialog->setObjectName(QStringLiteral("previewSaveDialog"));
        updatePreview();

        connect(m_previewDialog, &kpDocumentSaveOptionsPreviewDialog::finished, this, &kpDocumentSaveOptionsWidget::hidePreview);

        KConfigGroup cfg(KSharedConfig::openConfig(), QStringLiteral(kpSettingsGroupPreviewSave));

        if (cfg.hasKey(kpSettingPreviewSaveUpdateDelay)) {
            m_updatePreviewDelay = cfg.readEntry(kpSettingPreviewSaveUpdateDelay, 0);
        } else {
            cfg.writeEntry(kpSettingPreviewSaveUpdateDelay, m_updatePreviewDelay);
            cfg.sync();
        }

        if (m_updatePreviewDelay < 0) {
            m_updatePreviewDelay = 0;
        }
#if DEBUG_KP_DOCUMENT_SAVE_OPTIONS_WIDGET
        qCDebug(kpLogWidgets) << "\tread cfg preview dialog update delay=" << m_updatePreviewDelay;
#endif

        if (m_previewDialogLastRelativeGeometry.isEmpty()) {
#if DEBUG_KP_DOCUMENT_SAVE_OPTIONS_WIDGET
            qCDebug(kpLogWidgets) << "\tread cfg preview dialog last rel geometry";
#endif
            KConfigGroup cfg(KSharedConfig::openConfig(), QStringLiteral(kpSettingsGroupPreviewSave));

            m_previewDialogLastRelativeGeometry = cfg.readEntry(kpSettingPreviewSaveGeometry, QRect());
        }

#if DEBUG_KP_DOCUMENT_SAVE_OPTIONS_WIDGET
        qCDebug(kpLogWidgets) << "\tpreviewDialogLastRelativeGeometry=" << m_previewDialogLastRelativeGeometry
                              << " visualParent->rect()=" << m_visualParent->rect() << endl;
#endif

        QRect relativeGeometry;
        if (!m_previewDialogLastRelativeGeometry.isEmpty() && m_visualParent->rect().intersects(m_previewDialogLastRelativeGeometry)) {
#if DEBUG_KP_DOCUMENT_SAVE_OPTIONS_WIDGET
            qCDebug(kpLogWidgets) << "\tok";
#endif
            relativeGeometry = m_previewDialogLastRelativeGeometry;
        } else {
#if DEBUG_KP_DOCUMENT_SAVE_OPTIONS_WIDGET
            qCDebug(kpLogWidgets) << "\t\tinvalid";
#endif
            const int margin = 20;

            relativeGeometry = QRect(m_visualParent->width() - m_previewDialog->preferredMinimumSize().width() - margin,
                                     margin * 2, // Avoid folder combo
                                     m_previewDialog->preferredMinimumSize().width(),
                                     m_previewDialog->preferredMinimumSize().height());
        }

        const QRect globalGeometry = kpWidgetMapper::toGlobal(m_visualParent, relativeGeometry);
#if DEBUG_KP_DOCUMENT_SAVE_OPTIONS_WIDGET
        qCDebug(kpLogWidgets) << "\trelativeGeometry=" << relativeGeometry << " globalGeometry=" << globalGeometry << endl;
#endif

        m_previewDialog->resize(globalGeometry.size());
        m_previewDialog->move(globalGeometry.topLeft());

        m_previewDialog->show();

#if DEBUG_KP_DOCUMENT_SAVE_OPTIONS_WIDGET
        qCDebug(kpLogWidgets) << "\tgeometry after show="
                              << QRect(m_previewDialog->x(), m_previewDialog->y(), m_previewDialog->width(), m_previewDialog->height()) << endl;
#endif

        updatePreviewDialogLastRelativeGeometry();

        connect(m_previewDialog, &kpDocumentSaveOptionsPreviewDialog::moved, this, &kpDocumentSaveOptionsWidget::updatePreviewDialogLastRelativeGeometry);

        connect(m_previewDialog, &kpDocumentSaveOptionsPreviewDialog::resized, this, &kpDocumentSaveOptionsWidget::updatePreviewDialogLastRelativeGeometry);

        m_updatePreviewDialogLastRelativeGeometryTimer->start(200 /*ms*/);
    } else {
        m_updatePreviewDialogLastRelativeGeometryTimer->stop();

        KConfigGroup cfg(KSharedConfig::openConfig(), QStringLiteral(kpSettingsGroupPreviewSave));

        cfg.writeEntry(kpSettingPreviewSaveGeometry, m_previewDialogLastRelativeGeometry);
        cfg.sync();

#if DEBUG_KP_DOCUMENT_SAVE_OPTIONS_WIDGET
        qCDebug(kpLogWidgets) << "\tsaving preview geometry " << m_previewDialogLastRelativeGeometry << " (Qt would have us believe "
                              << kpWidgetMapper::fromGlobal(
                                     m_visualParent,
                                     QRect(m_previewDialog->x(), m_previewDialog->y(), m_previewDialog->width(), m_previewDialog->height()))
                              << ")" << endl;
#endif

        m_previewDialog->deleteLater();
        m_previewDialog = nullptr;
    }
}

// protected slot
void kpDocumentSaveOptionsWidget::hidePreview()
{
    if (m_previewButton->isChecked()) {
        m_previewButton->toggle();
    }
}

// protected slot
void kpDocumentSaveOptionsWidget::updatePreviewDelayed()
{
    // (single shot)
    m_updatePreviewTimer->start(m_updatePreviewDelay);
}

// protected slot
void kpDocumentSaveOptionsWidget::updatePreview()
{
    if (!m_previewDialog || !m_documentPixmap) {
        return;
    }

    m_updatePreviewTimer->stop();

    QApplication::setOverrideCursor(Qt::WaitCursor);

    QByteArray data;

    QBuffer buffer(&data);
    buffer.open(QIODevice::WriteOnly);
    bool savedOK = kpDocument::savePixmapToDevice(*m_documentPixmap, &buffer, documentSaveOptions(), m_documentMetaInfo, false /*no lossy prompt*/, this);
    buffer.close();

    QImage image;

    // Ignore any failed saves.
    //
    // Failed saves might literally have written half a file.  The final
    // save (when the user clicks OK), _will_ fail so we shouldn't have a
    // preview even if this "half a file" is actually loadable by
    // QImage::loadFormData().
    if (savedOK) {
        image.loadFromData(data);
    } else {
        // Leave <image> as invalid.
        // TODO: This code path has not been well tested.
        //       Will we trigger divide by zero errors in "m_previewDialog"?
    }

    // REFACTOR: merge with kpDocument::getPixmapFromFile()
    m_previewDialog->setFilePixmapAndSize(image, data.size());

    QApplication::restoreOverrideCursor();
}

// protected slot
void kpDocumentSaveOptionsWidget::updatePreviewDialogLastRelativeGeometry()
{
#if DEBUG_KP_DOCUMENT_SAVE_OPTIONS_WIDGET
    qCDebug(kpLogWidgets) << "kpDocumentSaveOptionsWidget::"
                          << "updatePreviewDialogLastRelativeGeometry()" << endl;
#endif

    if (m_previewDialog && m_previewDialog->isVisible()) {
        m_previewDialogLastRelativeGeometry =
            kpWidgetMapper::fromGlobal(m_visualParent, QRect(m_previewDialog->x(), m_previewDialog->y(), m_previewDialog->width(), m_previewDialog->height()));
#if DEBUG_KP_DOCUMENT_SAVE_OPTIONS_WIDGET
        qCDebug(kpLogWidgets) << "\tcaching pos = " << m_previewDialogLastRelativeGeometry;
#endif
    } else {
#if DEBUG_KP_DOCUMENT_SAVE_OPTIONS_WIDGET
        qCDebug(kpLogWidgets) << "\tnot visible - ignoring geometry";
#endif
    }
}

#include "moc_kpDocumentSaveOptionsWidget.cpp"
