/*
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.

*/
#include "thumbnailview.h"

// STL
#include <cmath>

// Qt
#include <QApplication>
#include <QDateTime>
#include <QDrag>
#include <QDragEnterEvent>
#include <QDropEvent>
#include <QMimeData>
#include <QPainter>
#include <QPointer>
#include <QQueue>
#include <QScrollBar>
#include <QScroller>
#include <QTimeLine>
#include <QTimer>
#include <QWindow>

// KF
#include <KDirLister>
#include <KDirModel>
#include <KIconLoader>
#include <KPixmapSequence>
#include <KPixmapSequenceLoader>
#include <KUrlMimeData>

// Local
#include "abstractdocumentinfoprovider.h"
#include "abstractthumbnailviewhelper.h"
#include "dragpixmapgenerator.h"
#include "gwenview_lib_debug.h"
#include "gwenviewconfig.h"
#include "mimetypeutils.h"
#include "urlutils.h"
#include <lib/gvdebug.h>
#include <lib/scrollerutils.h>
#include <lib/semanticinfo/sorteddirmodel.h>
#include <lib/thumbnailprovider/thumbnailprovider.h>
#include <lib/touch/touch.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

/** How many msec to wait before starting to smooth thumbnails */
const int SMOOTH_DELAY = 500;

const int WHEEL_ZOOM_MULTIPLIER = 4;

static KFileItem fileItemForIndex(const QModelIndex &index)
{
    if (!index.isValid()) {
        LOG("Invalid index");
        return {};
    }
    QVariant data = index.data(KDirModel::FileItemRole);
    return qvariant_cast<KFileItem>(data);
}

static QUrl urlForIndex(const QModelIndex &index)
{
    KFileItem item = fileItemForIndex(index);
    return item.isNull() ? QUrl() : item.url();
}

struct Thumbnail {
    Thumbnail(const QPersistentModelIndex &index_, const QDateTime &mtime)
        : mIndex(index_)
        , mModificationTime(mtime)
        , mFileSize(0)
        , mRough(true)
        , mWaitingForThumbnail(true)
    {
    }

    Thumbnail()
        : mFileSize(0)
        , mRough(true)
        , mWaitingForThumbnail(true)
    {
    }

    /**
     * Init the thumbnail based on a icon
     */
    void initAsIcon(const QPixmap &pix)
    {
        mGroupPix = pix;
        int largeGroupSize = ThumbnailGroup::pixelSize(ThumbnailGroup::Large);
        mFullSize = QSize(largeGroupSize, largeGroupSize);
    }

    bool isGroupPixAdaptedForSize(int size) const
    {
        if (mWaitingForThumbnail) {
            return false;
        }
        if (mGroupPix.isNull()) {
            return false;
        }
        const int groupSize = qMax(mGroupPix.width(), mGroupPix.height());
        if (groupSize >= size) {
            return true;
        }

        // groupSize is less than size, but this may be because the full image
        // is the same size as groupSize
        return groupSize == qMax(mFullSize.width(), mFullSize.height());
    }

    void prepareForRefresh(const QDateTime &mtime)
    {
        mModificationTime = mtime;
        mFileSize = 0;
        mGroupPix = QPixmap();
        mAdjustedPix = QPixmap();
        mFullSize = QSize();
        mRealFullSize = QSize();
        mRough = true;
        mWaitingForThumbnail = true;
    }

    QPersistentModelIndex mIndex;
    QDateTime mModificationTime;
    /// The pix loaded from .thumbnails/{large,normal}
    QPixmap mGroupPix;
    /// Scaled version of mGroupPix, adjusted to ThumbnailView::thumbnailSize
    QPixmap mAdjustedPix;
    /// Size of the full image
    QSize mFullSize;
    /// Real size of the full image, invalid unless the thumbnail
    /// represents a raster image (not an icon)
    QSize mRealFullSize;
    /// File size of the full image
    KIO::filesize_t mFileSize;
    /// Whether mAdjustedPix represents has been scaled using fast or smooth
    /// transformation
    bool mRough;
    /// Set to true if mGroupPix should be replaced with a real thumbnail
    bool mWaitingForThumbnail;
};

using ThumbnailForUrl = QHash<QUrl, Thumbnail>;
using UrlQueue = QQueue<QUrl>;
using PersistentModelIndexSet = QSet<QPersistentModelIndex>;

class WindowScaleWatcher : public QObject
{
    Q_OBJECT
public:
    void setWindow(QWindow *window)
    {
        if (mWindow == window) {
            return;
        }
        if (mWindow) {
            mWindow->removeEventFilter(this);
        }
        mWindow = window;
        if (mWindow) {
            mWindow->installEventFilter(this);
            updateDevicePixelRatio();
        }
    }

protected:
    bool eventFilter(QObject *watched, QEvent *event) override
    {
        if (event->type() == QEvent::DevicePixelRatioChange) {
            updateDevicePixelRatio();
        }
        return QObject::eventFilter(watched, event);
    }

private:
    void updateDevicePixelRatio()
    {
        if (mWindow->devicePixelRatio() != mLastDevicePixelRatio) {
            Q_EMIT scaleChanged();
            mLastDevicePixelRatio = mWindow->devicePixelRatio();
        }
    }

Q_SIGNALS:
    void scaleChanged();

private:
    QPointer<QWindow> mWindow;
    // we don't know what dpr the widget was using
    // before getting a window as it comes from the primary screen
    // always trigger on the first set
    qreal mLastDevicePixelRatio = -1;
};

