// Copyright (C) 2019 Klaralvdalens Datakonsult AB (KDAB).
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only

#include "imagesubmissioncontext_p.h"
#include <Qt3DRender/private/shaderimage_p.h>
#include <Qt3DRender/qshaderimage.h>
#include <graphicscontext_p.h>
#include <gltexture_p.h>
#include <logging_p.h>

QT_BEGIN_NAMESPACE

// ES 3.1+ or GL 4.2+
#ifndef GL_READ_ONLY
#define GL_READ_ONLY                      0x88B8
#endif
#ifndef GL_WRITE_ONLY
#define GL_WRITE_ONLY                     0x88B9
#endif
#ifndef GL_READ_WRITE
#define GL_READ_WRITE                     0x88BA
#endif

namespace Qt3DRender {
namespace Render {
namespace OpenGL {

class GraphicsContext;
class GLTexture;

namespace  {

GLenum glAccessEnumForShaderImageAccess(QShaderImage::Access access)
{
    switch (access) {
    case QShaderImage::ReadOnly:
        return GL_READ_ONLY;
    case QShaderImage::WriteOnly:
        return GL_WRITE_ONLY;
    case QShaderImage::ReadWrite:
    default:
        break;
    }
    return GL_READ_WRITE;
}

GLenum glImageFormatToGL(QShaderImage::ImageFormat format)
{
    // Right now we can abuse from the fact that the ImageFormat enum values
    // have been assigned the same value as the GL enum
    return GLenum(format);
}

GLenum glImageFormatForShaderImageFormat(QShaderImage::ImageFormat format,
                                         QAbstractTexture::TextureFormat textureFormat)
{
    Q_ASSERT_X(format != QShaderImage::NoFormat, Q_FUNC_INFO, "Valid image format or Automatic expected");

    if (format != QShaderImage::Automatic)
        return glImageFormatToGL(format);

    // Otherwise try to mind to best texture format
    switch (textureFormat) {
    case QAbstractTexture::R8_UNorm:
        return glImageFormatToGL(QShaderImage::R8_UNorm);
    case QAbstractTexture::RG8_UNorm:
        return glImageFormatToGL(QShaderImage::RG8_UNorm);
    case QAbstractTexture::RGBA8_UNorm:
        return glImageFormatToGL(QShaderImage::RGBA8_UNorm);

    case QAbstractTexture::R16_UNorm:
        return glImageFormatToGL(QShaderImage::R16_UNorm);
    case QAbstractTexture::RG16_UNorm:
        return glImageFormatToGL(QShaderImage::RG16_UNorm);
    case QAbstractTexture::RGBA16_UNorm:
        return glImageFormatToGL(QShaderImage::RGBA16_UNorm);

    case QAbstractTexture::R8_SNorm:
        return glImageFormatToGL(QShaderImage::R8_SNorm);
    case QAbstractTexture::RG8_SNorm:
        return glImageFormatToGL(QShaderImage::RG8_SNorm);
    case QAbstractTexture::RGBA8_SNorm:
        return glImageFormatToGL(QShaderImage::RGBA8_SNorm);

    case QAbstractTexture::R16_SNorm:
        return glImageFormatToGL(QShaderImage::R16_SNorm);
    case QAbstractTexture::RG16_SNorm:
        return glImageFormatToGL(QShaderImage::RG16_SNorm);
    case QAbstractTexture::RGBA16_SNorm:
        return glImageFormatToGL(QShaderImage::RGBA16_SNorm);

    case QAbstractTexture::R8U:
        return glImageFormatToGL(QShaderImage::R8U);
    case QAbstractTexture::RG8U:
        return glImageFormatToGL(QShaderImage::RG8U);
    case QAbstractTexture::RGBA8U:
        return glImageFormatToGL(QShaderImage::RGBA8U);

    case QAbstractTexture::R16U:
        return glImageFormatToGL(QShaderImage::R16U);
    case QAbstractTexture::RG16U:
        return glImageFormatToGL(QShaderImage::RG16U);
    case QAbstractTexture::RGBA16U:
        return glImageFormatToGL(QShaderImage::RGBA16U);

    case QAbstractTexture::R32U:
        return glImageFormatToGL(QShaderImage::R32U);
    case QAbstractTexture::RG32U:
        return glImageFormatToGL(QShaderImage::RG32U);
    case QAbstractTexture::RGBA32U:
        return glImageFormatToGL(QShaderImage::RGBA32U);

    case QAbstractTexture::R8I:
        return glImageFormatToGL(QShaderImage::R8I);
    case QAbstractTexture::RG8I:
        return glImageFormatToGL(QShaderImage::RG8I);
    case QAbstractTexture::RGBA8I:
        return glImageFormatToGL(QShaderImage::RGBA8I);

    case QAbstractTexture::R16I:
        return glImageFormatToGL(QShaderImage::R16I);
    case QAbstractTexture::RG16I:
        return glImageFormatToGL(QShaderImage::RG16I);
    case QAbstractTexture::RGBA16I:
        return glImageFormatToGL(QShaderImage::RGBA16I);

    case QAbstractTexture::R32I:
        return glImageFormatToGL(QShaderImage::R32I);
    case QAbstractTexture::RG32I:
        return glImageFormatToGL(QShaderImage::RG32I);
    case QAbstractTexture::RGBA32I:
        return glImageFormatToGL(QShaderImage::RGBA32I);

    case QAbstractTexture::R16F:
        return glImageFormatToGL(QShaderImage::R16F);
    case QAbstractTexture::RG16F:
        return glImageFormatToGL(QShaderImage::RG16F);
    case QAbstractTexture::RGBA16F:
        return glImageFormatToGL(QShaderImage::RGBA16F);

    case QAbstractTexture::R32F:
        return glImageFormatToGL(QShaderImage::R32F);
    case QAbstractTexture::RG32F:
        return glImageFormatToGL(QShaderImage::RG32F);
    case QAbstractTexture::RGBA32F:
        return glImageFormatToGL(QShaderImage::RGBA32F);

    case QAbstractTexture::RG11B10F:
        return glImageFormatToGL(QShaderImage::RG11B10F);
    case QAbstractTexture::RGB10A2:
        return glImageFormatToGL(QShaderImage::RGB10A2);
    case QAbstractTexture::RGB10A2U:
        return glImageFormatToGL(QShaderImage::RGB10A2U);

    default:
        qWarning() << "Cannot map Texture format" << textureFormat << "to a valid Image Format";
        Q_UNREACHABLE_RETURN(GL_NONE);
    }
}

} // anonymous

ImageSubmissionContext::ImageSubmissionContext()
    : m_ctx(nullptr)
{
}

void ImageSubmissionContext::initialize(GraphicsContext *context)
{
    m_ctx = context;
    m_activeImages.resize(m_ctx->maxImageUnitsCount());
}

void ImageSubmissionContext::endDrawing()
{
    // Reduce score of all active Images
    decayImageScores();
}

// Return Image Unit for Image
// If Image was used previously and recently, it will return the last used unit
// for that image. Otherwise it will try to return the image unit the least used.
int ImageSubmissionContext::activateImage(ShaderImage *image, GLTexture *tex)
{
    const int onUnit = assignUnitForImage(image->peerId());

    if (onUnit < 0) {
        qWarning() << "Unable to find available image unit";
        return -1;
    }

    QOpenGLTexture *glTex = tex->getGLTexture();
    if (glTex == nullptr) {
        qWarning() << "Unable to retrieve valid texture for Image";
        return -1;
    }

    // Bind Image against Texture and resolve Image Format
    m_ctx->bindImageTexture(onUnit,
                            glTex->textureId(),
                            image->mipLevel(),
                            image->layered(),
                            image->layer(),
                            glAccessEnumForShaderImageAccess(image->access()),
                            glImageFormatForShaderImageFormat(image->format(),
                                                              tex->properties().format));

    // Store information about the Texture/Image on ActiveImage for given
    // image unit
    m_activeImages[onUnit].shaderImageId = image->peerId();
    m_activeImages[onUnit].texture = tex;
    m_activeImages[onUnit].score = 200;
    m_activeImages[onUnit].pinned = true;

    return onUnit;
}

// Unset pinned Active Image and reduce their score
void ImageSubmissionContext::deactivateImages()
{
    for (size_t u = 0, m = m_activeImages.size();  u < m; ++u) {
        if (m_activeImages[u].pinned) {
            m_activeImages[u].pinned = false;
            m_activeImages[u].score = qMax(m_activeImages[u].score - 1, 0);
        }
    }
}

// Reduce score of all active images (pinned or not)
void ImageSubmissionContext::decayImageScores()
{
    for (size_t u = 0, m = m_activeImages.size();  u < m; ++u)
        m_activeImages[u].score = qMax(m_activeImages[u].score - 1, 0);
}

int ImageSubmissionContext::assignUnitForImage(Qt3DCore::QNodeId shaderImageId)
{
    int lowestScoredUnit = -1;
    int lowestScore = 0xfffffff;

    const size_t m = m_activeImages.size();
    for (size_t u = 0; u < m; ++u) {
        if (m_activeImages[u].shaderImageId == shaderImageId)
            return int(u);
    }

    for (size_t u = 0; u < m; ++u) {
        // No image is currently active on the image unit
        // we save the image unit with the texture that has been on there
        // the longest time while not being used
        if (!m_activeImages[u].pinned) {
            const int score = m_activeImages[u].score;
            if (score < lowestScore) {
                lowestScore = score;
                lowestScoredUnit = int(u);
            }
        }
    }

    if (lowestScoredUnit == -1)
        qCWarning(Backend) << Q_FUNC_INFO << "No free image units!";

    return lowestScoredUnit;
}

} // namespace OpenGL
} // namespace Render
} // namespace Qt3DRender

QT_END_NAMESPACE
