/*
 *  SPDX-FileCopyrightText: 2011 Marco Martin <mart@kde.org>
 *  SPDX-FileCopyrightText: 2014 Aleix Pol Gonzalez <aleixpol@blue-systems.com>
 *
 *  SPDX-License-Identifier: LGPL-2.0-or-later
 */

#include "icon.h"
#include "libkirigami/platformtheme.h"
#include "scenegraph/managedtexturenode.h"

#include <QSGSimpleTextureNode>
#include <QQuickWindow>
#include <QIcon>
#include <QBitmap>
#include <QSGTexture>
#include <QDebug>
#include <QSharedPointer>
#include <QtQml>
#include <QQuickImageProvider>
#include <QGuiApplication>
#include <QPainter>
#include <QScreen>



Q_GLOBAL_STATIC(ImageTexturesCache, s_iconImageCache)

Icon::Icon(QQuickItem *parent)
    : QQuickItem(parent),
      m_changed(false),
      m_active(false),
      m_selected(false),
      m_isMask(false)
{
    setFlag(ItemHasContents, true);
    // Using 32 because Icon used to redefine implicitWidth and implicitHeight and hardcode them to 32
    setImplicitSize(32, 32);
    //FIXME: not necessary anymore
    connect(qApp, &QGuiApplication::paletteChanged, this, &QQuickItem::polish);
    connect(this, &QQuickItem::enabledChanged, this, &QQuickItem::polish);
    connect(this, &QQuickItem::smoothChanged, this, &QQuickItem::polish);
}


Icon::~Icon()
{
}

void Icon::setSource(const QVariant &icon)
{
    if (m_source == icon) {
        return;
    }
    m_source = icon;
    m_monochromeHeuristics.clear();

    if (!m_theme) {
        m_theme = static_cast<Kirigami::PlatformTheme *>(qmlAttachedPropertiesObject<Kirigami::PlatformTheme>(this, true));
        Q_ASSERT(m_theme);

        connect(m_theme, &Kirigami::PlatformTheme::colorsChanged, this, &QQuickItem::polish);
    }

    if (icon.type() == QVariant::String) {
        const QString iconSource = icon.toString();
        m_isMaskHeuristic = (iconSource.endsWith(QLatin1String("-symbolic"))
                            || iconSource.endsWith(QLatin1String("-symbolic-rtl"))
                            || iconSource.endsWith(QLatin1String("-symbolic-ltr")));
        emit isMaskChanged();
    }

    if (m_networkReply) {
        //if there was a network query going on, interrupt it
        m_networkReply->close();
    }
    m_loadedImage = QImage();
    setStatus(Loading);

    polish();
    emit sourceChanged();
    emit validChanged();
}

QVariant Icon::source() const
{
    return m_source;
}

void Icon::setActive(const bool active)
{
    if (active == m_active) {
        return;
    }
    m_active = active;
    polish();
    emit activeChanged();
}

bool Icon::active() const
{
    return m_active;
}

bool Icon::valid() const
{
    // TODO: should this be return m_status == Ready?
    // Consider an empty URL invalid, even though isNull() will say false
    if (m_source.canConvert<QUrl>() && m_source.toUrl().isEmpty()) {
        return false;
    }

    return !m_source.isNull();
}

void Icon::setSelected(const bool selected)
{
    if (selected == m_selected) {
        return;
    }
    m_selected = selected;
    polish();
    emit selectedChanged();
}

bool Icon::selected() const
{
    return m_selected;
}

void Icon::setIsMask(bool mask)
{
    if (m_isMask == mask) {
        return;
    }

    m_isMask = mask;
    m_isMaskHeuristic = mask;
    polish();
    emit isMaskChanged();
}

bool Icon::isMask() const
{
    return m_isMask || m_isMaskHeuristic;
}

void Icon::setColor(const QColor &color)
{
    if (m_color == color) {
        return;
    }

    m_color = color;
    polish();
    emit colorChanged();
}