struct ThumbnailViewPrivate {
    ThumbnailView *q;
    ThumbnailView::ThumbnailScaleMode mScaleMode;
    QSize mThumbnailSize;
    int mThumbnailLogicalWidth = 0;
    qreal mThumbnailAspectRatio;
    AbstractDocumentInfoProvider *mDocumentInfoProvider;
    AbstractThumbnailViewHelper *mThumbnailViewHelper;
    ThumbnailForUrl mThumbnailForUrl;
    QTimer mScheduledThumbnailGenerationTimer;
    WindowScaleWatcher mWindowScaleWatcher;

    UrlQueue mSmoothThumbnailQueue;
    QTimer mSmoothThumbnailTimer;

    QPixmap mWaitingThumbnail;
    QPointer<ThumbnailProvider> mThumbnailProvider;

    PersistentModelIndexSet mBusyIndexSet;
    KPixmapSequence mBusySequence;
    QTimeLine *mBusyAnimationTimeLine;

    bool mCreateThumbnailsForRemoteUrls;

    QScroller *mScroller;
    Touch *mTouch;

    bool loading = false;

    void setupBusyAnimation()
    {
        mBusySequence = KPixmapSequenceLoader::load(QStringLiteral("process-working"), 22);
        mBusyAnimationTimeLine = new QTimeLine(100 * mBusySequence.frameCount(), q);
        mBusyAnimationTimeLine->setEasingCurve(QEasingCurve::Linear);
        mBusyAnimationTimeLine->setEndFrame(mBusySequence.frameCount() - 1);
        mBusyAnimationTimeLine->setLoopCount(0);
        QObject::connect(mBusyAnimationTimeLine, &QTimeLine::frameChanged, q, &ThumbnailView::updateBusyIndexes);
    }

    void scheduleThumbnailGeneration()
    {
        if (mThumbnailProvider) {
            mThumbnailProvider->removePendingItems();
        }
        mSmoothThumbnailQueue.clear();
        if (!mScheduledThumbnailGenerationTimer.isActive()) {
            mScheduledThumbnailGenerationTimer.start();
        }
    }

    void updateThumbnailForModifiedDocument(const QModelIndex &index)
    {
        Q_ASSERT(mDocumentInfoProvider);
        KFileItem item = fileItemForIndex(index);
        QUrl url = item.url();
        ThumbnailGroup::Enum group = ThumbnailGroup::fromPixelSize(mThumbnailSize.width());
        QPixmap pix;
        QSize fullSize;
        mDocumentInfoProvider->thumbnailForDocument(url, group, &pix, &fullSize);
        mThumbnailForUrl[url] = Thumbnail(QPersistentModelIndex(index), QDateTime::currentDateTime());
        q->setThumbnail(item, pix, fullSize, 0);
    }

    void appendItemsToThumbnailProvider(const KFileItemList &list)
    {
        if (mThumbnailProvider) {
            ThumbnailGroup::Enum group = ThumbnailGroup::fromPixelSize(mThumbnailSize.width());
            mThumbnailProvider->setThumbnailGroup(group);
            mThumbnailProvider->appendItems(list);
        }
    }

    void roughAdjustThumbnail(Thumbnail *thumbnail)
    {
        const QPixmap &mGroupPix = thumbnail->mGroupPix;
        const int groupSize = qMax(mGroupPix.width(), mGroupPix.height());
        const int fullSize = qMax(thumbnail->mFullSize.width(), thumbnail->mFullSize.height());
        if (fullSize == groupSize && mGroupPix.height() <= mThumbnailSize.height() && mGroupPix.width() <= mThumbnailSize.width()) {
            thumbnail->mAdjustedPix = mGroupPix;
            thumbnail->mRough = false;
        } else {
            thumbnail->mAdjustedPix = scale(mGroupPix, Qt::FastTransformation);
            thumbnail->mRough = true;
        }
    }

    void initDragPixmap(QDrag *drag, const QModelIndexList &indexes)
    {
        const int thumbCount = qMin(indexes.count(), int(DragPixmapGenerator::MaxCount));
        QList<QPixmap> lst;
        for (int row = 0; row < thumbCount; ++row) {
            const QUrl url = urlForIndex(indexes[row]);
            lst << mThumbnailForUrl.value(url).mAdjustedPix;
        }
        DragPixmapGenerator::DragPixmap dragPixmap = DragPixmapGenerator::generate(lst, indexes.count());
        drag->setPixmap(dragPixmap.pix);
        drag->setHotSpot(dragPixmap.hotSpot);
    }

    QPixmap scale(const QPixmap &pix, Qt::TransformationMode transformationMode)
    {
        switch (mScaleMode) {
        case ThumbnailView::ScaleToFit:
            return pix.scaled(mThumbnailSize.width(), mThumbnailSize.height(), Qt::KeepAspectRatio, transformationMode);
        case ThumbnailView::ScaleToSquare: {
            int minSize = qMin(pix.width(), pix.height());
            QPixmap pix2 = pix.copy((pix.width() - minSize) / 2, (pix.height() - minSize) / 2, minSize, minSize);
            return pix2.scaled(mThumbnailSize.width(), mThumbnailSize.height(), Qt::KeepAspectRatio, transformationMode);
        }
        case ThumbnailView::ScaleToHeight:
            return pix.scaledToHeight(mThumbnailSize.height(), transformationMode);
        case ThumbnailView::ScaleToWidth:
            return pix.scaledToWidth(mThumbnailSize.width(), transformationMode);
        }
        // Keep compiler happy
        Q_ASSERT(0);
        return {};
    }
};

