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

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

#define DEBUG_KP_DOCUMENT 0

#include "kpDocument.h"
#include "kpDocumentPrivate.h"

#include <QFile>
#include <QImage>
#include <QMimeDatabase>
#include <QSaveFile>
#include <QTemporaryFile>

#include "kpLogCategories.h"
#include <KIO/FileCopyJob>
#include <KJobWidgets>
#include <KLocalizedString>
#include <KMessageBox>

#include "document/kpDocumentSaveOptions.h"
#include "environments/document/kpDocumentEnvironment.h"
#include "imagelib/effects/kpEffectReduceColors.h"
#include "imagelib/kpColor.h"
#include "imagelib/kpDocumentMetaInfo.h"
#include "kpDefs.h"
#include "lgpl/generic/kpUrlFormatter.h"
#include "pixmapfx/kpPixmapFX.h"
#include "tools/kpTool.h"
#include "views/manager/kpViewManager.h"
#include "widgets/toolbars/kpColorToolBar.h"
#include "widgets/toolbars/kpToolToolBar.h"

bool kpDocument::save(bool lossyPrompt)
{
#if DEBUG_KP_DOCUMENT
    qCDebug(kpLogDocument) << "kpDocument::save("
                           << ",lossyPrompt=" << lossyPrompt << ") url=" << m_url << " savedAtLeastOnceBefore=" << savedAtLeastOnceBefore();
#endif

    // TODO: check feels weak
    if (m_url.isEmpty() || m_saveOptions->mimeType().isEmpty()) {
        KMessageBox::detailedError(d->environ->dialogParent(),
                                   i18n("Could not save image - insufficient information."),
                                   i18n("URL: %1\n"
                                        "Mimetype: %2",
                                        prettyUrl(),
                                        m_saveOptions->mimeType().isEmpty() ? i18n("<empty>") : m_saveOptions->mimeType()),
                                   i18nc("@title:window", "Internal Error"));
        return false;
    }

    return saveAs(m_url, *m_saveOptions, lossyPrompt);
}

//---------------------------------------------------------------------

// public static
bool kpDocument::lossyPromptContinue(const QImage &pixmap, const kpDocumentSaveOptions &saveOptions, QWidget *parent)
{
#if DEBUG_KP_DOCUMENT
    qCDebug(kpLogDocument) << "kpDocument::lossyPromptContinue()";
#endif

#define QUIT_IF_CANCEL(messageBoxCommand)                                                                                                                      \
    {                                                                                                                                                          \
        if (messageBoxCommand != KMessageBox::Continue) {                                                                                                      \
            return false;                                                                                                                                      \
        }                                                                                                                                                      \
    }

    const int lossyType = saveOptions.isLossyForSaving(pixmap);
    if (lossyType & (kpDocumentSaveOptions::MimeTypeMaximumColorDepthLow | kpDocumentSaveOptions::Quality)) {
        QMimeDatabase db;

        QUIT_IF_CANCEL(KMessageBox::warningContinueCancel(parent,
                                                          i18n("<qt><p>The <b>%1</b> format may not be able"
                                                               " to preserve all of the image's color information.</p>"

                                                               "<p>Are you sure you want to save in this format?</p></qt>",
                                                               db.mimeTypeForName(saveOptions.mimeType()).comment()),
                                                          // TODO: caption misleading for lossless formats that have
                                                          //       low maximum color depth
                                                          i18nc("@title:window", "Lossy File Format"),
                                                          KStandardGuiItem::save(),
                                                          KStandardGuiItem::cancel(),
                                                          QLatin1String("SaveInLossyMimeTypeDontAskAgain")));
    } else if (lossyType & kpDocumentSaveOptions::ColorDepthLow) {
        QUIT_IF_CANCEL(KMessageBox::warningContinueCancel(parent,
                                                          i18n("<qt><p>Saving the image at the low color depth of %1-bit"
                                                               " may result in the loss of color information."

                                                               // TODO: It looks like 8-bit QImage's now support alpha.
                                                               //       Update kpDocumentSaveOptions::isLossyForSaving()
                                                               //       and change "might" to "will".
                                                               " Any transparency might also be removed.</p>"

                                                               "<p>Are you sure you want to save at this color depth?</p></qt>",
                                                               saveOptions.colorDepth()),
                                                          i18nc("@title:window", "Low Color Depth"),
                                                          KStandardGuiItem::save(),
                                                          KStandardGuiItem::cancel(),
                                                          QLatin1String("SaveAtLowColorDepthDontAskAgain")));
    }
#undef QUIT_IF_CANCEL

    return true;
}

//---------------------------------------------------------------------

