// Copyright (C) 2019 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only

#include "qsgplaintexture_p.h"
#include <QtQuick/private/qsgcontext_p.h>
#include <qmath.h>
#include <private/qquickprofiler_p.h>
#include <private/qqmlglobal_p.h>
#include <QtGui/qguiapplication.h>
#include <QtGui/qpa/qplatformnativeinterface.h>
#include <rhi/qrhi.h>
#include <QtQuick/private/qsgrhisupport_p.h>

#include <qtquick_tracepoints_p.h>

QT_BEGIN_NAMESPACE

QSGPlainTexture::QSGPlainTexture()
    : QSGTexture(*(new QSGPlainTexturePrivate(this)))
    , m_texture(nullptr)
    , m_has_alpha(false)
    , m_dirty_texture(false)
    , m_owns_texture(true)
    , m_mipmaps_generated(false)
    , m_retain_image(false)
    , m_mipmap_warned(false)
{
}

QSGPlainTexture::QSGPlainTexture(QSGPlainTexturePrivate &dd)
    : QSGTexture(dd)
    , m_texture(nullptr)
    , m_has_alpha(false)
    , m_dirty_texture(false)
    , m_owns_texture(true)
    , m_mipmaps_generated(false)
    , m_retain_image(false)
    , m_mipmap_warned(false)
{
}

QSGPlainTexture::~QSGPlainTexture()
{
    if (m_texture && m_owns_texture)
        delete m_texture;
}

void QSGPlainTexture::setImage(const QImage &image)
{
    m_image = image;
    m_texture_size = image.size();
    m_has_alpha = image.hasAlphaChannel();
    m_dirty_texture = true;
    m_mipmaps_generated = false;
}

void QSGPlainTexture::setTexture(QRhiTexture *texture) // RHI only
{
    if (m_texture && m_owns_texture && m_texture != texture)
        delete m_texture;

    m_texture = texture;
    m_dirty_texture = false;
    m_image = QImage();
    m_mipmaps_generated = false;
}

void QSGPlainTexture::setTextureFromNativeTexture(QRhi *rhi,
                                                  quint64 nativeObjectHandle,
                                                  int nativeLayoutOrState,
                                                  uint nativeFormat,
                                                  const QSize &size,
                                                  QQuickWindow::CreateTextureOptions options,
                                                  QQuickWindowPrivate::TextureFromNativeTextureFlags flags)
{
    QRhiTexture::Flags texFlags;
    if (options.testFlag(QQuickWindow::TextureHasMipmaps))
        texFlags |= QRhiTexture::MipMapped | QRhiTexture::UsedWithGenerateMips;
    if (flags.testFlag(QQuickWindowPrivate::NativeTextureIsExternalOES))
        texFlags |= QRhiTexture::ExternalOES;

    QRhiTexture::Format format = QRhiTexture::RGBA8;

    QRhiTexture::Flags formatFlags;
    auto rhiFormat = QSGRhiSupport::instance()->toRhiTextureFormat(nativeFormat, &formatFlags);
    if (rhiFormat != QRhiTexture::UnknownFormat) {
        format = rhiFormat;
        texFlags |= formatFlags;
    }

    QRhiTexture *t = rhi->newTexture(format, size, 1, texFlags);

    // ownership of the native object is never taken
    t->createFrom({nativeObjectHandle, nativeLayoutOrState});

    setTexture(t);
}

qint64 QSGPlainTexture::comparisonKey() const
{
    if (m_texture)
        return qint64(m_texture);

    // two textures (and so materials) with not-yet-created texture underneath are never equal
    return qint64(this);
}

QRhiTexture *QSGPlainTexture::rhiTexture() const
{
    return m_texture;
}