ThumbnailView::ThumbnailView(QWidget *parent)
    : QListView(parent)
    , d(new ThumbnailViewPrivate)
{
    d->q = this;
    d->mScaleMode = ScaleToFit;
    d->mThumbnailViewHelper = nullptr;
    d->mDocumentInfoProvider = nullptr;
    d->mThumbnailProvider = nullptr;
    // Init to some stupid value so that the first call to setThumbnailSize()
    // is not ignored (do not use 0 in case someone try to divide by
    // mThumbnailSize...)
    d->mThumbnailSize = QSize(1, 1);
    d->mThumbnailAspectRatio = 1;
    d->mCreateThumbnailsForRemoteUrls = true;

    setFrameShape(QFrame::NoFrame);
    setViewMode(QListView::IconMode);
    setResizeMode(QListView::Adjust);
    setDragEnabled(true);
    setAcceptDrops(true);
    setDropIndicatorShown(true);
    setUniformItemSizes(true);
    setEditTriggers(QAbstractItemView::EditKeyPressed);

    d->setupBusyAnimation();

    setVerticalScrollMode(ScrollPerPixel);
    setHorizontalScrollMode(ScrollPerPixel);

    d->mScheduledThumbnailGenerationTimer.setSingleShot(true);
    d->mScheduledThumbnailGenerationTimer.setInterval(500);
    connect(&d->mScheduledThumbnailGenerationTimer, &QTimer::timeout, this, &ThumbnailView::generateThumbnailsForItems);

    d->mSmoothThumbnailTimer.setSingleShot(true);
    connect(&d->mSmoothThumbnailTimer, &QTimer::timeout, this, &ThumbnailView::smoothNextThumbnail);

    setContextMenuPolicy(Qt::CustomContextMenu);
    connect(this, &ThumbnailView::customContextMenuRequested, this, &ThumbnailView::showContextMenu);

    connect(this, &ThumbnailView::activated, this, &ThumbnailView::emitIndexActivatedIfNoModifiers);

    connect(&d->mWindowScaleWatcher, &WindowScaleWatcher::scaleChanged, this, [this]() {
        setThumbnailWidth(d->mThumbnailLogicalWidth);
    });

    d->mScroller = ScrollerUtils::setQScroller(this->viewport());
    d->mTouch = new Touch(viewport());
    connect(d->mTouch, &Touch::twoFingerTapTriggered, this, &ThumbnailView::showContextMenu);
    connect(d->mTouch, &Touch::pinchZoomTriggered, this, &ThumbnailView::zoomGesture);
    connect(d->mTouch, &Touch::pinchGestureStarted, this, &ThumbnailView::setZoomParameter);
    connect(d->mTouch, &Touch::tapTriggered, this, &ThumbnailView::tapGesture);
    connect(d->mTouch, &Touch::tapHoldAndMovingTriggered, this, &ThumbnailView::startDragFromTouch);

    const QFontMetrics metrics(viewport()->font());
    const int singleStep = metrics.height() * QApplication::wheelScrollLines();

    verticalScrollBar()->setSingleStep(singleStep);
    horizontalScrollBar()->setSingleStep(singleStep);
}

ThumbnailView::~ThumbnailView()
{
    delete d->mTouch;
    delete d;
}

ThumbnailView::ThumbnailScaleMode ThumbnailView::thumbnailScaleMode() const
{
    return d->mScaleMode;
}

void ThumbnailView::setThumbnailScaleMode(ThumbnailScaleMode mode)
{
    d->mScaleMode = mode;
    setUniformItemSizes(mode == ScaleToFit || mode == ScaleToSquare);
}

void ThumbnailView::setModel(QAbstractItemModel *newModel)
{
    if (model()) {
        disconnect(model(), nullptr, this, nullptr);

        const auto sortedModel = qobject_cast<SortedDirModel *>(newModel);
        if (sortedModel) {
            sortedModel->dirLister()->disconnect(this);
        }
    }
    QListView::setModel(newModel);

    connect(model(), &QAbstractItemModel::rowsRemoved, this, [=](const QModelIndex &index, int first, int last) {
        // Avoid the delegate doing a ton of work if we're not visible
        if (isVisible()) {
            Q_EMIT rowsRemovedSignal(index, first, last);
        }
    });

    const auto sortedModel = qobject_cast<SortedDirModel *>(newModel);
    if (sortedModel) {
        connect(sortedModel->dirLister(), &KDirLister::started, this, [this]() {
            d->loading = true;
        });

        connect(sortedModel->dirLister(), &KDirLister::listingDirCompleted, this, [this]() {
            d->loading = false;
            d->scheduleThumbnailGeneration();
        });
    }
}

void ThumbnailView::setThumbnailProvider(ThumbnailProvider *thumbnailProvider)
{
    GV_RETURN_IF_FAIL(d->mThumbnailProvider != thumbnailProvider);
    if (thumbnailProvider) {
        connect(thumbnailProvider, &ThumbnailProvider::thumbnailLoaded, this, &ThumbnailView::setThumbnail);
        connect(thumbnailProvider, &ThumbnailProvider::thumbnailLoadingFailed, this, &ThumbnailView::setBrokenThumbnail);
    } else {
        disconnect(d->mThumbnailProvider, nullptr, this, nullptr);
    }
    d->mThumbnailProvider = thumbnailProvider;
}