// public static
bool kpDocument::savePixmapToDevice(const QImage &image,
                                    QIODevice *device,
                                    const kpDocumentSaveOptions &saveOptions,
                                    const kpDocumentMetaInfo &metaInfo,
                                    bool lossyPrompt,
                                    QWidget *parent,
                                    bool *userCancelled)
{
    if (userCancelled)
        *userCancelled = false;

    QString type = QMimeDatabase().mimeTypeForName(saveOptions.mimeType()).preferredSuffix();
#if DEBUG_KP_DOCUMENT
    qCDebug(kpLogDocument) << "\tmimeType=" << saveOptions.mimeType() << " type=" << type;
#endif
    if (type.isEmpty())
        return false;

    if (lossyPrompt && !lossyPromptContinue(image, saveOptions, parent)) {
        if (userCancelled)
            *userCancelled = true;

#if DEBUG_KP_DOCUMENT
        qCDebug(kpLogDocument) << "\treturning false because of lossyPrompt";
#endif
        return false;
    }

    // TODO: fix dup with kpDocumentSaveOptions::isLossyForSaving()
    const bool useSaveOptionsColorDepth = (saveOptions.mimeTypeHasConfigurableColorDepth() && !saveOptions.colorDepthIsInvalid());

    const bool useSaveOptionsQuality = (saveOptions.mimeTypeHasConfigurableQuality() && !saveOptions.qualityIsInvalid());

    //
    // Reduce colors if required
    //

#if DEBUG_KP_DOCUMENT
    qCDebug(kpLogDocument) << "\tuseSaveOptionsColorDepth=" << useSaveOptionsColorDepth << "current image depth=" << image.depth()
                           << "save options depth=" << saveOptions.colorDepth();
#endif
    QImage imageToSave(image);

    if (useSaveOptionsColorDepth && imageToSave.depth() != saveOptions.colorDepth()) {
        // TODO: I think this erases the mask!
        //
        //       I suspect this doesn't matter since this is only called to
        //       reduce color depth and QImage's with depth < 32 don't
        //       support masks anyway.
        //
        //       Later: I think the mask is preserved for 8-bit since Qt4
        //              seems to support it for QImage.
        imageToSave = kpEffectReduceColors::convertImageDepth(imageToSave, saveOptions.colorDepth(), saveOptions.dither());
    }

    //
    // Write Meta Info
    //

    imageToSave.setDotsPerMeterX(metaInfo.dotsPerMeterX());
    imageToSave.setDotsPerMeterY(metaInfo.dotsPerMeterY());
    imageToSave.setOffset(metaInfo.offset());

    const QStringList keys = metaInfo.textKeys();
    for (const QString &key : keys)
        imageToSave.setText(key, metaInfo.text(key));

    //
    // Save at required quality
    //

    int quality = -1; // default

    if (useSaveOptionsQuality)
        quality = saveOptions.quality();

#if DEBUG_KP_DOCUMENT
    qCDebug(kpLogDocument) << "\tsaving";
#endif
    if (!imageToSave.save(device, type.toLatin1().constData(), quality)) {
#if DEBUG_KP_DOCUMENT
        qCDebug(kpLogDocument) << "\tQImage::save() returned false";
#endif
        return false;
    }

#if DEBUG_KP_DOCUMENT
    qCDebug(kpLogDocument) << "\tsave OK";
#endif
    return true;
}

//---------------------------------------------------------------------

static void CouldNotCreateTemporaryFileDialog(QWidget *parent)
{
    KMessageBox::error(parent, i18n("Could not save image - unable to create temporary file."));
}

//---------------------------------------------------------------------

static void CouldNotSaveDialog(const QUrl &url, const QString &error, QWidget *parent)
{
    KMessageBox::error(parent, i18n("Could not save as \"%1\": %2", kpUrlFormatter::PrettyFilename(url), error));
}

//---------------------------------------------------------------------

