/****************************************************************************
**
** Copyright (C) 2008-2012 NVIDIA Corporation.
** Copyright (C) 2019 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Quick 3D.
**
** $QT_BEGIN_LICENSE:GPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 or (at your option) any later version
** approved by the KDE Free Qt Foundation. The licenses are as published by
** the Free Software Foundation and appearing in the file LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/

#include "qssgrenderprefiltertexture_p.h"

#include <QtQuick3DRender/private/qssgrendercontext_p.h>
#include <QtQuick3DRender/private/qssgrendershaderprogram_p.h>

QT_BEGIN_NAMESPACE

QSSGRenderPrefilterTexture::QSSGRenderPrefilterTexture(const QSSGRef<QSSGRenderContext> &inQSSGRenderContext,
                                                           qint32 inWidth,
                                                           qint32 inHeight,
                                                           const QSSGRef<QSSGRenderTexture2D> &inTexture2D,
                                                           QSSGRenderTextureFormat inDestFormat)
    : m_texture2D(inTexture2D), m_destinationFormat(inDestFormat), m_width(inWidth), m_height(inHeight), m_renderContext(inQSSGRenderContext)
{
    // Calculate mip level
    int maxDim = inWidth >= inHeight ? inWidth : inHeight;

    m_maxMipMapLevel = static_cast<int>(logf((float)maxDim) / logf(2.0f));
    // no concept of sizeOfFormat just does'nt make sense
    m_sizeOfFormat = m_destinationFormat.getSizeofFormat();
    m_noOfComponent = m_destinationFormat.getNumberOfComponent();
}

QSSGRef<QSSGRenderPrefilterTexture> QSSGRenderPrefilterTexture::create(const QSSGRef<QSSGRenderContext> &inQSSGRenderContext,
                                                                             qint32 inWidth,
                                                                             qint32 inHeight,
                                                                             const QSSGRef<QSSGRenderTexture2D> &inTexture2D,
                                                                             QSSGRenderTextureFormat inDestFormat)
{
    QSSGRef<QSSGRenderPrefilterTexture> theBSDFMipMap;

    if (inQSSGRenderContext->supportsCompute()) {
        theBSDFMipMap = new QSSGRenderPrefilterTextureCompute(inQSSGRenderContext, inWidth, inHeight, inTexture2D, inDestFormat);
    }

    if (!theBSDFMipMap) {
        theBSDFMipMap = new QSSGRenderPrefilterTextureCPU(inQSSGRenderContext, inWidth, inHeight, inTexture2D, inDestFormat);
    }

    return theBSDFMipMap;
}

QSSGRenderPrefilterTexture::~QSSGRenderPrefilterTexture() = default;

//------------------------------------------------------------------------------------
// CPU based filtering
//------------------------------------------------------------------------------------

QSSGRenderPrefilterTextureCPU::QSSGRenderPrefilterTextureCPU(const QSSGRef<QSSGRenderContext> &inQSSGRenderContext,
                                                                 int inWidth,
                                                                 int inHeight,
                                                                 const QSSGRef<QSSGRenderTexture2D> &inTexture2D,
                                                                 QSSGRenderTextureFormat inDestFormat)
    : QSSGRenderPrefilterTexture(inQSSGRenderContext, inWidth, inHeight, inTexture2D, inDestFormat)
{
}

inline int QSSGRenderPrefilterTextureCPU::wrapMod(int a, int base)
{
    return (a >= 0) ? a % base : (a % base) + base;
}

inline void QSSGRenderPrefilterTextureCPU::getWrappedCoords(int &sX, int &sY, int width, int height)
{
    if (sY < 0) {
        sX -= width >> 1;
        sY = -sY;
    }
    if (sY >= height) {
        sX += width >> 1;
        sY = height - sY;
    }
    sX = wrapMod(sX, width);
}

struct M8E8
{
    quint8 m;
    quint8 e;
    M8E8() : m(0), e(0){
    }
    M8E8(const float val) {
        float l2 = 1.f + floor(log2f(val));
        float mm = val / powf(2.f, l2);
        m = quint8(mm * 255.f);
        e = quint8(l2 + 128);
    }
    M8E8(const float val, quint8 exp) {
        if (val <= 0) {
            m = e = 0;
            return;
        }
        float mm = val / powf(2.f, exp - 128);
        m = quint8(mm * 255.f);
        e = exp;
    }
};

void QSSGRenderTextureFormat::decodeToFloat(void *inPtr, qint32 byteOfs, float *outPtr) const
{
    Q_ASSERT(byteOfs >= 0);
    outPtr[0] = 0.0f;
    outPtr[1] = 0.0f;
    outPtr[2] = 0.0f;
    outPtr[3] = 0.0f;
    quint8 *src = reinterpret_cast<quint8 *>(inPtr);
    switch (format) {
    case Alpha8:
        outPtr[0] = (float(src[byteOfs])) / 255.0f;
        break;

    case Luminance8:
    case LuminanceAlpha8:
    case R8:
    case RG8:
    case RGB8:
    case RGBA8:
    case SRGB8:
    case SRGB8A8:
        for (qint32 i = 0; i < getSizeofFormat(); ++i) {
            float val = (float(src[byteOfs + i])) / 255.0f;
            outPtr[i] = (i < 3) ? std::pow(val, 0.4545454545f) : val;
        }
        break;
    case RGBE8:
        {
            float pwd = powf(2.0f, int(src[byteOfs + 3]) - 128);
            outPtr[0] = float(src[byteOfs + 0]) * pwd / 255.0;
            outPtr[1] = float(src[byteOfs + 1]) * pwd / 255.0;
            outPtr[2] = float(src[byteOfs + 2]) * pwd / 255.0;
            outPtr[3] = 1.0f;
        } break;

    case R32F:
        outPtr[0] = reinterpret_cast<float *>(src + byteOfs)[0];
        break;
    case RG32F:
        outPtr[0] = reinterpret_cast<float *>(src + byteOfs)[0];
        outPtr[1] = reinterpret_cast<float *>(src + byteOfs)[1];
        break;
    case RGBA32F:
        outPtr[0] = reinterpret_cast<float *>(src + byteOfs)[0];
        outPtr[1] = reinterpret_cast<float *>(src + byteOfs)[1];
        outPtr[2] = reinterpret_cast<float *>(src + byteOfs)[2];
        outPtr[3] = reinterpret_cast<float *>(src + byteOfs)[3];
        break;
    case RGB32F:
        outPtr[0] = reinterpret_cast<float *>(src + byteOfs)[0];
        outPtr[1] = reinterpret_cast<float *>(src + byteOfs)[1];
        outPtr[2] = reinterpret_cast<float *>(src + byteOfs)[2];
        break;

    case R16F:
    case RG16F:
    case RGBA16F:
        for (qint32 i = 0; i < (getSizeofFormat() >> 1); ++i) {
            // NOTE : This only works on the assumption that we don't have any denormals,
            // Infs or NaNs.
            // Every pixel in our source image should be "regular"
            quint16 h = reinterpret_cast<quint16 *>(src + byteOfs)[i];
            quint32 sign = (h & 0x8000u) << 16u;
            quint32 exponent = (((((h & 0x7c00u) >> 10) - 15) + 127) << 23);
            quint32 mantissa = ((h & 0x3ffu) << 13);
            quint32 result = sign | exponent | mantissa;

            if (h == 0 || h == 0x8000)
                result = 0;
            memcpy(outPtr + i, &result, 4);
        }
        break;

    case R11G11B10:
        // place holder
        Q_ASSERT(false);
        break;

    default:
        outPtr[0] = 0.0f;
        outPtr[1] = 0.0f;
        outPtr[2] = 0.0f;
        outPtr[3] = 0.0f;
        break;
    }
}

void QSSGRenderTextureFormat::encodeToPixel(float *inPtr, void *outPtr, qint32 byteOfs) const
{
    Q_ASSERT(byteOfs >= 0);
    quint8 *dest = reinterpret_cast<quint8 *>(outPtr);
    switch (format) {
    case QSSGRenderTextureFormat::Alpha8:
        dest[byteOfs] = quint8(inPtr[0] * 255.0f);
        break;

    case Luminance8:
    case LuminanceAlpha8:
    case R8:
    case RG8:
    case RGB8:
    case RGBA8:
    case SRGB8:
    case SRGB8A8:
        for (qint32 i = 0; i < getSizeofFormat(); ++i) {
            inPtr[i] = (inPtr[i] > 1.0f) ? 1.0f : inPtr[i];
            if (i < 3)
                dest[byteOfs + i] = quint8(powf(inPtr[i], 2.2f) * 255.0f);
            else
                dest[byteOfs + i] = quint8(inPtr[i] * 255.0f);
        }
        break;
    case RGBE8:
    {
        float max = qMax(inPtr[0], qMax(inPtr[1], inPtr[2]));
        M8E8 ex(max);
        M8E8 a(inPtr[0], ex.e);
        M8E8 b(inPtr[1], ex.e);
        M8E8 c(inPtr[2], ex.e);
        quint8 *dst = reinterpret_cast<quint8 *>(outPtr) + byteOfs;
        dst[0] = a.m;
        dst[1] = b.m;
        dst[2] = c.m;
        dst[3] = ex.e;
    } break;

    case R32F:
        reinterpret_cast<float *>(dest + byteOfs)[0] = inPtr[0];
        break;
    case RG32F:
        reinterpret_cast<float *>(dest + byteOfs)[0] = inPtr[0];
        reinterpret_cast<float *>(dest + byteOfs)[1] = inPtr[1];
        break;
    case RGBA32F:
        reinterpret_cast<float *>(dest + byteOfs)[0] = inPtr[0];
        reinterpret_cast<float *>(dest + byteOfs)[1] = inPtr[1];
        reinterpret_cast<float *>(dest + byteOfs)[2] = inPtr[2];
        reinterpret_cast<float *>(dest + byteOfs)[3] = inPtr[3];
        break;
    case RGB32F:
        reinterpret_cast<float *>(dest + byteOfs)[0] = inPtr[0];
        reinterpret_cast<float *>(dest + byteOfs)[1] = inPtr[1];
        reinterpret_cast<float *>(dest + byteOfs)[2] = inPtr[2];
        break;

    case R16F:
    case RG16F:
    case RGBA16F:
        for (qint32 i = 0; i < (getSizeofFormat() >> 1); ++i) {
            // NOTE : This also has the limitation of not handling  infs, NaNs and
            // denormals, but it should be
            // sufficient for our purposes.
            if (inPtr[i] > 65519.0f)
                inPtr[i] = 65519.0f;
            if (std::fabs(inPtr[i]) < 6.10352E-5f)
                inPtr[i] = 0.0f;
            quint32 f = reinterpret_cast<quint32 *>(inPtr)[i];
            quint32 sign = (f & 0x80000000) >> 16;
            qint32 exponent = (f & 0x7f800000) >> 23;
            quint32 mantissa = (f >> 13) & 0x3ff;
            exponent = exponent - 112;
            if (exponent > 31)
                exponent = 31;
            if (exponent < 0)
                exponent = 0;
            exponent = exponent << 10;
            reinterpret_cast<quint16 *>(dest + byteOfs)[i] = quint16(sign | quint32(exponent) | mantissa);
        }
        break;

    case R11G11B10:
        // place holder
        Q_ASSERT(false);
        break;

    default:
        dest[byteOfs] = 0;
        dest[byteOfs + 1] = 0;
        dest[byteOfs + 2] = 0;
        dest[byteOfs + 3] = 0;
        break;
    }
}

QSSGTextureData QSSGRenderPrefilterTextureCPU::createBsdfMipLevel(QSSGTextureData &inCurMipLevel,
                                                                      QSSGTextureData &inPrevMipLevel,
                                                                      int width,
                                                                      int height) //, IPerfTimer& inPerfTimer )
{
    QSSGTextureData retval;
    int newWidth = width >> 1;
    int newHeight = height >> 1;
    newWidth = newWidth >= 1 ? newWidth : 1;
    newHeight = newHeight >= 1 ? newHeight : 1;

    if (inCurMipLevel.data) {
        retval = inCurMipLevel;
        retval.dataSizeInBytes = newWidth * newHeight * inPrevMipLevel.format.getSizeofFormat();
    } else {
        retval.dataSizeInBytes = newWidth * newHeight * inPrevMipLevel.format.getSizeofFormat();
        retval.format = inPrevMipLevel.format; // inLoadedImage.format;
        retval.data = ::malloc(retval.dataSizeInBytes);
    }

    for (int y = 0; y < newHeight; ++y) {
        for (int x = 0; x < newWidth; ++x) {
            float accumVal[4];
            accumVal[0] = 0;
            accumVal[1] = 0;
            accumVal[2] = 0;
            accumVal[3] = 0;
            for (int sy = -2; sy <= 2; ++sy) {
                for (int sx = -2; sx <= 2; ++sx) {
                    int sampleX = sx + (x << 1);
                    int sampleY = sy + (y << 1);
                    getWrappedCoords(sampleX, sampleY, width, height);

                    // Cauchy filter (this is simply because it's the easiest to evaluate, and
                    // requires no complex
                    // functions).
                    float filterPdf = 1.f / (1.f + float(sx * sx + sy * sy) * 2.f);
                    // With FP HDR formats, we're not worried about intensity loss so much as
                    // unnecessary energy gain,
                    // whereas with LDR formats, the fear with a continuous normalization factor is
                    // that we'd lose
                    // intensity and saturation as well.
                    filterPdf /= (retval.format.getSizeofFormat() >= 8) ? 4.71238898f : 4.5403446f;
                    // filterPdf /= 4.5403446f;        // Discrete normalization factor
                    // filterPdf /= 4.71238898f;        // Continuous normalization factor
                    float curPix[4];
                    qint32 byteOffset = (sampleY * width + sampleX) * retval.format.getSizeofFormat();
                    if (byteOffset < 0) {
                        sampleY = height + sampleY;
                        byteOffset = (sampleY * width + sampleX) * retval.format.getSizeofFormat();
                    }

                    retval.format.decodeToFloat(inPrevMipLevel.data, byteOffset, curPix);

                    accumVal[0] += filterPdf * curPix[0];
                    accumVal[1] += filterPdf * curPix[1];
                    accumVal[2] += filterPdf * curPix[2];
                    accumVal[3] += filterPdf * curPix[3];
                }
            }

            quint32 newIdx = (y * newWidth + x) * retval.format.getSizeofFormat();

            retval.format.encodeToPixel(accumVal, retval.data, newIdx);
        }
    }

    return retval;
}

void QSSGRenderPrefilterTextureCPU::build(void *inTextureData, qint32 inTextureDataSize, QSSGRenderTextureFormat inFormat)
{
    m_sizeOfInternalFormat = inFormat.getSizeofFormat();
    m_internalNoOfComponent = inFormat.getNumberOfComponent();

    m_texture2D->setMaxLevel(m_maxMipMapLevel);
    m_texture2D->setTextureData(QSSGByteView((quint8 *)inTextureData, inTextureDataSize), 0, m_width, m_height, inFormat, m_destinationFormat);

    QSSGTextureData theMipImage;
    QSSGTextureData prevImage;
    prevImage.data = inTextureData;
    prevImage.dataSizeInBytes = inTextureDataSize;
    prevImage.format = inFormat;
    int curWidth = m_width;
    int curHeight = m_height;
    int size = inFormat.getSizeofFormat();
    for (int idx = 1; idx <= m_maxMipMapLevel; ++idx) {
        theMipImage = createBsdfMipLevel(theMipImage, prevImage, curWidth, curHeight); //, m_PerfTimer );
        curWidth = curWidth >> 1;
        curHeight = curHeight >> 1;
        curWidth = curWidth >= 1 ? curWidth : 1;
        curHeight = curHeight >= 1 ? curHeight : 1;
        inTextureDataSize = curWidth * curHeight * size;

        m_texture2D->setTextureData(toByteView((const char *)theMipImage.data, (quint32)inTextureDataSize),
                                    (quint8)idx,
                                    (quint32)curWidth,
                                    (quint32)curHeight,
                                    theMipImage.format,
                                    m_destinationFormat);

        if (prevImage.data == inTextureData)
            prevImage = QSSGTextureData();

        QSSGTextureData temp = prevImage;
        prevImage = theMipImage;
        theMipImage = temp;
    }
    ::free(theMipImage.data);
    ::free(prevImage.data);
}

//------------------------------------------------------------------------------------
// GL compute based filtering
//------------------------------------------------------------------------------------

static const char *computeUploadShader(QByteArray &prog, QSSGRenderTextureFormat inFormat, bool binESContext)
{
    if (binESContext) {
        prog += "#version 310 es\n"
                "#extension GL_ARB_compute_shader : enable\n"
                "precision highp float;\n"
                "precision highp int;\n"
                "precision mediump image2D;\n";
    } else {
        prog += "#version 430\n"
                "#extension GL_ARB_compute_shader : enable\n";
    }

    if (inFormat == QSSGRenderTextureFormat::RGBA8) {
        prog += "// Set workgroup layout;\n"
                "layout (local_size_x = 16, local_size_y = 16) in;\n\n"
                "layout (rgba8, binding = 1) readonly uniform image2D inputImage;\n\n"
                "layout (rgba16f, binding = 2) writeonly uniform image2D outputImage;\n\n"
                "void main()\n"
                "{\n"
                "  if ( gl_GlobalInvocationID.x >= gl_NumWorkGroups.x || gl_GlobalInvocationID.y "
                ">= gl_NumWorkGroups.y )\n"
                "    return;\n"
                "  vec4 value = imageLoad(inputImage, ivec2(gl_GlobalInvocationID.xy));\n"
                "  imageStore( outputImage, ivec2(gl_GlobalInvocationID.xy), value );\n"
                "}\n";
    } else {
        prog += "float convertToFloat( in uint inValue )\n"
                "{\n"
                "  uint v = inValue & uint(0xFF);\n"
                "  float f = float(v)/256.0;\n"
                "  return f;\n"
                "}\n";

        prog += "int getMod( in int inValue, in int mod )\n"
                "{\n"
                "  int v = mod * (inValue/mod);\n"
                "  return inValue - v;\n"
                "}\n";

        prog += "vec4 getRGBValue( in int byteNo, vec4 inVal, vec4 inVal1 )\n"
                "{\n"
                "  vec4 result= vec4(0.0);\n"
                "  if( byteNo == 0) {\n"
                "    result.r = inVal.r;\n"
                "    result.g = inVal.g;\n"
                "    result.b = inVal.b;\n"
                "  }\n"
                "  else if( byteNo == 1) {\n"
                "    result.r = inVal.g;\n"
                "    result.g = inVal.b;\n"
                "    result.b = inVal.a;\n"
                "  }\n"
                "  else if( byteNo == 2) {\n"
                "    result.r = inVal.b;\n"
                "    result.g = inVal.a;\n"
                "    result.b = inVal1.r;\n"
                "  }\n"
                "  else if( byteNo == 3) {\n"
                "    result.r = inVal.a;\n"
                "    result.g = inVal1.r;\n"
                "    result.b = inVal1.g;\n"
                "  }\n"
                "  return result;\n"
                "}\n";

        prog += "// Set workgroup layout;\n"
                "layout (local_size_x = 16, local_size_y = 16) in;\n\n"
                "layout (rgba8, binding = 1) readonly uniform image2D inputImage;\n\n"
                "layout (rgba16f, binding = 2) writeonly uniform image2D outputImage;\n\n"
                "void main()\n"
                "{\n"
                "  vec4 result = vec4(0.0);\n"
                "  if ( gl_GlobalInvocationID.x >= gl_NumWorkGroups.x || gl_GlobalInvocationID.y "
                ">= gl_NumWorkGroups.y )\n"
                "    return;\n"
                "  int xpos = (int(gl_GlobalInvocationID.x)*3)/4;\n"
                "  int xmod = getMod(int(gl_GlobalInvocationID.x)*3, 4);\n"
                "  ivec2 readPos = ivec2(xpos, gl_GlobalInvocationID.y);\n"
                "  vec4 value = imageLoad(inputImage, readPos);\n"
                "  vec4 value1 = imageLoad(inputImage, ivec2(readPos.x + 1, readPos.y));\n"
                "  result = getRGBValue( xmod, value, value1);\n"
                "  imageStore( outputImage, ivec2(gl_GlobalInvocationID.xy), result );\n"
                "}\n";
    }
    return prog.constData();
}

static const char *computeWorkShader(QByteArray &prog, bool binESContext, bool rgbe)
{
    if (binESContext) {
        prog += "#version 310 es\n"
                "#extension GL_ARB_compute_shader : enable\n"
                "precision highp float;\n"
                "precision highp int;\n"
                "precision mediump image2D;\n";
    } else {
        prog += "#version 430\n"
                "#extension GL_ARB_compute_shader : enable\n";
    }

    prog += "int wrapMod( in int a, in int base )\n"
            "{\n"
            "  return ( a >= 0 ) ? a % base : -(a % base) + base;\n"
            "}\n";

    prog += "void getWrappedCoords( inout int sX, inout int sY, in int width, in int height )\n"
            "{\n"
            "  if (sY < 0) { sX -= width >> 1; sY = -sY; }\n"
            "  if (sY >= height) { sX += width >> 1; sY = height - sY; }\n"
            "  sX = wrapMod( sX, width );\n"
            "}\n";

    if (rgbe) {
        prog += "vec4 decodeRGBE(in vec4 rgbe)\n"
                "{\n"
                " float f = pow(2.0, 255.0 * rgbe.a - 128.0);\n"
                " return vec4(rgbe.rgb * f, 1.0);\n"
                "}\n";
        prog += "vec4 encodeRGBE(in vec4 rgba)\n"
                "{\n"
                " float maxMan = max(rgba.r, max(rgba.g, rgba.b));\n"
                " float maxExp = 1.0 + floor(log2(maxMan));\n"
                " return vec4(rgba.rgb / pow(2.0, maxExp), (maxExp + 128.0) / 255.0);\n"
                "}\n";
    }

    prog += "// Set workgroup layout;\n"
            "layout (local_size_x = 16, local_size_y = 16) in;\n\n";

    if (rgbe) {
        prog +=
            "layout (rgba8, binding = 1) readonly uniform image2D inputImage;\n\n"
            "layout (rgba8, binding = 2) writeonly uniform image2D outputImage;\n\n";
    } else {
        prog +=
            "layout (rgba16f, binding = 1) readonly uniform image2D inputImage;\n\n"
            "layout (rgba16f, binding = 2) writeonly uniform image2D outputImage;\n\n";
    }

    prog +=
            "void main()\n"
            "{\n"
            "  int prevWidth = int(gl_NumWorkGroups.x) << 1;\n"
            "  int prevHeight = int(gl_NumWorkGroups.y) << 1;\n"
            "  if ( gl_GlobalInvocationID.x >= gl_NumWorkGroups.x || gl_GlobalInvocationID.y >= "
            "gl_NumWorkGroups.y )\n"
            "    return;\n"
            "  vec4 accumVal = vec4(0.0);\n"
            "  for ( int sy = -2; sy <= 2; ++sy )\n"
            "  {\n"
            "    for ( int sx = -2; sx <= 2; ++sx )\n"
            "    {\n"
            "      int sampleX = sx + (int(gl_GlobalInvocationID.x) << 1);\n"
            "      int sampleY = sy + (int(gl_GlobalInvocationID.y) << 1);\n"
            "      getWrappedCoords(sampleX, sampleY, prevWidth, prevHeight);\n"
            "       if ((sampleY * prevWidth + sampleX) < 0 )\n"
            "        sampleY = prevHeight + sampleY;\n"
            "      ivec2 pos = ivec2(sampleX, sampleY);\n"
            "      vec4 value = imageLoad(inputImage, pos);\n";

    if (rgbe) {
        prog +=
            "      value = decodeRGBE(value);\n";
    }

    prog += "      float filterPdf = 1.0 / ( 1.0 + float(sx*sx + sy*sy)*2.0 );\n"
            "      filterPdf /= 4.71238898;\n"
            "      accumVal[0] += filterPdf * value.r;\n"
            "       accumVal[1] += filterPdf * value.g;\n"
            "       accumVal[2] += filterPdf * value.b;\n"
            "       accumVal[3] += filterPdf * value.a;\n"
            "    }\n"
            "  }\n";

    if (rgbe) {
        prog +=
            "  accumVal = encodeRGBE(accumVal);\n";
    }

    prog += "  imageStore( outputImage, ivec2(gl_GlobalInvocationID.xy), accumVal );\n"
            "}\n";

    return prog.constData();
}

static bool isGLESContext(const QSSGRef<QSSGRenderContext> &context)
{
    QSSGRenderContextType ctxType = context->renderContextType();

    // Need minimum of GL3 or GLES3
    if (ctxType == QSSGRenderContextType::GLES2 || ctxType == QSSGRenderContextType::GLES3
        || ctxType == QSSGRenderContextType::GLES3PLUS) {
        return true;
    }

    return false;
}

#define WORKGROUP_SIZE 16

QSSGRenderPrefilterTextureCompute::QSSGRenderPrefilterTextureCompute(const QSSGRef<QSSGRenderContext> &inQSSGRenderContext,
                                                                         qint32 inWidth,
                                                                         qint32 inHeight,
                                                                         const QSSGRef<QSSGRenderTexture2D> &inTexture2D,
                                                                         QSSGRenderTextureFormat inDestFormat)
    : QSSGRenderPrefilterTexture(inQSSGRenderContext, inWidth, inHeight, inTexture2D, inDestFormat)
{
}

QSSGRenderPrefilterTextureCompute::~QSSGRenderPrefilterTextureCompute() = default;

QSSGRenderShaderProgram *QSSGRenderPrefilterTextureCompute::createComputeProgram(
        const QSSGRef<QSSGRenderContext> &context, QSSGRenderTextureFormat inFormat)
{
    QByteArray computeProg;

    if (!m_bsdfProgram && inFormat != QSSGRenderTextureFormat::RGBE8) {
        m_bsdfProgram = context->compileComputeSource("Compute BSDF mipmap shader",
                                                      toByteView(computeWorkShader(computeProg, isGLESContext(context), false)))
                                .m_shader;
        return m_bsdfProgram.get();
    }
    if (!m_bsdfRGBEProgram && inFormat == QSSGRenderTextureFormat::RGBE8) {
        m_bsdfRGBEProgram = context->compileComputeSource("Compute BSDF RGBE mipmap shader",
                                                      toByteView(computeWorkShader(computeProg, isGLESContext(context), true)))
                                .m_shader;
        return m_bsdfRGBEProgram.get();
    }
    return nullptr;
}

QSSGRef<QSSGRenderShaderProgram> QSSGRenderPrefilterTextureCompute::getOrCreateUploadComputeProgram(const QSSGRef<QSSGRenderContext> &context,
                                                                                                          QSSGRenderTextureFormat inFormat)
{
    QByteArray computeProg;

    if (inFormat == QSSGRenderTextureFormat::RGB8) {
        if (!m_uploadProgram_RGB8) {
            m_uploadProgram_RGB8 = context->compileComputeSource("Compute BSDF mipmap level 0 RGB8 shader",
                                                                 toByteView(computeUploadShader(computeProg, inFormat, isGLESContext(context))))
                                           .m_shader;
        }

        return m_uploadProgram_RGB8;
    } else {
        if (!m_uploadProgram_RGBA8) {
            m_uploadProgram_RGBA8 = context->compileComputeSource("Compute BSDF mipmap level 0 RGBA8 shader",
                                                                  toByteView(computeUploadShader(computeProg, inFormat, isGLESContext(context))))
                                            .m_shader;
        }

        return m_uploadProgram_RGBA8;
    }
}

void QSSGRenderPrefilterTextureCompute::createLevel0Tex(void *inTextureData, qint32 inTextureDataSize, QSSGRenderTextureFormat inFormat)
{
    QSSGRenderTextureFormat theFormat = inFormat;
    qint32 theWidth = m_width;

    // Since we cannot use RGB format in GL compute
    // we treat it as a RGBA component format
    if (inFormat == QSSGRenderTextureFormat::RGB8) {
        // This works only with 4 byte aligned data
        Q_ASSERT(m_width % 4 == 0);
        theFormat = QSSGRenderTextureFormat::RGBA8;
        theWidth = (m_width * 3) / 4;
    }

    if (m_level0Tex == nullptr) {
        m_level0Tex = new QSSGRenderTexture2D(m_renderContext);
        m_level0Tex->setTextureStorage(1, theWidth, m_height, theFormat, theFormat, QSSGByteView((quint8 *)inTextureData, inTextureDataSize));
    } else {
        m_level0Tex->setTextureSubData(QSSGByteView((quint8 *)inTextureData, inTextureDataSize), 0, 0, 0, theWidth, m_height, theFormat);
    }
}

void QSSGRenderPrefilterTextureCompute::build(void *inTextureData, qint32 inTextureDataSize, QSSGRenderTextureFormat inFormat)
{
    bool needMipUpload = (inFormat != m_destinationFormat);
    QSSGRenderShaderProgram *program = nullptr;
    // re-upload data
    if (!m_textureCreated) {
        m_texture2D->setTextureStorage(m_maxMipMapLevel + 1,
                                       m_width,
                                       m_height,
                                       m_destinationFormat,
                                       inFormat,
                                       (needMipUpload) ? QSSGByteView()
                                                       : QSSGByteView((quint8 *)inTextureData, inTextureDataSize));
        // create a compute shader (if not aloread done) which computes the BSDF mipmaps for this
        // texture
        program = createComputeProgram(m_renderContext, inFormat);

        if (!program) {
            Q_ASSERT(false);
            return;
        }

        m_textureCreated = true;
    } else if (!needMipUpload) {
        m_texture2D->setTextureSubData(QSSGByteView((quint8 *)inTextureData, inTextureDataSize), 0, 0, 0, m_width, m_height, inFormat);
    }

    if (needMipUpload) {
        createLevel0Tex(inTextureData, inTextureDataSize, inFormat);
    }

    QSSGRef<QSSGRenderImage2D> theInputImage;
    QSSGRef<QSSGRenderImage2D> theOutputImage;
    theInputImage = new QSSGRenderImage2D(m_renderContext, m_texture2D, QSSGRenderImageAccessType::ReadWrite);
    theOutputImage = new QSSGRenderImage2D(m_renderContext, m_texture2D, QSSGRenderImageAccessType::ReadWrite);

    if (needMipUpload && m_level0Tex) {
        const QSSGRef<QSSGRenderShaderProgram> &uploadProg = getOrCreateUploadComputeProgram(m_renderContext, inFormat);
        if (!uploadProg)
            return;

        m_renderContext->setActiveShader(uploadProg);

        QSSGRef<QSSGRenderImage2D> theInputImage0
                = new QSSGRenderImage2D(m_renderContext, m_level0Tex, QSSGRenderImageAccessType::ReadWrite);

        theInputImage0->setTextureLevel(0);
        QSSGRenderCachedShaderProperty<QSSGRenderImage2D *> theCachedinputImage0("inputImage", uploadProg);
        theCachedinputImage0.set(theInputImage0.data());

        theOutputImage->setTextureLevel(0);
        QSSGRenderCachedShaderProperty<QSSGRenderImage2D *> theCachedOutputImage("outputImage", uploadProg);
        theCachedOutputImage.set(theOutputImage.data());

        m_renderContext->dispatchCompute(uploadProg, m_width, m_height, 1);

        // sync
        QSSGRenderBufferBarrierFlags flags(QSSGRenderBufferBarrierValues::ShaderImageAccess);
        m_renderContext->setMemoryBarrier(flags);
    }

    int width = m_width >> 1;
    int height = m_height >> 1;

    m_renderContext->setActiveShader(program);

    for (int i = 1; i <= m_maxMipMapLevel; ++i) {
        theOutputImage->setTextureLevel(i);
        QSSGRenderCachedShaderProperty<QSSGRenderImage2D *> theCachedOutputImage("outputImage", program);
        theCachedOutputImage.set(theOutputImage.data());
        theInputImage->setTextureLevel(i - 1);
        QSSGRenderCachedShaderProperty<QSSGRenderImage2D *> theCachedinputImage("inputImage", program);
        theCachedinputImage.set(theInputImage.data());

        m_renderContext->dispatchCompute(program, width, height, 1);

        width = width > 2 ? width >> 1 : 1;
        height = height > 2 ? height >> 1 : 1;

        // sync
        QSSGRenderBufferBarrierFlags flags(QSSGRenderBufferBarrierValues::ShaderImageAccess);
        m_renderContext->setMemoryBarrier(flags);
    }
}

QT_END_NAMESPACE