void ThumbnailView::updateThumbnailSize()
{
    QSize value = d->mThumbnailSize;
    // mWaitingThumbnail
    const auto dpr = devicePixelRatioF();
    int waitingThumbnailSize;
    if (value.width() > 64 * dpr) {
        waitingThumbnailSize = qRound(48 * dpr);
    } else {
        waitingThumbnailSize = qRound(32 * dpr);
    }
    QPixmap icon = QIcon::fromTheme(QStringLiteral("chronometer")).pixmap(waitingThumbnailSize);
    QPixmap pix(value);
    pix.fill(Qt::transparent);
    QPainter painter(&pix);
    painter.setOpacity(0.5);
    style()->drawItemPixmap(&painter, QRect(QPoint(), pix.deviceIndependentSize().toSize()), Qt::AlignCenter, icon);
    painter.end();
    d->mWaitingThumbnail = pix;
    d->mWaitingThumbnail.setDevicePixelRatio(dpr);

    // Stop smoothing
    d->mSmoothThumbnailTimer.stop();
    d->mSmoothThumbnailQueue.clear();

    // Clear adjustedPixes
    ThumbnailForUrl::iterator it = d->mThumbnailForUrl.begin(), end = d->mThumbnailForUrl.end();
    for (; it != end; ++it) {
        it.value().mAdjustedPix = QPixmap();
    }

    Q_EMIT thumbnailSizeChanged(value / dpr);
    Q_EMIT thumbnailWidthChanged(qRound(value.width() / dpr));
    if (d->mScaleMode != ScaleToFit) {
        scheduleDelayedItemsLayout();
    }
    d->scheduleThumbnailGeneration();
}

void ThumbnailView::setThumbnailWidth(int width)
{
    d->mThumbnailLogicalWidth = width;
    const auto dpr = devicePixelRatioF();
    const qreal newWidthF = width * dpr;
    const int newWidth = qRound(newWidthF);
    if (d->mThumbnailSize.width() == newWidth) {
        return;
    }
    int height = qRound(newWidthF / d->mThumbnailAspectRatio);
    d->mThumbnailSize = QSize(newWidth, height);
    updateThumbnailSize();
}

void ThumbnailView::setThumbnailAspectRatio(qreal ratio)
{
    if (d->mThumbnailAspectRatio == ratio) {
        return;
    }
    d->mThumbnailAspectRatio = ratio;
    int width = d->mThumbnailSize.width();
    int height = round((qreal)width / d->mThumbnailAspectRatio);
    d->mThumbnailSize = QSize(width, height);
    updateThumbnailSize();
}

qreal ThumbnailView::thumbnailAspectRatio() const
{
    return d->mThumbnailAspectRatio;
}

QSize ThumbnailView::thumbnailSize() const
{
    return d->mThumbnailSize / devicePixelRatioF();
}

void ThumbnailView::setThumbnailViewHelper(AbstractThumbnailViewHelper *helper)
{
    d->mThumbnailViewHelper = helper;
}

AbstractThumbnailViewHelper *ThumbnailView::thumbnailViewHelper() const
{
    return d->mThumbnailViewHelper;
}

void ThumbnailView::setDocumentInfoProvider(AbstractDocumentInfoProvider *provider)
{
    d->mDocumentInfoProvider = provider;
    if (provider) {
        connect(provider, &AbstractDocumentInfoProvider::busyStateChanged, this, &ThumbnailView::updateThumbnailBusyState);
        connect(provider, &AbstractDocumentInfoProvider::documentChanged, this, &ThumbnailView::updateThumbnail);
    }
}

AbstractDocumentInfoProvider *ThumbnailView::documentInfoProvider() const
{
    return d->mDocumentInfoProvider;
}

void ThumbnailView::rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end)
{
    QListView::rowsAboutToBeRemoved(parent, start, end);

    // Remove references to removed items
    KFileItemList itemList;
    for (int pos = start; pos <= end; ++pos) {
        QModelIndex index = model()->index(pos, 0, parent);
        KFileItem item = fileItemForIndex(index);
        if (item.isNull()) {
            // qCDebug(GWENVIEW_LIB_LOG) << "Skipping invalid item!" << index.data().toString();
            continue;
        }

        QUrl url = item.url();
        d->mThumbnailForUrl.remove(url);
        d->mSmoothThumbnailQueue.removeAll(url);

        itemList.append(item);
    }

    if (d->mThumbnailProvider) {
        d->mThumbnailProvider->removeItems(itemList);
    }

    // Removing rows might make new images visible, make sure their thumbnail
    // is generated
    if (!d->mScheduledThumbnailGenerationTimer.isActive()) {
        d->mScheduledThumbnailGenerationTimer.start();
    }
}

void ThumbnailView::rowsInserted(const QModelIndex &parent, int start, int end)
{
    QListView::rowsInserted(parent, start, end);

    if (!d->mScheduledThumbnailGenerationTimer.isActive()) {
        d->mScheduledThumbnailGenerationTimer.start();
    }

    if (isVisible()) {
        Q_EMIT rowsInsertedSignal(parent, start, end);
    }
}