QColor Icon::color() const
{
    return m_color;
}

QSGNode* Icon::updatePaintNode(QSGNode* node, QQuickItem::UpdatePaintNodeData* /*data*/)
{
    if (m_source.isNull() || qFuzzyIsNull(width()) || qFuzzyIsNull(height())) {
        delete node;
        return Q_NULLPTR;
    }

    if (m_changed || node == nullptr) {
        const QSize itemSize(width(), height());
        QRect nodeRect(QPoint(0,0), itemSize);

        ManagedTextureNode* mNode = dynamic_cast<ManagedTextureNode*>(node);
        if (!mNode) {
            delete node;
            mNode = new ManagedTextureNode;
        }
        if (itemSize.width() != 0 && itemSize.height() != 0) {
            const auto multiplier = QCoreApplication::instance()->testAttribute(Qt::AA_UseHighDpiPixmaps) ? 1 : (window() ? window()->devicePixelRatio() : qGuiApp->devicePixelRatio());
            const QSize size = itemSize * multiplier;
            mNode->setTexture(s_iconImageCache->loadTexture(window(), m_icon));
            if (m_icon.size() != size) {
                // At this point, the image will already be scaled, but we need to output it in
                // the correct aspect ratio, painted centered in the viewport. So:
                QRect destination(QPoint(0, 0), m_icon.size().scaled(itemSize, Qt::KeepAspectRatio));
                destination.moveCenter(nodeRect.center());
                nodeRect = destination;
            }
        }
        mNode->setRect(nodeRect);
        node = mNode;
        if (smooth()) {
            mNode->setFiltering(QSGTexture::Linear);
        }
        m_changed = false;
    }

    return node;
}

void Icon::geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry)
{
    QQuickItem::geometryChanged(newGeometry, oldGeometry);
    if (newGeometry.size() != oldGeometry.size()) {
        polish();
    }
}

void Icon::handleRedirect(QNetworkReply* reply)
{
    QNetworkAccessManager* qnam = reply->manager();
    if (reply->error() != QNetworkReply::NoError) {
        return;
    }
    const QUrl possibleRedirectUrl = reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl();
    if (!possibleRedirectUrl.isEmpty()) {
        const QUrl redirectUrl = reply->url().resolved(possibleRedirectUrl);
        if (redirectUrl == reply->url()) {
            // no infinite redirections thank you very much
            reply->deleteLater();
            return;
        }
        reply->deleteLater();
        QNetworkRequest request(possibleRedirectUrl);
        request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache);
        m_networkReply = qnam->get(request);
        connect(m_networkReply.data(), &QNetworkReply::finished, this, [this](){
            if (m_networkReply)
                handleFinished(m_networkReply);
        });
    }
}

void Icon::handleFinished(QNetworkReply* reply)
{
    reply->deleteLater();
    if (!reply->attribute(QNetworkRequest::RedirectionTargetAttribute).isNull()) {
        handleRedirect(reply);
        return;
    }

    m_loadedImage = QImage();

    const QString filename = reply->url().fileName();
    if (!m_loadedImage.load(reply, filename.mid(filename.indexOf(QLatin1Char('.'))).toLatin1().constData())) {
        qWarning() << "received broken image" << reply->url();

        // broken image from data, inform the user of this with some useful broken-image thing...
        m_loadedImage = QIcon::fromTheme(m_fallback).pixmap(window(), QSize(width(), height()), iconMode(), QIcon::On).toImage();
    }
    polish();
}

