// vim: set tabstop=4 shiftwidth=4 expandtab:
/*
Gwenview: an image viewer
Copyright 2007 Aurélien Gâteau <agateau@kde.org>

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

*/
// Self
#include "loadingdocumentimpl.h"

// STL
#include <memory>

// Exiv2
#include <exiv2/exiv2.hpp>

// Qt
#include <QBuffer>
#include <QByteArray>
#include <QColorSpace>
#include <QFile>
#include <QFuture>
#include <QFutureWatcher>
#include <QImage>
#include <QImageReader>
#include <QPointer>
#include <QUrl>
#include <QtConcurrent>

// KF
#include <KIO/TransferJob>
#include <KLocalizedString>
#include <KProtocolInfo>

#ifdef KDCRAW_FOUND
#include <kdcraw/kdcraw.h>
#endif

// Local
#include "animateddocumentloadedimpl.h"
#include "cms/cmsprofile.h"
#include "document.h"
#include "documentloadedimpl.h"
#include "emptydocumentimpl.h"
#include "exiv2imageloader.h"
#include "gvdebug.h"
#include "gwenview_lib_debug.h"
#include "gwenviewconfig.h"
#include "jpegcontent.h"
#include "jpegdocumentloadedimpl.h"
#include "svgdocumentloadedimpl.h"
#include "urlutils.h"
#include "videodocumentloadedimpl.h"

namespace Gwenview
{
#undef ENABLE_LOG
#undef LOG
// #define ENABLE_LOG
#ifdef ENABLE_LOG
#define LOG(x) // qCDebug(GWENVIEW_LIB_LOG) << x
#else
#define LOG(x) ;
#endif

const int HEADER_SIZE = 256;

struct LoadingDocumentImplPrivate {
    LoadingDocumentImpl *q;
    QPointer<KIO::TransferJob> mTransferJob;
    QFuture<bool> mMetaInfoFuture;
    QFutureWatcher<bool> mMetaInfoFutureWatcher;
    QFuture<void> mImageDataFuture;
    QFutureWatcher<void> mImageDataFutureWatcher;

    // If != 0, this means we need to load an image at zoom =
    // 1/mImageDataInvertedZoom
    int mImageDataInvertedZoom;

    bool mMetaInfoLoaded;
    bool mAnimated;
    bool mDownSampledImageLoaded;
    QByteArray mFormatHint;
    QByteArray mData;
    QByteArray mFormat;
    QSize mImageSize;
    std::unique_ptr<Exiv2::Image> mExiv2Image;
    std::unique_ptr<JpegContent> mJpegContent;
    QImage mImage;
    Cms::Profile::Ptr mCmsProfile;
    QMimeType mMimeType;

    /**
     * Determine kind of document and switch to an implementation if it is not
     * necessary to download more data.
     * @return true if switched to another implementation.
     */
    bool determineKind()
    {
        const QUrl &url = q->document()->url();
        QMimeDatabase db;
        if (KProtocolInfo::determineMimetypeFromExtension(url.scheme())) {
            mMimeType = db.mimeTypeForFileNameAndData(url.fileName(), mData);
        } else {
            mMimeType = db.mimeTypeForData(mData);
        }

        MimeTypeUtils::Kind kind = MimeTypeUtils::mimeTypeKind(mMimeType.name());
        LOG("mimeType:" << mMimeType.name());
        LOG("kind:" << kind);
        q->setDocumentKind(kind);

        switch (kind) {
        case MimeTypeUtils::KIND_RASTER_IMAGE:
        case MimeTypeUtils::KIND_SVG_IMAGE:
            return false;

        case MimeTypeUtils::KIND_VIDEO:
            q->switchToImpl(new VideoDocumentLoadedImpl(q->document()));
            return true;

        default:
            q->setDocumentErrorString(
                i18nc("@info",
                      "Gwenview cannot display documents of type %1. Install the kimageformats package to gain support for a wider range of media types.",
                      mMimeType.name()));
            Q_EMIT q->loadingFailed();
            q->switchToImpl(new EmptyDocumentImpl(q->document()));
            return true;
        }
    }

    void startLoading()
    {
        Q_ASSERT(!mMetaInfoLoaded);

        switch (q->document()->kind()) {
        case MimeTypeUtils::KIND_RASTER_IMAGE:
            // The hint is used to:
            // - Speed up loadMetaInfo(): QImageReader will try to decode the
            //   image using plugins matching this format first.
            // - Avoid breakage: Because of a bug in Qt TGA image plugin, some
            //   PNG were incorrectly identified as PCX! See:
            //   https://bugs.kde.org/show_bug.cgi?id=289819
            //
            mFormatHint = mMimeType.preferredSuffix().toLocal8Bit().toLower();
            mMetaInfoFuture = QtConcurrent::run(&LoadingDocumentImplPrivate::loadMetaInfo, this);
            mMetaInfoFutureWatcher.setFuture(mMetaInfoFuture);
            break;

        case MimeTypeUtils::KIND_SVG_IMAGE:
            q->switchToImpl(new SvgDocumentLoadedImpl(q->document(), mData));
            break;

        case MimeTypeUtils::KIND_VIDEO:
            break;

        default:
            qCWarning(GWENVIEW_LIB_LOG) << "We should not reach this point!";
            break;
        }
    }