void ThumbnailView::dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles)
{
    QListView::dataChanged(topLeft, bottomRight, roles);
    bool thumbnailsNeedRefresh = false;
    for (int row = topLeft.row(); row <= bottomRight.row(); ++row) {
        QModelIndex index = model()->index(row, 0);
        KFileItem item = fileItemForIndex(index);
        if (item.isNull()) {
            qCWarning(GWENVIEW_LIB_LOG) << "Invalid item for index" << index << ". This should not happen!";
            GV_FATAL_FAILS;
            continue;
        }

        ThumbnailForUrl::Iterator it = d->mThumbnailForUrl.find(item.url());
        if (it != d->mThumbnailForUrl.end()) {
            // All thumbnail views are connected to the model, so
            // ThumbnailView::dataChanged() is called for all of them. As a
            // result this method will also be called for views which are not
            // currently visible, and do not yet have a thumbnail for the
            // modified url.
            QDateTime mtime = item.time(KFileItem::ModificationTime);
            if (it->mModificationTime != mtime || it->mFileSize != item.size()) {
                // dataChanged() is called when the file changes but also when
                // the model fetched additional data such as semantic info. To
                // avoid needless refreshes, we only trigger a refresh if the
                // modification time changes.
                thumbnailsNeedRefresh = true;
                it->prepareForRefresh(mtime);
            }
        }
    }
    if (thumbnailsNeedRefresh && !d->mScheduledThumbnailGenerationTimer.isActive()) {
        d->mScheduledThumbnailGenerationTimer.start();
    }
}

void ThumbnailView::showContextMenu()
{
    d->mThumbnailViewHelper->showContextMenu(this);
}

void ThumbnailView::emitIndexActivatedIfNoModifiers(const QModelIndex &index)
{
    if (QApplication::keyboardModifiers() == Qt::NoModifier) {
        Q_EMIT indexActivated(index);
    }
}

void ThumbnailView::setThumbnail(const KFileItem &item, const QPixmap &pixmap, const QSize &size, qulonglong fileSize)
{
    ThumbnailForUrl::iterator it = d->mThumbnailForUrl.find(item.url());
    if (it == d->mThumbnailForUrl.end()) {
        return;
    }
    Thumbnail &thumbnail = it.value();
    thumbnail.mGroupPix = pixmap;
    thumbnail.mAdjustedPix = QPixmap();
    int largeGroupSize = ThumbnailGroup::pixelSize(ThumbnailGroup::XLarge);
    thumbnail.mFullSize = size.isValid() ? size : QSize(largeGroupSize, largeGroupSize);
    thumbnail.mRealFullSize = size;
    thumbnail.mWaitingForThumbnail = false;
    thumbnail.mFileSize = fileSize;

    update(thumbnail.mIndex);
    if (d->mScaleMode != ScaleToFit) {
        scheduleDelayedItemsLayout();
    }
}

void ThumbnailView::setBrokenThumbnail(const KFileItem &item)
{
    ThumbnailForUrl::iterator it = d->mThumbnailForUrl.find(item.url());
    if (it == d->mThumbnailForUrl.end()) {
        return;
    }
    Thumbnail &thumbnail = it.value();
    MimeTypeUtils::Kind kind = MimeTypeUtils::fileItemKind(item);
    if (kind == MimeTypeUtils::KIND_VIDEO) {
        // Special case for videos because our kde install may come without
        // support for video thumbnails so we show the mimetype icon instead of
        // a broken image icon
        const QPixmap pix = QIcon::fromTheme(item.iconName()).pixmap(d->mThumbnailSize.height());
        thumbnail.initAsIcon(pix);
    } else if (kind == MimeTypeUtils::KIND_DIR) {
        // Special case for folders because ThumbnailProvider does not return a
        // thumbnail if there is no images
        thumbnail.mWaitingForThumbnail = false;
        return;
    } else {
        thumbnail.initAsIcon(QIcon::fromTheme(QStringLiteral("image-missing")).pixmap(48));
        thumbnail.mFullSize = thumbnail.mGroupPix.size();
    }
    update(thumbnail.mIndex);
}