void Icon::updatePolish()
{
    QQuickItem::updatePolish();

    if (m_source.isNull()) {
        setStatus(Ready);
        updatePaintedGeometry();
        update();
        return;
    }

    const QSize itemSize(width(), height());
    if (itemSize.width() != 0 && itemSize.height() != 0) {
        const auto multiplier = QCoreApplication::instance()->testAttribute(Qt::AA_UseHighDpiPixmaps) ? 1 : (window() ? window()->devicePixelRatio() : qGuiApp->devicePixelRatio());
        const QSize size = itemSize * multiplier;

        switch(m_source.type()){
        case QVariant::Pixmap:
            m_icon = m_source.value<QPixmap>().toImage();
            break;
        case QVariant::Image:
            m_icon = m_source.value<QImage>();
            break;
        case QVariant::Bitmap:
            m_icon = m_source.value<QBitmap>().toImage();
            break;
        case QVariant::Icon:
            m_icon = m_source.value<QIcon>().pixmap(window(), itemSize, iconMode(), QIcon::On).toImage();
            break;
        case QVariant::Url:
        case QVariant::String:
            m_icon = findIcon(size);
            break;
        case QVariant::Brush:
            //todo: fill here too?
        case QVariant::Color:
            m_icon = QImage(size, QImage::Format_Alpha8);
            m_icon.fill(m_source.value<QColor>());
            break;
        default:
            break;
        }

        if (m_icon.isNull()){
            m_icon = QImage(size, QImage::Format_Alpha8);
            m_icon.fill(Qt::transparent);
        }

        const QColor tintColor = !m_color.isValid() || m_color == Qt::transparent ? (m_selected ? m_theme->highlightedTextColor() : m_theme->textColor()) : m_color;

        //TODO: initialize m_isMask with icon.isMask()
        if (tintColor.alpha() > 0 && (isMask() || guessMonochrome(m_icon))) {
            QPainter p(&m_icon);
            p.setCompositionMode(QPainter::CompositionMode_SourceIn);
            p.fillRect(m_icon.rect(), tintColor);
            p.end();
        }
    }
    m_changed = true;
    updatePaintedGeometry();
    update();
}