    void startImageDataLoading()
    {
        LOG("");
        Q_ASSERT(mMetaInfoLoaded);
        Q_ASSERT(mImageDataInvertedZoom != 0);
        Q_ASSERT(!mImageDataFuture.isRunning());
        mImageDataFuture = QtConcurrent::run(&LoadingDocumentImplPrivate::loadImageData, this);
        mImageDataFutureWatcher.setFuture(mImageDataFuture);
    }

    bool loadMetaInfo()
    {
        LOG("mFormatHint" << mFormatHint);
        QBuffer buffer;
        buffer.setBuffer(&mData);
        buffer.open(QIODevice::ReadOnly);

        Exiv2ImageLoader loader;
        if (loader.load(mData)) {
            mExiv2Image = loader.popImage();
        }

        QImageReader reader;

#ifdef KDCRAW_FOUND
        if (!QImageReader::supportedImageFormats().contains(QByteArray("raw"))
            && KDcrawIface::KDcraw::rawFilesList().contains(QString::fromLatin1(mFormatHint))) {
            QByteArray previewData;

            // if the image is in format supported by dcraw, fetch its embedded preview
            mJpegContent = std::make_unique<JpegContent>();

            // use KDcraw for getting the embedded preview
            // KDcraw functionality cloned locally (temp. solution)
            bool ret = KDcrawIface::KDcraw::loadEmbeddedPreview(previewData, buffer);

            if (!ret) {
                // if the embedded preview loading failed, load half preview instead.
                // That's slower but it works even for images containing
                // small (160x120px) or none embedded preview.
                if (!KDcrawIface::KDcraw::loadHalfPreview(previewData, buffer)) {
                    qCWarning(GWENVIEW_LIB_LOG) << "unable to get half preview for " << q->document()->url().fileName();
                    return false;
                }
            }

            buffer.close();

            // now it's safe to replace mData with the jpeg data
            mData = previewData;

            // need to fill mFormat so gwenview can tell the type when trying to save
            mFormat = mFormatHint;
        } else {
#else
        {
#endif
            reader.setFormat(mFormatHint);
            reader.setDevice(&buffer);
            mImageSize = reader.size();

            if (!reader.canRead()) {
                qCWarning(GWENVIEW_LIB_LOG) << "QImageReader::read() using format hint" << mFormatHint << "failed:" << reader.errorString();
                if (buffer.pos() != 0) {
                    qCWarning(GWENVIEW_LIB_LOG) << "A bad Qt image decoder moved the buffer to" << buffer.pos() << "in a call to canRead()! Rewinding.";
                    buffer.seek(0);
                }
                reader.setFormat(QByteArray());
                // Set buffer again, otherwise QImageReader won't restart from scratch
                reader.setDevice(&buffer);
                if (!reader.canRead()) {
                    qCWarning(GWENVIEW_LIB_LOG) << "QImageReader::read() without format hint failed:" << reader.errorString();
                    return false;
                }
                qCWarning(GWENVIEW_LIB_LOG) << "Image format is actually" << reader.format() << "not" << mFormatHint;
            }

            mFormat = reader.format();

            if (mFormat == "jpg") {
                // if mFormatHint was "jpg", then mFormat is "jpg", but the rest of
                // Gwenview code assumes JPEG images have "jpeg" format.
                mFormat = "jpeg";
            }
        }

        LOG("mFormat" << mFormat);
        GV_RETURN_VALUE_IF_FAIL(!mFormat.isEmpty(), false);

        if (mFormat == "jpeg" && mExiv2Image.get()) {
            mJpegContent = std::make_unique<JpegContent>();
        }

        if (mJpegContent.get()) {
            if (!mJpegContent->loadFromData(mData, mExiv2Image.get()) && !mJpegContent->loadFromData(mData)) {
                qCWarning(GWENVIEW_LIB_LOG) << "Unable to use preview of " << q->document()->url().fileName();
                return false;
            }
            // Use the size from JpegContent, as its correctly transposed if the
            // image has been rotated
            mImageSize = mJpegContent->size();

            mCmsProfile = Cms::Profile::loadFromExiv2Image(mExiv2Image.get());
        }

        LOG("mImageSize" << mImageSize);

        if (!mCmsProfile) {
            mCmsProfile = Cms::Profile::loadFromImageData(mData, mFormat);
        }

        if (!mCmsProfile && reader.canRead()) {
            const QImage qtimage = reader.read();
            if (!qtimage.isNull()) {
                mCmsProfile = Cms::Profile::loadFromICC(qtimage.colorSpace().iccProfile());
            }
        }

        return true;
    }

    void loadImageData()
    {
        QBuffer buffer;
        buffer.setBuffer(&mData);
        buffer.open(QIODevice::ReadOnly);
        QImageReader reader(&buffer, mFormat);

        LOG("mImageDataInvertedZoom=" << mImageDataInvertedZoom);
        if (mImageSize.isValid() && mImageDataInvertedZoom != 1 && reader.supportsOption(QImageIOHandler::ScaledSize)) {
            // Do not use mImageSize here: QImageReader needs a non-transposed
            // image size
            QSize size = reader.size() / mImageDataInvertedZoom;
            if (!size.isEmpty()) {
                LOG("Setting scaled size to" << size);
                reader.setScaledSize(size);
            } else {
                LOG("Not setting scaled size as it is empty" << size);
            }
        }

        if (GwenviewConfig::applyExifOrientation()) {
            reader.setAutoTransform(true);
        }

        bool ok = reader.read(&mImage);
        if (!ok) {
            LOG("QImageReader::read() failed");
            return;
        }
        // Color space conversion logic
        if (!mImage.isNull() && mImage.colorSpace().isValid()) {
            // Convert to sRGB if not already sRGB
            if (mImage.colorSpace() != QColorSpace::SRgb) {
                LOG("Converting color space from" << mImage.colorSpace().description() << "to sRGB");
                QImage convertedImage = mImage.convertedToColorSpace(QColorSpace::SRgb);
                if (!convertedImage.isNull()) {
                    mImage = std::move(convertedImage);
                    LOG("Color space conversion completed");
                }
            }
        }
        if (reader.supportsAnimation() && reader.nextImageDelay() > 0 // Assume delay == 0 <=> only one frame
        ) {
            /*
             * QImageReader is not really helpful to detect animated gif:
             * - QImageReader::imageCount() returns 0
             * - QImageReader::nextImageDelay() may return something > 0 if the
             *   image consists of only one frame but includes a "Graphic
             *   Control Extension" (usually only present if we have an
             *   animation) (Bug #185523)
             *
             * Decoding the next frame is the only reliable way I found to
             * detect an animated gif
             */
            LOG("May be an animated image. delay:" << reader.nextImageDelay());
            QImage nextImage;
            if (reader.read(&nextImage)) {
                LOG("Really an animated image (more than one frame)");
                mAnimated = true;
            } else {
                qCWarning(GWENVIEW_LIB_LOG) << q->document()->url() << "is not really an animated image (only one frame)";
            }
        }
    }
};

LoadingDocumentImpl::LoadingDocumentImpl(Document *document)
    : AbstractDocumentImpl(document)
    , d(new LoadingDocumentImplPrivate)
{
    d->q = this;
    d->mMetaInfoLoaded = false;
    d->mAnimated = false;
    d->mDownSampledImageLoaded = false;
    d->mImageDataInvertedZoom = 0;

    connect(&d->mMetaInfoFutureWatcher, &QFutureWatcherBase::finished, this, &LoadingDocumentImpl::slotMetaInfoLoaded);

    connect(&d->mImageDataFutureWatcher, &QFutureWatcherBase::finished, this, &LoadingDocumentImpl::slotImageLoaded);
}

LoadingDocumentImpl::~LoadingDocumentImpl()
{
    LOG("");
    // Disconnect watchers to make sure they do not trigger further work
    d->mMetaInfoFutureWatcher.disconnect();
    d->mImageDataFutureWatcher.disconnect();

    d->mMetaInfoFutureWatcher.waitForFinished();
    d->mImageDataFutureWatcher.waitForFinished();

    if (d->mTransferJob) {
        d->mTransferJob->kill();
    }
    delete d;
}

void LoadingDocumentImpl::init()
{
    QUrl url = document()->url();

    if (UrlUtils::urlIsFastLocalFile(url)) {
        // Load file content directly
        QFile file(url.toLocalFile());
        if (!file.open(QIODevice::ReadOnly)) {
            setDocumentErrorString(i18nc("@info", "Could not open file %1", url.toLocalFile()));
            Q_EMIT loadingFailed();
            switchToImpl(new EmptyDocumentImpl(document()));
            return;
        }
        d->mData = file.read(HEADER_SIZE);
        if (d->determineKind()) {
            return;
        }
        d->mData += file.readAll();
        d->startLoading();
    } else {
        // Transfer file via KIO
        d->mTransferJob = KIO::get(document()->url(), KIO::NoReload, KIO::HideProgressInfo);
        connect(d->mTransferJob.data(), &KIO::TransferJob::data, this, &LoadingDocumentImpl::slotDataReceived);
        connect(d->mTransferJob.data(), &KJob::result, this, &LoadingDocumentImpl::slotTransferFinished);
        d->mTransferJob->start();
    }
}

void LoadingDocumentImpl::loadImage(int invertedZoom)
{
    if (d->mImageDataInvertedZoom == invertedZoom) {
        LOG("Already loading an image at invertedZoom=" << invertedZoom);
        return;
    }
    if (d->mImageDataInvertedZoom == 1) {
        LOG("Ignoring request: we are loading a full image");
        return;
    }
    d->mImageDataFutureWatcher.waitForFinished();
    d->mImageDataInvertedZoom = invertedZoom;

    if (d->mMetaInfoLoaded) {
        // Do not test on mMetaInfoFuture.isRunning() here: it might not have
        // started if we are downloading the image from a remote url
        d->startImageDataLoading();
    }
}

void LoadingDocumentImpl::slotDataReceived(KIO::Job *job, const QByteArray &chunk)
{
    d->mData.append(chunk);
    if (document()->kind() == MimeTypeUtils::KIND_UNKNOWN && d->mData.length() >= HEADER_SIZE) {
        if (d->determineKind()) {
            job->kill();
            return;
        }
    }
}

void LoadingDocumentImpl::slotTransferFinished(KJob *job)
{
    if (job->error()) {
        setDocumentErrorString(job->errorString());
        Q_EMIT loadingFailed();
        switchToImpl(new EmptyDocumentImpl(document()));
        return;
    } else if (document()->kind() == MimeTypeUtils::KIND_UNKNOWN) {
        // Transfer finished. If the mime type is still unknown (e.g. for files < HEADER_SIZE)
        // determine the kind again.
        if (d->determineKind()) {
            return;
        }
    }
    d->startLoading();
}

bool LoadingDocumentImpl::isEditable() const
{
    return d->mDownSampledImageLoaded;
}

Document::LoadingState LoadingDocumentImpl::loadingState() const
{
    if (!document()->image().isNull()) {
        return Document::Loaded;
    } else if (d->mMetaInfoLoaded) {
        return Document::MetaInfoLoaded;
    } else if (document()->kind() != MimeTypeUtils::KIND_UNKNOWN) {
        return Document::KindDetermined;
    } else {
        return Document::Loading;
    }
}

void LoadingDocumentImpl::slotMetaInfoLoaded()
{
    LOG("");
    Q_ASSERT(!d->mMetaInfoFuture.isRunning());
    if (!d->mMetaInfoFuture.result()) {
        setDocumentErrorString(i18nc("@info", "Loading meta information failed."));
        Q_EMIT loadingFailed();
        switchToImpl(new EmptyDocumentImpl(document()));
        return;
    }

    setDocumentFormat(d->mFormat);
    setDocumentImageSize(d->mImageSize);
    setDocumentExiv2Image(std::move(d->mExiv2Image));
    setDocumentCmsProfile(d->mCmsProfile);

    d->mMetaInfoLoaded = true;
    Q_EMIT metaInfoLoaded();

    // Start image loading if necessary
    // We test if mImageDataFuture is not already running because code connected to
    // metaInfoLoaded() signal could have called loadImage()
    if (!d->mImageDataFuture.isRunning() && d->mImageDataInvertedZoom != 0) {
        d->startImageDataLoading();
    }
}

void LoadingDocumentImpl::slotImageLoaded()
{
    LOG("");
    if (d->mImage.isNull()) {
        setDocumentErrorString(i18nc("@info", "Loading image failed."));
        Q_EMIT loadingFailed();
        switchToImpl(new EmptyDocumentImpl(document()));
        return;
    }

    if (d->mAnimated) {
        if (d->mImage.size() == d->mImageSize) {
            // We already decoded the first frame at the right size, let's show
            // it
            setDocumentImage(d->mImage);
        }

        switchToImpl(new AnimatedDocumentLoadedImpl(document(), d->mData));

        return;
    }

    if (d->mImageDataInvertedZoom != 1 && d->mImage.size() != d->mImageSize) {
        LOG("Loaded a down sampled image");
        d->mDownSampledImageLoaded = true;
        // We loaded a down sampled image
        setDocumentDownSampledImage(d->mImage, d->mImageDataInvertedZoom);
        return;
    }

    LOG("Loaded a full image");
    setDocumentImage(d->mImage);
    DocumentLoadedImpl *impl;
    if (d->mJpegContent.get()) {
        impl = new JpegDocumentLoadedImpl(document(), d->mJpegContent.release());
    } else {
        impl = new DocumentLoadedImpl(document(), d->mData);
    }
    switchToImpl(impl);
}

} // namespace

#include "moc_loadingdocumentimpl.cpp"