QPixmap ThumbnailView::thumbnailForIndex(const QModelIndex &index, QSize *fullSize)
{
    KFileItem item = fileItemForIndex(index);
    if (item.isNull()) {
        LOG("Invalid item");
        if (fullSize) {
            *fullSize = QSize();
        }
        return {};
    }
    QUrl url = item.url();

    // Find or create Thumbnail instance
    ThumbnailForUrl::Iterator it = d->mThumbnailForUrl.find(url);
    if (it == d->mThumbnailForUrl.end()) {
        Thumbnail thumbnail = Thumbnail(QPersistentModelIndex(index), item.time(KFileItem::ModificationTime));
        it = d->mThumbnailForUrl.insert(url, thumbnail);
    }
    Thumbnail &thumbnail = it.value();

    // If dir or archive, generate a thumbnail from fileitem pixmap
    MimeTypeUtils::Kind kind = MimeTypeUtils::fileItemKind(item);
    if (kind == MimeTypeUtils::KIND_ARCHIVE || kind == MimeTypeUtils::KIND_DIR) {
        int groupSize = ThumbnailGroup::pixelSize(ThumbnailGroup::fromPixelSize(d->mThumbnailSize.height()));
        if (thumbnail.mGroupPix.isNull() || thumbnail.mGroupPix.height() < groupSize) {
            const QPixmap pix = QIcon::fromTheme(item.iconName()).pixmap(d->mThumbnailSize.height());

            thumbnail.initAsIcon(pix);
            if (kind == MimeTypeUtils::KIND_ARCHIVE) {
                // No thumbnails for archives
                thumbnail.mWaitingForThumbnail = false;
            } else if (!d->mCreateThumbnailsForRemoteUrls && !UrlUtils::urlIsFastLocalFile(url)) {
                // If we don't want thumbnails for remote urls, use
                // "folder-remote" icon for remote folders, so that they do
                // not look like regular folders
                thumbnail.mWaitingForThumbnail = false;
                thumbnail.initAsIcon(QIcon::fromTheme(QStringLiteral("folder-remote")).pixmap(groupSize));
            } else {
                // set mWaitingForThumbnail to true (necessary in the case
                // 'thumbnail' already existed before, but with a too small
                // mGroupPix)
                thumbnail.mWaitingForThumbnail = true;
            }
        }
    }

    if (thumbnail.mGroupPix.isNull()) {
        if (fullSize) {
            *fullSize = QSize();
        }
        return d->mWaitingThumbnail;
    }

    // Adjust thumbnail
    if (thumbnail.mAdjustedPix.isNull()) {
        d->roughAdjustThumbnail(&thumbnail);
    }
    if (GwenviewConfig::lowResourceUsageMode() && thumbnail.mRough && !d->mSmoothThumbnailQueue.contains(url)) {
        d->mSmoothThumbnailQueue.enqueue(url);
        if (!d->mSmoothThumbnailTimer.isActive()) {
            d->mSmoothThumbnailTimer.start(SMOOTH_DELAY);
        }
    }
    if (fullSize) {
        *fullSize = thumbnail.mRealFullSize;
    }
    thumbnail.mAdjustedPix.setDevicePixelRatio(devicePixelRatioF());
    return thumbnail.mAdjustedPix;
}

bool ThumbnailView::isModified(const QModelIndex &index) const
{
    if (!d->mDocumentInfoProvider) {
        return false;
    }
    QUrl url = urlForIndex(index);
    return d->mDocumentInfoProvider->isModified(url);
}

bool ThumbnailView::isBusy(const QModelIndex &index) const
{
    if (!d->mDocumentInfoProvider) {
        return false;
    }
    QUrl url = urlForIndex(index);
    return d->mDocumentInfoProvider->isBusy(url);
}

void ThumbnailView::startDrag(Qt::DropActions)
{
    const QModelIndexList indexes = selectionModel()->selectedIndexes();
    if (indexes.isEmpty()) {
        return;
    }

    KFileItemList selectedFiles;
    for (const auto &index : indexes) {
        selectedFiles << fileItemForIndex(index);
    }

    auto drag = new QDrag(this);
    auto *mimeData = MimeTypeUtils::selectionMimeData(selectedFiles, MimeTypeUtils::DropTarget);
    KUrlMimeData::exportUrlsToPortal(mimeData);
    drag->setMimeData(mimeData);
    d->initDragPixmap(drag, indexes);
    drag->exec(Qt::MoveAction | Qt::CopyAction | Qt::LinkAction, Qt::CopyAction);
}

void ThumbnailView::setZoomParameter()
{
    const qreal sensitivityModifier = 0.25;
    d->mTouch->setZoomParameter(sensitivityModifier, thumbnailSize().width());
}

void ThumbnailView::zoomGesture(qreal newZoom, const QPoint &)
{
    if (newZoom >= 0.0) {
        int width = qBound(int(MinThumbnailSize), static_cast<int>(newZoom), int(MaxThumbnailSize));
        setThumbnailWidth(width);
    }
}

void ThumbnailView::tapGesture(const QPoint &pos)
{
    const QRect rect = QRect(pos, QSize(1, 1));
    setSelection(rect, QItemSelectionModel::ClearAndSelect);
    Q_EMIT activated(indexAt(pos));
}

void ThumbnailView::startDragFromTouch(const QPoint &pos)
{
    QModelIndex index = indexAt(pos);
    if (index.isValid()) {
        setCurrentIndex(index);
        d->mScroller->stop();
        startDrag(Qt::CopyAction);
    }
}

void ThumbnailView::dragEnterEvent(QDragEnterEvent *event)
{
    QAbstractItemView::dragEnterEvent(event);
    if (event->mimeData()->hasUrls()) {
        event->acceptProposedAction();
    }
}

void ThumbnailView::dragMoveEvent(QDragMoveEvent *event)
{
    // Necessary, otherwise we don't reach dropEvent()
    QAbstractItemView::dragMoveEvent(event);
    event->acceptProposedAction();
}

void ThumbnailView::dropEvent(QDropEvent *event)
{
    const QList<QUrl> urlList = KUrlMimeData::urlsFromMimeData(event->mimeData());
    if (urlList.isEmpty()) {
        return;
    }

    QModelIndex destIndex = indexAt(event->pos());
    if (destIndex.isValid()) {
        KFileItem item = fileItemForIndex(destIndex);
        if (item.isDir()) {
            QUrl destUrl = item.url();
            d->mThumbnailViewHelper->showMenuForUrlDroppedOnDir(this, urlList, destUrl);
            return;
        }
    }

    d->mThumbnailViewHelper->showMenuForUrlDroppedOnViewport(this, urlList);

    event->acceptProposedAction();
}