QImage Icon::findIcon(const QSize &size)
{
    QImage img;
    QString iconSource = m_source.toString();

    if (iconSource.startsWith(QLatin1String("image://"))) {
        const auto multiplier = QCoreApplication::instance()->testAttribute(Qt::AA_UseHighDpiPixmaps) ? (window() ? window()->devicePixelRatio() : qGuiApp->devicePixelRatio()) : 1;
        QUrl iconUrl(iconSource);
        QString iconProviderId = iconUrl.host();
        QString iconId = iconUrl.path();

        // QRC paths are not correctly handled by .path()
        if (iconId.size() >=2 && iconId.startsWith(QLatin1String("/:"))) {
            iconId.remove(0, 1);
        }

        QSize actualSize;
        QQuickImageProvider* imageProvider = dynamic_cast<QQuickImageProvider*>(
                    qmlEngine(this)->imageProvider(iconProviderId));
        if (!imageProvider)
            return img;
        switch(imageProvider->imageType()){
        case QQmlImageProviderBase::Image:
            img = imageProvider->requestImage(iconId, &actualSize, size * multiplier);
            break;
        case QQmlImageProviderBase::Pixmap:
            img = imageProvider->requestPixmap(iconId, &actualSize, size * multiplier).toImage();
            break;
        case QQmlImageProviderBase::ImageResponse:
        {
            if (!m_loadedImage.isNull()) {
                setStatus(Ready);
                return m_loadedImage.scaled(size, Qt::KeepAspectRatio, smooth() ? Qt::SmoothTransformation : Qt::FastTransformation );
            }
            QQuickAsyncImageProvider *provider = dynamic_cast<QQuickAsyncImageProvider*>(imageProvider);
            auto response = provider->requestImageResponse(iconId, size * multiplier);
            connect(response, &QQuickImageResponse::finished, this, [iconId, response, this](){
                if (response->errorString().isEmpty()) {
                    QQuickTextureFactory *textureFactory = response->textureFactory();
                    if (textureFactory) {
                        m_loadedImage = textureFactory->image();
                    }
                    if (m_loadedImage.isNull()) {
                        // broken image from data, inform the user of this with some useful broken-image thing...
                        m_loadedImage = QIcon::fromTheme(m_fallback).pixmap(window(), QSize(width(), height()), iconMode(), QIcon::On).toImage();
                        setStatus(Error);
                    } else {
                        setStatus(Ready);
                    }
                    polish();
                }
            });
            // Temporary icon while we wait for the real image to load...
            img = QIcon::fromTheme(m_placeholder).pixmap(window(), size, iconMode(), QIcon::On).toImage();
            break;
        }
        case QQmlImageProviderBase::Texture:
        {
            QQuickTextureFactory *textureFactory = imageProvider->requestTexture(iconId, &actualSize, size * multiplier);
            if (textureFactory) {
                img = textureFactory->image();
            }
            if (img.isNull()) {
                // broken image from data, or the texture factory wasn't healthy, inform the user of this with some useful broken-image thing...
                img = QIcon::fromTheme(m_fallback).pixmap(window(), QSize(width(), height()), iconMode(), QIcon::On).toImage();
                setStatus(Error);
            } else {
                setStatus(Ready);
            }
            break;
        }
        case QQmlImageProviderBase::Invalid:
            //will have to investigate this more
            setStatus(Error);
            break;
        }
    } else if(iconSource.startsWith(QLatin1String("http://")) || iconSource.startsWith(QLatin1String("https://"))) {
        if(!m_loadedImage.isNull()) {
            setStatus(Ready);
            return m_loadedImage.scaled(size, Qt::KeepAspectRatio, smooth() ? Qt::SmoothTransformation : Qt::FastTransformation );
        }
        const auto url = m_source.toUrl();
        QQmlEngine* engine = qmlEngine(this);
        QNetworkAccessManager* qnam;
        if (engine && (qnam = engine->networkAccessManager()) && (!m_networkReply || m_networkReply->url() != url)) {
            QNetworkRequest request(url);
            request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache);
            m_networkReply = qnam->get(request);
            connect(m_networkReply.data(), &QNetworkReply::finished, this, [this](){
                if (m_networkReply)
                    handleFinished(m_networkReply);
            });
        }
        // Temporary icon while we wait for the real image to load...
        img = QIcon::fromTheme(m_placeholder).pixmap(window(), size, iconMode(), QIcon::On).toImage();
    } else {
        if (iconSource.startsWith(QLatin1String("qrc:/"))) {
            iconSource = iconSource.mid(3);
        } else if (iconSource.startsWith(QLatin1String("file:/"))) {
            iconSource = QUrl(iconSource).path();
        }

        QIcon icon;
        const bool isPath = iconSource.contains(QLatin1String("/"));
        if (isPath) {
            icon = QIcon(iconSource);
        } else {
            if (icon.isNull()) {
                icon = m_theme->iconFromTheme(iconSource, m_color);
            }
        }
        if (!icon.isNull()) {
            img = icon.pixmap(window(), size, iconMode(), QIcon::On).toImage();

            setStatus(Ready);
            /*const QColor tintColor = !m_color.isValid() || m_color == Qt::transparent ? (m_selected ? m_theme->highlightedTextColor() : m_theme->textColor()) : m_color;

            if (m_isMask || icon.isMask() || iconSource.endsWith(QLatin1String("-symbolic")) || iconSource.endsWith(QLatin1String("-symbolic-rtl")) || iconSource.endsWith(QLatin1String("-symbolic-ltr")) || guessMonochrome(img)) {
                QPainter p(&img);
                p.setCompositionMode(QPainter::CompositionMode_SourceIn);
                p.fillRect(img.rect(), tintColor);
                p.end();
            }*/
        }
    }

    if (!iconSource.isEmpty() && img.isNull()) {
        setStatus(Error);
        img = QIcon::fromTheme(m_fallback).pixmap(window(), size, iconMode(), QIcon::On).toImage();
    }
    return img;
}

QIcon::Mode Icon::iconMode() const
{
    if (!isEnabled()) {
        return QIcon::Disabled;
    } else if (m_selected) {
        return QIcon::Selected;
    } else if (m_active) {
        return QIcon::Active;
    }
    return QIcon::Normal;
}