void QSGPlainTexture::commitTextureOperations(QRhi *rhi, QRhiResourceUpdateBatch *resourceUpdates)
{
    Q_D(QSGPlainTexture);

    const bool hasMipMaps = mipmapFiltering() != QSGTexture::None;
    const bool mipmappingChanged = m_texture && ((hasMipMaps && !m_texture->flags().testFlag(QRhiTexture::MipMapped)) // did not have it before
            || (!hasMipMaps && m_texture->flags().testFlag(QRhiTexture::MipMapped))); // does not have it anymore

    if (!m_dirty_texture) {
        if (!m_texture)
            return;
        if (m_texture && !mipmappingChanged) {
            if (hasMipMaps && !m_mipmaps_generated) {
                resourceUpdates->generateMips(m_texture);
                m_mipmaps_generated = true;
            }
            return;
        }
    }

    if (m_image.isNull()) {
        if (!m_dirty_texture && mipmappingChanged) {
            // Full Mipmap Panic!
            if (!m_mipmap_warned) {
                qWarning("QSGPlainTexture: Mipmap settings changed without having image data available. "
                         "Call setImage() again or enable m_retain_image. "
                         "Falling back to previous mipmap filtering mode.");
                m_mipmap_warned = true;
            }
            // leave the texture valid and rather ignore the mipmap mode change attempt
            setMipmapFiltering(d->m_last_mipmap_filter);
            return;
        }

        if (m_texture && m_owns_texture)
            delete m_texture;

        m_texture = nullptr;
        m_texture_size = QSize();
        m_has_alpha = false;

        m_dirty_texture = false;
        return;
    }

    m_dirty_texture = false;

    QImage tmp;
    bool bgra = false;
    bool fp16 = false;
    bool fp32 = false;
    bool needsConvert8 = false;
    bool needsConvertFP16 = false;
    bool needsConvertFP32 = false;

    if (m_image.format() == QImage::Format_RGB32 || m_image.format() == QImage::Format_ARGB32_Premultiplied) {
#if Q_BYTE_ORDER == Q_LITTLE_ENDIAN
        if (rhi->isTextureFormatSupported(QRhiTexture::BGRA8)) {
            tmp = m_image;
            bgra = true;
        } else {
            needsConvert8 = true;
        }
#else
        needsConvert8 = true;
#endif
    } else if (m_image.format() == QImage::Format_RGBX16FPx4 || m_image.format() == QImage::Format_RGBA16FPx4_Premultiplied) {
#if Q_BYTE_ORDER == Q_LITTLE_ENDIAN
        if (rhi->isTextureFormatSupported(QRhiTexture::RGBA16F)) {
            tmp = m_image;
            fp16 = true;
        } else {
            needsConvert8 = true;
        }
#else
        needsConvert8 = true;
#endif
    } else if (m_image.format() == QImage::Format_RGBX32FPx4 || m_image.format() == QImage::Format_RGBA32FPx4_Premultiplied) {
#if Q_BYTE_ORDER == Q_LITTLE_ENDIAN
        if (rhi->isTextureFormatSupported(QRhiTexture::RGBA32F)) {
            tmp = m_image;
            fp32 = true;
        } else if (rhi->isTextureFormatSupported(QRhiTexture::RGBA16F))  {
            tmp = m_image;
            fp16 = true;
        } else {
            needsConvert8 = true;
        }
#else
        needsConvert8 = true;
#endif
    } else if (m_image.format() == QImage::Format_RGBX8888 || m_image.format() == QImage::Format_RGBA8888_Premultiplied) {
        tmp = m_image;
    } else if (m_image.format() == QImage::Format_RGBA16FPx4) {
        needsConvertFP16 = true;
    } else if (m_image.format() == QImage::Format_RGBA32FPx4) {
        needsConvertFP32 = true;
    } else {
        needsConvert8 = true;
    }

    if (needsConvertFP16) {
        tmp = m_image.convertToFormat(QImage::Format_RGBA16FPx4_Premultiplied);
        fp16 = true;
    } else if (needsConvertFP32) {
        tmp = m_image.convertToFormat(QImage::Format_RGBA32FPx4_Premultiplied);
        fp32 = true;
    } else if (needsConvert8) {
        tmp = m_image.convertToFormat(QImage::Format_RGBA8888_Premultiplied);
    }
    // Downscale the texture to fit inside the max texture limit if it is too big.
    // It would be better if the image was already downscaled to the right size,
    // but this information is not always available at that time, so as a last
    // resort we can do it here. Texture coordinates are normalized, so it
    // won't cause any problems and actual texture sizes will be written
    // based on QSGTexture::textureSize which is updated after this, so that
    // should be ok.
    const int max = rhi->resourceLimit(QRhi::TextureSizeMax);
    if (tmp.width() > max || tmp.height() > max) {
        tmp = tmp.scaled(qMin(max, tmp.width()), qMin(max, tmp.height()), Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
        m_texture_size = tmp.size();
    }

    if ((mipmapFiltering() != QSGTexture::None
            || horizontalWrapMode() != QSGTexture::ClampToEdge
            || verticalWrapMode() != QSGTexture::ClampToEdge)
            && !rhi->isFeatureSupported(QRhi::NPOTTextureRepeat))
    {
        const int w = qNextPowerOfTwo(tmp.width() - 1);
        const int h = qNextPowerOfTwo(tmp.height() - 1);
        if (tmp.width() != w || tmp.height() != h) {
            tmp = tmp.scaled(w, h, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
            m_texture_size = tmp.size();
        }
    }

    bool needsRebuild = false;

    if (m_texture && m_texture->pixelSize() != m_texture_size) {
        m_texture->setPixelSize(m_texture_size);
        needsRebuild = true;
    }

    if (mipmappingChanged) {
        QRhiTexture::Flags f = m_texture->flags();
        f.setFlag(QRhiTexture::MipMapped, hasMipMaps);
        f.setFlag(QRhiTexture::UsedWithGenerateMips, hasMipMaps);
        m_texture->setFlags(f);
        needsRebuild = true;
    }

    if (!m_texture) {
        QRhiTexture::Flags f;
        if (hasMipMaps)
            f |= QRhiTexture::MipMapped | QRhiTexture::UsedWithGenerateMips;

        QRhiTexture::Format format = QRhiTexture::RGBA8;
        if (fp16)
            format = QRhiTexture::RGBA16F;
        else if (fp32)
            format = QRhiTexture::RGBA32F;
        else if (bgra)
            format = QRhiTexture::BGRA8;

        m_texture = rhi->newTexture(format, m_texture_size, 1, f);
        needsRebuild = true;
    }

    if (needsRebuild) {
        if (!m_texture->create()) {
            qWarning("Failed to build texture for QSGPlainTexture (size %dx%d)",
                     m_texture_size.width(), m_texture_size.height());
            return;
        }
    }

    if (tmp.width() * 4 != tmp.bytesPerLine())
        tmp = tmp.copy();

    resourceUpdates->uploadTexture(m_texture, tmp);

    if (hasMipMaps) {
        resourceUpdates->generateMips(m_texture);
        m_mipmaps_generated = true;
    }

    d->m_last_mipmap_filter = mipmapFiltering();
    m_texture_rect = QRectF(0, 0, 1, 1);

    if (!m_retain_image)
        m_image = QImage();
}

QT_END_NAMESPACE

#include "moc_qsgplaintexture_p.cpp"
