/****************************************************************************
**
** 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 <QtQuick3DRuntimeRender/private/qssgrenderloadedtexture_p.h>
#include <QtQuick3DRuntimeRender/private/qssgrenderinputstreamfactory_p.h>
#include <QtQuick3DRuntimeRender/private/qssgrendererutil_p.h>
#include <QtQuick3DRuntimeRender/private/qssgruntimerenderlogging_p.h>
#include <QtGui/QImage>
#include <QtGui/QOpenGLTexture>
#include <QtMath>

#include <QtQuick3DUtils/private/qssgutils_p.h>

#include <private/qtexturefilereader_p.h>

QT_BEGIN_NAMESPACE

QSSGRef<QSSGLoadedTexture> QSSGLoadedTexture::loadQImage(const QString &inPath,
                                                               const QSSGRenderTextureFormat &inFormat,
                                                               qint32 flipVertical,
                                                               QSSGRenderContextType renderContextType)
{
    Q_UNUSED(flipVertical)
    Q_UNUSED(renderContextType)
    static constexpr bool systemIsLittleEndian = QSysInfo::ByteOrder == QSysInfo::LittleEndian;
    QSSGRef<QSSGLoadedTexture> retval(nullptr);
    QImage image(inPath);
    if (inFormat == QSSGRenderTextureFormat::Unknown) {
        switch (image.format()) {
        case QImage::Format_Mono:
        case QImage::Format_MonoLSB:
        case QImage::Format_Indexed8: // Convert palleted images
            image.convertTo(QImage::Format_RGBA8888_Premultiplied);
            break;
        case QImage::Format_RGBA64:
            image.convertTo(QImage::Format_RGBA8888);
            break;
        case QImage::Format_RGBA64_Premultiplied:
            image.convertTo(QImage::Format_RGBA8888_Premultiplied);
            break;
        case QImage::Format_RGBX64:
            image.convertTo(QImage::Format_RGBX8888);
            break;
        default:
            break;
        }
    }
    bool swizzleNeeded = image.pixelFormat().colorModel() == QPixelFormat::RGB
            && image.pixelFormat().typeInterpretation() == QPixelFormat::UnsignedInteger
            && systemIsLittleEndian;
    //??? What does inFormat mean? Is it even in use? Why always swizzle? Why do the musicians come out gradually?
    if (swizzleNeeded || inFormat != QSSGRenderTextureFormat::Unknown)
        image = std::move(image).rgbSwapped();
    image = std::move(image).mirrored();
    retval = new QSSGLoadedTexture;
    retval->width = image.width();
    retval->height = image.height();
    retval->components = image.pixelFormat().channelCount();
    retval->image = image;
    retval->data = (void *)retval->image.bits();
    retval->dataSizeInBytes = image.sizeInBytes();
    if (inFormat != QSSGRenderTextureFormat::Unknown)
        retval->format = inFormat;
    else
        retval->setFormatFromComponents();
    return retval;
}

QSSGRef<QSSGLoadedTexture> QSSGLoadedTexture::loadCompressedImage(const QString &inPath, const QSSGRenderTextureFormat &inFormat, bool inFlipY, const QSSGRenderContextType &renderContextType)
{
    Q_UNUSED(inFlipY)
    Q_UNUSED(renderContextType)

    QSSGRef<QSSGLoadedTexture> retval(nullptr);

    // Open File
    QFile imageFile(inPath);
    if (!imageFile.open(QIODevice::ReadOnly)) {
        qWarning() << "Could not open image file: " << inPath;
        return retval;
    }
    auto reader = new QTextureFileReader(&imageFile, inPath);

    if (!reader->canRead()) {
        qWarning() << "Unable to read image file: " << inPath;
        delete reader;
        return retval;
    }
    retval = new QSSGLoadedTexture;
    retval->compressedData = reader->read();
    if (inFormat != QSSGRenderTextureFormat::Unknown)
        retval->format = inFormat;

    delete reader;
    imageFile.close();

    return retval;

}

namespace {
typedef unsigned char RGBE[4];
#define R 0
#define G 1
#define B 2
#define E 3

#define MINELEN 8 // minimum scanline length for encoding
#define MAXELEN 0x7fff // maximum scanline length for encoding



inline int calculateLine(int width, int bitdepth) { return ((width * bitdepth) + 7) / 8; }

inline int calculatePitch(int line) { return (line + 3) & ~3; }

float convertComponent(int exponent, int val)
{
    float v = val / (256.0f);
    float d = powf(2.0f, (float)exponent - 128.0f);
    return v * d;
}

void decrunchScanline(const char *&p, const char *pEnd, RGBE *scanline, int w)
{
    scanline[0][R] = *p++;
    scanline[0][G] = *p++;
    scanline[0][B] = *p++;
    scanline[0][E] = *p++;

    if (scanline[0][R] == 2 && scanline[0][G] == 2 && scanline[0][B] < 128) {
        // new rle, the first pixel was a dummy
        for (int channel = 0; channel < 4; ++channel) {
            for (int x = 0; x < w && p < pEnd; ) {
                unsigned char c = *p++;
                if (c > 128) { // run
                    if (p < pEnd) {
                        int repCount = c & 127;
                        c = *p++;
                        while (repCount--)
                            scanline[x++][channel] = c;
                    }
                } else { // not a run
                    while (c-- && p < pEnd)
                        scanline[x++][channel] = *p++;
                }
            }
        }
    } else {
        // old rle
        scanline[0][R] = 2;
        int bitshift = 0;
        int x = 1;
        while (x < w && pEnd - p >= 4) {
            scanline[x][R] = *p++;
            scanline[x][G] = *p++;
            scanline[x][B] = *p++;
            scanline[x][E] = *p++;

            if (scanline[x][R] == 1 && scanline[x][G] == 1 && scanline[x][B] == 1) { // run
                int repCount = scanline[x][3] << bitshift;
                while (repCount--) {
                    memcpy(scanline[x], scanline[x - 1], 4);
                    ++x;
                }
                bitshift += 8;
            } else { // not a run
                ++x;
                bitshift = 0;
            }
        }
    }
}

void decodeScanlineToTexture(RGBE *scanline, int width, void *outBuf, quint32 offset, QSSGRenderTextureFormat inFormat)
{
    quint8 *target = reinterpret_cast<quint8 *>(outBuf);
    target += offset;

    if (inFormat == QSSGRenderTextureFormat::RGBE8) {
        memcpy(target, scanline, size_t(4 * width));
    } else {
        float rgbaF32[4];
        for (int i = 0; i < width; ++i) {
            rgbaF32[R] = convertComponent(scanline[i][E], scanline[i][R]);
            rgbaF32[G] = convertComponent(scanline[i][E], scanline[i][G]);
            rgbaF32[B] = convertComponent(scanline[i][E], scanline[i][B]);
            rgbaF32[3] = 1.0f;

            inFormat.encodeToPixel(rgbaF32, target, i * inFormat.getSizeofFormat());
        }
    }
}

}

QSSGRef<QSSGLoadedTexture> QSSGLoadedTexture::loadHdrImage(const QSharedPointer<QIODevice> &source, QSSGRenderContextType renderContextType)
{
    Q_UNUSED(renderContextType)
    QSSGRef<QSSGLoadedTexture> imageData(nullptr);

    char sig[256];
    source->read(sig, 11);
    if (strncmp(sig, "#?RADIANCE\n", 11))
        return imageData;

    QByteArray buf = source->readAll();
    const char *p = buf.constData();
    const char *pEnd = p + buf.size();

    // Process lines until the empty one.
    QByteArray line;
    while (p < pEnd) {
        char c = *p++;
        if (c == '\n') {
            if (line.isEmpty())
                break;
            if (line.startsWith(QByteArrayLiteral("FORMAT="))) {
                const QByteArray format = line.mid(7).trimmed();
                if (format != QByteArrayLiteral("32-bit_rle_rgbe")) {
                    qWarning("HDR format '%s' is not supported", format.constData());
                    return imageData;
                }
            }
            line.clear();
        } else {
            line.append(c);
        }
    }
    if (p == pEnd) {
        qWarning("Malformed HDR image data at property strings");
        return imageData;
    }

    // Get the resolution string.
    while (p < pEnd) {
        char c = *p++;
        if (c == '\n')
            break;
        line.append(c);
    }
    if (p == pEnd) {
        qWarning("Malformed HDR image data at resolution string");
        return imageData;
    }

    int width = 0;
    int height = 0;
    // We only care about the standard orientation.
    if (!sscanf(line.constData(), "-Y %d +X %d", &height, &width)) {
        qWarning("Unsupported HDR resolution string '%s'", line.constData());
        return imageData;
    }
    if (width <= 0 || height <= 0) {
        qWarning("Invalid HDR resolution");
        return imageData;
    }


    // Format
    QSSGRenderTextureFormat imageFormat(QSSGRenderTextureFormat::RGBE8);

    const int bytesPerPixel = imageFormat.getSizeofFormat();
    const int bitCount = bytesPerPixel * 8;
    const int pitch = calculatePitch(calculateLine(width, bitCount));
    const quint32 dataSize = quint32(height * pitch);
    imageData = new QSSGLoadedTexture;
    imageData->dataSizeInBytes = dataSize;
    imageData->data = ::malloc(dataSize);
    imageData->width = width;
    imageData->height = height;
    imageData->m_bitCount = bitCount;
    imageData->m_ExtendedFormat = QSSGExtendedTextureFormats::CustomRGB;
    imageData->format = imageFormat;
    imageData->components = imageFormat.getNumberOfComponent();

    // Allocate a scanline worth of RGBE data
    RGBE *scanline = new RGBE[width];

    // Note we are writing to the data buffer from bottom to top
    // to correct for -Y orientation
    for (int y = 0; y < height; ++y) {
        quint32 byteOffset = quint32((height - 1 - y) * width * bytesPerPixel);
        if (pEnd - p < 4) {
            qWarning("Unexpected end of HDR data");
            delete[] scanline;
            return imageData;
        }
        decrunchScanline(p, pEnd, scanline, width);
        decodeScanlineToTexture(scanline, width, imageData->data, byteOffset, imageFormat);
    }

    delete[] scanline;

    return imageData;
}

namespace {

bool scanImageForAlpha(const void *inData, quint32 inWidth, quint32 inHeight, quint32 inPixelSizeInBytes, quint8 inAlphaSizeInBits)
{
    const quint8 *rowPtr = reinterpret_cast<const quint8 *>(inData);
    bool hasAlpha = false;
    if (inAlphaSizeInBits == 0)
        return hasAlpha;
    if (inPixelSizeInBytes != 2 && inPixelSizeInBytes != 4) {
        Q_ASSERT(false);
        return false;
    }
    if (inAlphaSizeInBits > 8) {
        Q_ASSERT(false);
        return false;
    }

    quint32 alphaRightShift = inPixelSizeInBytes * 8 - inAlphaSizeInBits;
    quint32 maxAlphaValue = (1 << inAlphaSizeInBits) - 1;

    for (quint32 rowIdx = 0; rowIdx < inHeight && !hasAlpha; ++rowIdx) {
        for (quint32 idx = 0; idx < inWidth && !hasAlpha; ++idx, rowPtr += inPixelSizeInBytes) {
            quint32 pixelValue = 0;
            if (inPixelSizeInBytes == 2)
                pixelValue = *(reinterpret_cast<const quint16 *>(rowPtr));
            else
                pixelValue = *(reinterpret_cast<const quint32 *>(rowPtr));
            pixelValue = pixelValue >> alphaRightShift;
            if (pixelValue < maxAlphaValue)
                hasAlpha = true;
        }
    }
    return hasAlpha;
}
}

QSSGLoadedTexture::~QSSGLoadedTexture()
{
    if (data && image.sizeInBytes() <= 0) {
        ::free(data);
    }
    if (m_palette)
        ::free(m_palette);
    if (m_transparencyTable)
        ::free(m_transparencyTable);
}

bool QSSGLoadedTexture::scanForTransparency()
{
    switch (format.format) {
    case QSSGRenderTextureFormat::SRGB8A8:
    case QSSGRenderTextureFormat::RGBA8:
        if (!data) // dds
            return true;

        return scanImageForAlpha(data, width, height, 4, 8);
    // Scan the image.
    case QSSGRenderTextureFormat::SRGB8:
    case QSSGRenderTextureFormat::RGB8:
    case QSSGRenderTextureFormat::RGBE8:
        return false;
    case QSSGRenderTextureFormat::RGB565:
        return false;
    case QSSGRenderTextureFormat::RGBA5551:
        if (!data) { // dds
            return true;
        } else {
            return scanImageForAlpha(data, width, height, 2, 1);
        }
    case QSSGRenderTextureFormat::Alpha8:
        return true;
    case QSSGRenderTextureFormat::R8:
    case QSSGRenderTextureFormat::Luminance8:
    case QSSGRenderTextureFormat::RG8:
        return false;
    case QSSGRenderTextureFormat::LuminanceAlpha8:
        if (!data) // dds
            return true;

        return scanImageForAlpha(data, width, height, 2, 8);
    case QSSGRenderTextureFormat::RGB_DXT1:
        return false;
    case QSSGRenderTextureFormat::RGBA_DXT3:
    case QSSGRenderTextureFormat::RGBA_DXT1:
    case QSSGRenderTextureFormat::RGBA_DXT5:
        return false;
    case QSSGRenderTextureFormat::RGB9E5:
        return false;
    case QSSGRenderTextureFormat::RG32F:
    case QSSGRenderTextureFormat::RGB32F:
    case QSSGRenderTextureFormat::RGBA16F:
    case QSSGRenderTextureFormat::RGBA32F:
        // PKC TODO : For now, since IBL will be the main consumer, we'll just pretend there's no
        // alpha.
        // Need to do a proper scan down the line, but doing it for floats is a little different
        // from
        // integer scans.
        return false;
    default:
        break;
    }
    Q_ASSERT(false);
    return false;
}

QSSGRef<QSSGLoadedTexture> QSSGLoadedTexture::load(const QString &inPath,
                                                         const QSSGRenderTextureFormat &inFormat,
                                                         QSSGInputStreamFactory &inFactory,
                                                         bool inFlipY,
                                                         const QSSGRenderContextType &renderContextType)
{
    if (inPath.isEmpty())
        return nullptr;

    QSSGRef<QSSGLoadedTexture> theLoadedImage = nullptr;
    QSharedPointer<QIODevice> theStream(inFactory.getStreamForFile(inPath));
    QString fileName;
    inFactory.getPathForFile(inPath, fileName);
    if (theStream && inPath.size() > 3) {
        if (inPath.endsWith(QStringLiteral("png"), Qt::CaseInsensitive) || inPath.endsWith(QStringLiteral("jpg"), Qt::CaseInsensitive)
            || inPath.endsWith(QStringLiteral("peg"), Qt::CaseInsensitive)
            || inPath.endsWith(QStringLiteral("gif"), Qt::CaseInsensitive)
            || inPath.endsWith(QStringLiteral("bmp"), Qt::CaseInsensitive)) {
            theLoadedImage = loadQImage(fileName, inFormat, inFlipY, renderContextType);
        } else if (inPath.endsWith(QStringLiteral("dds"), Qt::CaseInsensitive)
                   || inPath.endsWith(QStringLiteral("ktx"), Qt::CaseInsensitive)
                   || inPath.endsWith(QStringLiteral("pkm"), Qt::CaseInsensitive)
                   || inPath.endsWith(QStringLiteral("astc"), Qt::CaseInsensitive)) {
            theLoadedImage = loadCompressedImage(fileName, inFormat, inFlipY, renderContextType);
        } else if (inPath.endsWith(QStringLiteral("hdr"), Qt::CaseInsensitive)) {
            theLoadedImage = loadHdrImage(theStream, renderContextType);
        } else {
            qCWarning(INTERNAL_ERROR, "Unrecognized image extension: %s", qPrintable(inPath));
        }
    }
    return theLoadedImage;
}

QT_END_NAMESPACE