bool Icon::guessMonochrome(const QImage &img)
{
    //don't try for too big images
    if (img.width() >= 256 || m_theme->supportsIconColoring()) {
        return false;
    }
    // round size to a standard size. hardcode as we can't use KIconLoader
    int stdSize;
    if (img.width() <= 16) {
        stdSize = 16;
    } else if (img.width() <= 22) {
        stdSize = 22;
    } else if (img.width() <= 24) {
        stdSize = 24;
    } else if (img.width() <= 32) {
        stdSize = 32;
    } else if (img.width() <= 48) {
        stdSize = 48;
    } else if (img.width() <= 64) {
        stdSize = 64;
    } else {
        stdSize = 128;
    }

    auto findIt = m_monochromeHeuristics.constFind(stdSize);
    if (findIt != m_monochromeHeuristics.constEnd()) {
        return findIt.value();
    }

    QHash<int, int> dist;
    int transparentPixels = 0;
    int saturatedPixels = 0;
    for(int x=0; x < img.width(); x++) {
        for(int y=0; y < img.height(); y++) {
            QColor color = QColor::fromRgba(qUnpremultiply(img.pixel(x, y)));
            if (color.alpha() < 100) {
                ++transparentPixels;
                continue;
            } else if (color.saturation() > 84) {
                ++saturatedPixels;
            }
            dist[qGray(color.rgb())]++;
        }
    }

    QMultiMap<int, int> reverseDist;
    auto it = dist.constBegin();
    qreal entropy = 0;
    while (it != dist.constEnd()) {
        reverseDist.insert(it.value(), it.key());
        qreal probability = qreal(it.value()) / qreal(img.size().width() * img.size().height() - transparentPixels);
        entropy -= probability * log(probability) / log(255);
        ++it;
    }

    // Arbitrarly low values of entropy and colored pixels
    m_monochromeHeuristics[stdSize] = saturatedPixels <= (img.size().width()*img.size().height() - transparentPixels) * 0.3 && entropy <= 0.3;
    return m_monochromeHeuristics[stdSize];
}

QString Icon::fallback() const
{
    return m_fallback;
}

void Icon::setFallback(const QString& fallback)
{
    if (m_fallback != fallback) {
        m_fallback = fallback;
        Q_EMIT fallbackChanged(fallback);
    }
}

QString Icon::placeholder() const
{
    return m_placeholder;
}

void Icon::setPlaceholder(const QString& placeholder)
{
    if (m_placeholder != placeholder) {
        m_placeholder = placeholder;
        Q_EMIT placeholderChanged(placeholder);
    }
}

void Icon::setStatus(Status status)
{
    if (status == m_status) {
        return;
    }

    m_status = status;
    emit statusChanged();
}

Icon::Status Icon::status() const
{
    return m_status;
}

qreal Icon::paintedWidth() const
{
    return m_paintedWidth;
}

qreal Icon::paintedHeight() const
{
    return m_paintedHeight;
}

void Icon::updatePaintedGeometry()
{
    qreal newWidth = 0.0;
    qreal newHeight = 0.0;
    if (!m_icon.width() || !m_icon.height()) {
        newWidth = newHeight = 0.0;
    } else {
        const qreal w = widthValid() ? width() : m_icon.width();
        const qreal widthScale = w / m_icon.width();
        const qreal h = heightValid() ? height() : m_icon.height();
        const qreal heightScale = h / m_icon.height();
        if (widthScale <= heightScale) {
            newWidth = w;
            newHeight = widthScale * m_icon.height();
        } else if (heightScale < widthScale) {
            newWidth = heightScale * m_icon.width();
            newHeight = h;
        }
    }
    if (newWidth != m_paintedWidth || newHeight != m_paintedHeight) {
        m_paintedWidth = newWidth;
        m_paintedHeight = newHeight;
        Q_EMIT paintedAreaChanged();
    }
}

#include "moc_icon.cpp"