// public static
bool kpDocument::savePixmapToFile(const QImage &pixmap,
                                  const QUrl &url,
                                  const kpDocumentSaveOptions &saveOptions,
                                  const kpDocumentMetaInfo &metaInfo,
                                  bool lossyPrompt,
                                  QWidget *parent)
{
    // TODO: Use KIO::NetAccess:mostLocalURL() for accessing home:/ (and other
    //       such local URLs) for efficiency and because only local writes
    //       are atomic.
#if DEBUG_KP_DOCUMENT
    qCDebug(kpLogDocument) << "kpDocument::savePixmapToFile (" << url << ",lossyPrompt=" << lossyPrompt << ")";
    saveOptions.printDebug(QLatin1String("\tsaveOptions"));
    metaInfo.printDebug(QLatin1String("\tmetaInfo"));
#endif

    if (lossyPrompt && !lossyPromptContinue(pixmap, saveOptions, parent)) {
#if DEBUG_KP_DOCUMENT
        qCDebug(kpLogDocument) << "\treturning false because of lossyPrompt";
#endif
        return false;
    }

    // Local file?
    if (url.isLocalFile()) {
        const QString filename = url.toLocalFile();

        // sync: All failure exit paths _must_ call QSaveFile::cancelWriting() or
        //       else, the QSaveFile destructor will overwrite the file,
        //       <filename>, despite the failure.
        QSaveFile atomicFileWriter(filename);
        {
            if (!atomicFileWriter.open(QIODevice::WriteOnly)) {
                // We probably don't need this as <filename> has not been
                // opened.
                atomicFileWriter.cancelWriting();

#if DEBUG_KP_DOCUMENT
                qCDebug(kpLogDocument) << "\treturning false because could not open QSaveFile"
                                       << " error=" << atomicFileWriter.error() << endl;
#endif
                ::CouldNotCreateTemporaryFileDialog(parent);
                return false;
            }

            // Write to local temporary file.
            if (!savePixmapToDevice(pixmap, &atomicFileWriter, saveOptions, metaInfo, false /*no lossy prompt*/, parent)) {
                atomicFileWriter.cancelWriting();

#if DEBUG_KP_DOCUMENT
                qCDebug(kpLogDocument) << "\treturning false because could not save pixmap to device" << endl;
#endif
                ::CouldNotSaveDialog(url, i18n("Error saving image"), parent);
                return false;
            }

            // Atomically overwrite local file with the temporary file
            // we saved to.
            if (!atomicFileWriter.commit()) {
                atomicFileWriter.cancelWriting();

#if DEBUG_KP_DOCUMENT
                qCDebug(kpLogDocument) << "\tcould not close QSaveFile";
#endif
                ::CouldNotSaveDialog(url, atomicFileWriter.errorString(), parent);
                return false;
            }
        } // sync QSaveFile.cancelWriting()
    }
    // Remote file?
    else {
        // Create temporary file that is deleted when the variable goes
        // out of scope.
        QTemporaryFile tempFile;
        if (!tempFile.open()) {
#if DEBUG_KP_DOCUMENT
            qCDebug(kpLogDocument) << "\treturning false because could not open tempFile";
#endif
            ::CouldNotCreateTemporaryFileDialog(parent);
            return false;
        }

        // Write to local temporary file.
        if (!savePixmapToDevice(pixmap, &tempFile, saveOptions, metaInfo, false /*no lossy prompt*/, parent)) {
#if DEBUG_KP_DOCUMENT
            qCDebug(kpLogDocument) << "\treturning false because could not save pixmap to device" << endl;
#endif
            ::CouldNotSaveDialog(url, i18n("Error saving image"), parent);
            return false;
        }

        // Collect name of temporary file now, as QTemporaryFile::fileName()
        // stops working after close() is called.
        const QString tempFileName = tempFile.fileName();
#if DEBUG_KP_DOCUMENT
        qCDebug(kpLogDocument) << "\ttempFileName='" << tempFileName << "'";
#endif
        Q_ASSERT(!tempFileName.isEmpty());

        tempFile.close();
        if (tempFile.error() != QFile::NoError) {
#if DEBUG_KP_DOCUMENT
            qCDebug(kpLogDocument) << "\treturning false because could not close";
#endif
            ::CouldNotSaveDialog(url, tempFile.errorString(), parent);
            return false;
        }

        // Copy local temporary file to overwrite remote.
        // It's the KIO worker's job to make this atomic (write to .part, then rename .part file)
        KIO::FileCopyJob *job = KIO::file_copy(QUrl::fromLocalFile(tempFileName), url, -1, KIO::Overwrite);
        KJobWidgets::setWindow(job, parent);
        if (!job->exec()) {
#if DEBUG_KP_DOCUMENT
            qCDebug(kpLogDocument) << "\treturning false because could not upload";
#endif
            KMessageBox::error(parent, i18n("Could not save image - failed to upload."));
            return false;
        }
    }

    return true;
}

//---------------------------------------------------------------------

bool kpDocument::saveAs(const QUrl &url, const kpDocumentSaveOptions &saveOptions, bool lossyPrompt)
{
#if DEBUG_KP_DOCUMENT
    qCDebug(kpLogDocument) << "kpDocument::saveAs (" << url << "," << saveOptions.mimeType() << ")" << endl;
#endif

    if (kpDocument::savePixmapToFile(imageWithSelection(), url, saveOptions, *metaInfo(), lossyPrompt, d->environ->dialogParent())) {
        setURL(url, true /*is from url*/);
        *m_saveOptions = saveOptions;
        m_modified = false;

        m_savedAtLeastOnceBefore = true;

        Q_EMIT documentSaved();
        return true;
    }

    return false;
}

//---------------------------------------------------------------------