void ThumbnailView::keyPressEvent(QKeyEvent *event)
{
    if (event->key() == Qt::Key_Return) {
        const QModelIndex index = selectionModel()->currentIndex();
        if (index.isValid() && selectionModel()->selectedIndexes().count() == 1) {
            Q_EMIT indexActivated(index);
        }
    } else if (event->key() == Qt::Key_Left && event->modifiers() == Qt::NoModifier) {
        if (flow() == LeftToRight && QApplication::isRightToLeft()) {
            setCurrentIndex(moveCursor(QAbstractItemView::MoveRight, Qt::NoModifier));
        } else {
            setCurrentIndex(moveCursor(QAbstractItemView::MoveLeft, Qt::NoModifier));
        }
        return;
    } else if (event->key() == Qt::Key_Right && event->modifiers() == Qt::NoModifier) {
        if (flow() == LeftToRight && QApplication::isRightToLeft()) {
            setCurrentIndex(moveCursor(QAbstractItemView::MoveLeft, Qt::NoModifier));
        } else {
            setCurrentIndex(moveCursor(QAbstractItemView::MoveRight, Qt::NoModifier));
        }
        return;
    }

    QListView::keyPressEvent(event);
}

void ThumbnailView::resizeEvent(QResizeEvent *event)
{
    QListView::resizeEvent(event);
    d->scheduleThumbnailGeneration();
}

void ThumbnailView::showEvent(QShowEvent *event)
{
    d->mWindowScaleWatcher.setWindow(window()->windowHandle());

    QListView::showEvent(event);
    d->scheduleThumbnailGeneration();
    QTimer::singleShot(0, this, &ThumbnailView::scrollToSelectedIndex);
}

void ThumbnailView::wheelEvent(QWheelEvent *event)
{
    // If we don't adjust the single step, the wheel scroll exactly one item up
    // and down, giving the impression that the items do not move but only
    // their label changes.
    // For some reason it is necessary to set the step here: setting it in
    // setThumbnailSize() does not work
    // verticalScrollBar()->setSingleStep(d->mThumbnailSize / 5);
    if (event->modifiers() == Qt::ControlModifier) {
        int width = thumbnailSize().width() + (event->angleDelta().y() > 0 ? 1 : -1) * WHEEL_ZOOM_MULTIPLIER;
        width = qMax(int(MinThumbnailSize), qMin(width, int(MaxThumbnailSize)));
        setThumbnailWidth(width);
    } else {
        QListView::wheelEvent(event);
    }
}

void ThumbnailView::mousePressEvent(QMouseEvent *event)
{
    switch (event->button()) {
    case Qt::ForwardButton:
    case Qt::BackButton:
        return;
    default:
        QListView::mousePressEvent(event);
    }
}

void ThumbnailView::scrollToSelectedIndex()
{
    QModelIndexList list = selectedIndexes();
    if (list.count() >= 1) {
        scrollTo(list.first(), PositionAtCenter);
    }
}

void ThumbnailView::selectionChanged(const QItemSelection &selected, const QItemSelection &deselected)
{
    QListView::selectionChanged(selected, deselected);
    Q_EMIT selectionChangedSignal(selected, deselected);
}

void ThumbnailView::scrollContentsBy(int dx, int dy)
{
    QListView::scrollContentsBy(dx, dy);
    d->scheduleThumbnailGeneration();
}

void ThumbnailView::generateThumbnailsForItems()
{
    if (!isVisible() || !model() || d->loading) {
        return;
    }
    const QRect visibleRect = viewport()->rect();
    const int visibleSurface = visibleRect.width() * visibleRect.height();
    const QPoint origin = visibleRect.center();
    // Keep thumbnails around that are at most two "screen heights" away.
    const int discardDistance = visibleRect.bottomRight().manhattanLength() * 2;

    // distance => item
    QMultiMap<int, KFileItem> itemMap;

    for (int row = 0; row < model()->rowCount(); ++row) {
        QModelIndex index = model()->index(row, 0);
        KFileItem item = fileItemForIndex(index);
        QUrl url = item.url();

        // Filter out remote items if necessary
        if (!d->mCreateThumbnailsForRemoteUrls && !url.isLocalFile()) {
            continue;
        }

        // Filter out archives
        MimeTypeUtils::Kind kind = MimeTypeUtils::fileItemKind(item);
        if (kind == MimeTypeUtils::KIND_ARCHIVE) {
            continue;
        }

        // Immediately update modified items
        if (d->mDocumentInfoProvider && d->mDocumentInfoProvider->isModified(url)) {
            d->updateThumbnailForModifiedDocument(index);
            continue;
        }

        ThumbnailForUrl::ConstIterator it = d->mThumbnailForUrl.constFind(url);

        // Compute distance
        int distance;
        const QRect itemRect = visualRect(index);
        if (itemRect.intersected(visibleRect).isValid()) {
            // Item is visible, order thumbnails from left to right, top to bottom
            // Distance is computed so that it is between 0 and visibleSurface
            distance = itemRect.top() * visibleRect.width() + itemRect.left();
            // Make sure directory thumbnails are generated after image thumbnails:
            // Distance is between visibleSurface and 2 * visibleSurface
            if (kind == MimeTypeUtils::KIND_DIR) {
                distance = distance + visibleSurface;
            }
        } else {
            // Calculate how far away the thumbnail is to determine if it could
            // become visible soon.
            qreal itemDistance = (itemRect.center() - origin).manhattanLength();

            if (itemDistance < discardDistance) {
                // Item is not visible but within an area that may potentially
                // become visible soon, order thumbnails according to distance
                // Start at 2 * visibleSurface to ensure invisible thumbnails are
                // generated *after* visible thumbnails
                distance = 2 * visibleSurface + itemDistance;
            } else {
                // Discard thumbnails that are too far away to prevent large
                // directories from consuming massive amounts of RAM.
                if (it != d->mThumbnailForUrl.constEnd()) {
                    // Thumbnail exists for this item, discard it.
                    const QUrl url = item.url();
                    d->mThumbnailForUrl.remove(url);
                    d->mSmoothThumbnailQueue.removeAll(url);
                    d->mThumbnailProvider->removeItems({item});
                }
                continue;
            }
        }

        // Filter out items which already have a thumbnail
        if (it != d->mThumbnailForUrl.constEnd() && it.value().isGroupPixAdaptedForSize(d->mThumbnailSize.height())) {
            continue;
        }

        // Add the item to our map
        itemMap.insert(distance, item);

        // Insert the thumbnail in mThumbnailForUrl, so that
        // setThumbnail() can find the item to update
        if (it == d->mThumbnailForUrl.constEnd()) {
            Thumbnail thumbnail = Thumbnail(QPersistentModelIndex(index), item.time(KFileItem::ModificationTime));
            d->mThumbnailForUrl.insert(url, thumbnail);
        }
    }

    if (!itemMap.isEmpty()) {
        d->appendItemsToThumbnailProvider(itemMap.values());
    }
}

void ThumbnailView::updateThumbnail(const QUrl &url)
{
    const ThumbnailForUrl::Iterator it = d->mThumbnailForUrl.find(url);
    if (it == d->mThumbnailForUrl.end()) {
        return;
    }

    if (d->mDocumentInfoProvider) {
        d->updateThumbnailForModifiedDocument(it->mIndex);
    } else {
        const KFileItem item = fileItemForIndex(it->mIndex);
        d->appendItemsToThumbnailProvider(KFileItemList({item}));
    }
}

void ThumbnailView::updateThumbnailBusyState(const QUrl &url, bool busy)
{
    const ThumbnailForUrl::Iterator it = d->mThumbnailForUrl.find(url);
    if (it == d->mThumbnailForUrl.end()) {
        return;
    }

    QPersistentModelIndex index(it->mIndex);
    if (busy && !d->mBusyIndexSet.contains(index)) {
        d->mBusyIndexSet << index;
        update(index);
        if (d->mBusyAnimationTimeLine->state() != QTimeLine::Running) {
            d->mBusyAnimationTimeLine->start();
        }
    } else if (!busy && d->mBusyIndexSet.remove(index)) {
        update(index);
        if (d->mBusyIndexSet.isEmpty()) {
            d->mBusyAnimationTimeLine->stop();
        }
    }
}

void ThumbnailView::updateBusyIndexes()
{
    for (const QPersistentModelIndex &index : qAsConst(d->mBusyIndexSet)) {
        update(index);
    }
}

QPixmap ThumbnailView::busySequenceCurrentPixmap() const
{
    return d->mBusySequence.frameAt(d->mBusyAnimationTimeLine->currentFrame());
}

void ThumbnailView::smoothNextThumbnail()
{
    if (d->mSmoothThumbnailQueue.isEmpty()) {
        return;
    }

    if (d->mThumbnailProvider && d->mThumbnailProvider->isRunning()) {
        // give mThumbnailProvider priority over smoothing
        d->mSmoothThumbnailTimer.start(SMOOTH_DELAY);
        return;
    }

    QUrl url = d->mSmoothThumbnailQueue.dequeue();
    ThumbnailForUrl::Iterator it = d->mThumbnailForUrl.find(url);
    GV_RETURN_IF_FAIL2(it != d->mThumbnailForUrl.end(), url << "not in mThumbnailForUrl.");

    Thumbnail &thumbnail = it.value();
    thumbnail.mAdjustedPix = d->scale(thumbnail.mGroupPix, Qt::SmoothTransformation);
    thumbnail.mRough = false;

    GV_RETURN_IF_FAIL2(thumbnail.mIndex.isValid(), "index for" << url << "is invalid.");
    update(thumbnail.mIndex);

    if (!d->mSmoothThumbnailQueue.isEmpty()) {
        d->mSmoothThumbnailTimer.start(0);
    }
}

void ThumbnailView::reloadThumbnail(const QModelIndex &index)
{
    QUrl url = urlForIndex(index);
    if (!url.isValid()) {
        qCWarning(GWENVIEW_LIB_LOG) << "Invalid url for index" << index;
        return;
    }
    ThumbnailProvider::deleteImageThumbnail(url);
    ThumbnailForUrl::Iterator it = d->mThumbnailForUrl.find(url);
    if (it == d->mThumbnailForUrl.end()) {
        return;
    }
    d->mThumbnailForUrl.erase(it);
    generateThumbnailsForItems();
}

void ThumbnailView::setCreateThumbnailsForRemoteUrls(bool createRemoteThumbs)
{
    d->mCreateThumbnailsForRemoteUrls = createRemoteThumbs;
}

} // namespace

#include "thumbnailview.moc"
