// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
// Qt-Security score:critical reason:data-parser

#include "qssglightmapio_p.h"

#include <private/qssgrenderloadedtexture_p.h>
#include <private/qssgassert_p.h>

#include <QDataStream>
#include <QDebug>
#include <QtEndian>
#include <QFile>

#include <algorithm>
#include <cstring>

QT_BEGIN_NAMESPACE

using IndexKey = std::tuple<QSSGLightmapIODataTag /* dataTag */, qint32 /* keySize */, QByteArray /* key */>;

struct IndexEntry
{
    qint64 keyOffset; // 8 bytes
    qint64 keySize; // 8 bytes
    qint64 dataOffset; // 8 bytes
    qint64 dataSize; // 8 bytes
    QSSGLightmapIODataTag dataTag; // 4 bytes
    quint32 padding;
};

struct QSSGLightmapIOPrivate
{
    QByteArray readKey(const IndexKey &indexKey) const;
    bool writeHeader() const;
    bool writeData(const QString &key, QSSGLightmapIODataTag tag, const QByteArray &data);
    bool writeFooter();
    bool decodeHeaders();
    QList<std::pair<QString, QSSGLightmapIODataTag>> getKeys() const;

    QSharedPointer<QIODevice> stream;
    QMap<IndexKey, IndexEntry> entries; // Maps from name -> entry
    qint64 entryCount = -1;
    qint64 indexOffset = -1;
    qint64 fileVersion = -1;
    qint64 fileSize = -1;
};

static_assert(offsetof(IndexEntry, keyOffset) == 0, "Unexpected alignment");
static_assert(offsetof(IndexEntry, keySize) == 8, "Unexpected alignment");
static_assert(offsetof(IndexEntry, dataOffset) == 16, "Unexpected alignment");
static_assert(offsetof(IndexEntry, dataSize) == 24, "Unexpected alignment");
static_assert(offsetof(IndexEntry, dataTag) == 32, "Unexpected alignment");
static_assert(offsetof(IndexEntry, padding) == 36, "Unexpected alignment");
static_assert(sizeof(IndexEntry) == 40, "Unexpected size");

constexpr char fileSignature[] = "QTLTMP";

static IndexKey keyToIndexKey(const QString &key, QSSGLightmapIODataTag tag)
{
    QByteArray keyBuffer = key.toUtf8();
    return std::make_tuple(tag, keyBuffer.size(), keyBuffer);
}

static QByteArray mapToByteArray(const QVariantMap &map)
{
    QByteArray byteArray;
    QDataStream out(&byteArray, QIODevice::WriteOnly);
    out.setVersion(QDataStream::Qt_6_9);
    out.setByteOrder(QDataStream::LittleEndian);
    out << QVariant(map);
    return byteArray;
}

static void convertEndian(QByteArray &buffer, int sizeOfDataType)
{
    if (QSysInfo::ByteOrder == QSysInfo::LittleEndian) {
        return; // No need to flip
    } else if (QSysInfo::ByteOrder == QSysInfo::BigEndian) {
        // Need to flip
    } else {
        qWarning() << "QSSGLightmapLoader::convertEndian: Unknown endianness";
        return;
    }

    Q_ASSERT(buffer.size() % sizeOfDataType == 0);
    if (buffer.size() % sizeOfDataType != 0) {
        qWarning() << "QSSGLightmapLoader::convertEndian: Unexpected buffer size";
        return;
    }

    if (sizeOfDataType == 1)
        return;

    const int buffSize = buffer.size();
    for (int offset = 0; offset < buffSize; offset += sizeOfDataType) {
        for (int i = 0; i < sizeOfDataType / 2; ++i) {
            std::swap(buffer[offset + i], buffer[offset + sizeOfDataType - 1 - i]);
        }
    }
}

static QVariantMap byteArrayToMap(QByteArray input)
{
    QVariant variant;
    QDataStream in(input);
    in.setVersion(QDataStream::Qt_6_9);
    in.setByteOrder(QDataStream::LittleEndian);
    in >> variant;
    return variant.toMap();
}

bool QSSGLightmapIOPrivate::decodeHeaders()
{
    Q_ASSERT(stream);

    if (!stream->isOpen())
        stream->open(QIODeviceBase::OpenModeFlag::ReadOnly);

    if (!stream->isOpen()) {
        qWarning() << "QSSGLightmapIO: Stream is not openable";
        return false;
    }

    if (!stream->isReadable()) {
        qWarning() << "QSSGLightmapIO: Stream is not readable";
        return false;
    }

    // [Magic Bytes][Version]
    constexpr int headerSize = 6 * sizeof(char) + sizeof(qint32);
    // [Entry Count][Offset Index]
    constexpr int footerSize = 2 * sizeof(qint64);

    const qint64 fileSize = stream->size();
    if (fileSize < headerSize + footerSize) {
        qWarning() << "QSSGLightmapIO: File too small to contain header and footer";
        return false;
    }

    stream->seek(0);
    QByteArray headerData = stream->read(headerSize);
    if (headerData.size() != qsizetype(headerSize)) {
        qWarning() << "Failed to read header";
        return false;
    }

    // Verify file signature
    if (QByteArrayView(headerData.constData(), 6) != QByteArray::fromRawData(fileSignature, 6)) {
        qWarning() << "QSSGLightmapIO: Invalid file signature";
        return false;
    }

    qint32 fileVersion = -1;

    // Read file version (4 bytes after 6 magic bytes)
    const char *versionPtr = headerData.constData() + 6;
    fileVersion = qFromLittleEndian<qint32>(versionPtr);

    if (fileVersion != 1) {
        qWarning() << "QSSGLightmapIO: Invalid file version";
        return false;
    }

    // Seek to the last bytes (footer)
    if (!stream->seek(fileSize - footerSize)) {
        qWarning() << "Failed to seek to footer";
        return false;
    }

    QByteArray footerData = stream->read(footerSize);
    if (footerData.size() != qsizetype(footerSize)) {
        qWarning() << "Failed to read footer";
        return false;
    }

    const char *footerPtr = footerData.constData();
    const qint64 indexOffset = qFromLittleEndian<qint64>(footerPtr);
    const qint64 entryCount = qFromLittleEndian<qint64>(footerPtr + sizeof(qint64));

    this->entryCount = entryCount;
    this->indexOffset = indexOffset;
    this->fileVersion = fileVersion;
    this->fileSize = fileSize;

    return true;
}

bool QSSGLightmapIOPrivate::writeHeader() const
{
    Q_ASSERT(stream);
    if (!stream->isOpen())
        stream->open(QIODeviceBase::OpenModeFlag::WriteOnly);

    if (!stream->isOpen()) {
        qWarning() << "QSSGLightmapIO: File is not openable";
        return false;
    }

    if (!stream->isWritable()) {
        qWarning() << "QSSGLightmapIO: File is not writable";
        return false;
    }

    stream->seek(0);

    // Write file signature
    if (stream->write(fileSignature, sizeof(fileSignature) - 1) != sizeof(fileSignature) - 1) {
        qWarning() << "QSSGLightmapIO: Failed to write file signature";
        stream->close();
        return false;
    }

    // Write file version
    const qint32 version = qToLittleEndian<qint32>(1);
    if (stream->write(reinterpret_cast<const char *>(&version), sizeof(version)) != sizeof(version)) {
        qWarning() << "QSSGLightmapIO: Failed to write file version";
        stream->close();
        return false;
    }

    return true;
}

bool QSSGLightmapIOPrivate::writeData(const QString &key, QSSGLightmapIODataTag tag, const QByteArray &data)
{
    Q_ASSERT(stream->isOpen() && stream->isWritable());
    IndexKey keyBytes = keyToIndexKey(key, tag);
    Q_ASSERT(!entries.contains(keyBytes));

    IndexEntry &entry = entries[keyBytes];
    entry.dataOffset = stream->pos();
    entry.dataSize = data.size();
    entry.dataTag = tag;

    if (stream->write(data) != data.size()) {
        qWarning() << "QSSGLightmapIO: Failed to write entry data";
        return false;
    }

    return true;
}

template<typename T>
bool writeType(const QSharedPointer<QIODevice> &stream, T value)
{
    return stream->write(reinterpret_cast<const char *>(&value), sizeof(value)) == sizeof(value);
}

bool QSSGLightmapIOPrivate::writeFooter()
{
    Q_ASSERT(stream);
    Q_ASSERT(stream->isOpen());
    Q_ASSERT(stream->isWritable());

    // Store the key strings
    for (auto it = entries.begin(); it != entries.end(); ++it) {
        auto [dataTag, keySize, key] = it.key();
        IndexEntry &entry = it.value();
        entry.keyOffset = stream->pos();
        entry.keySize = keySize;
        if (stream->write(key) != key.size()) {
            qWarning() << "QSSGLightmapIO: Failed to write key";
            return false;
        }
    }

    // The file should be seeked to the end of the data segment so we can just
    // add indices and footer now
    const qint64 indexOffset = qToLittleEndian<qint64>(stream->pos());
    const qint64 indexCount = qToLittleEndian<qint64>(entries.size());

    for (const IndexEntry &entry : std::as_const(entries)) {
        const qint64 keyOffset = qToLittleEndian<qint64>(entry.keyOffset);
        const qint64 keySize = qToLittleEndian<qint64>(entry.keySize);
        const qint64 dataOffset = qToLittleEndian<qint64>(entry.dataOffset);
        const qint64 dataSize = qToLittleEndian<qint64>(entry.dataSize);
        const quint32 dataTag = qToLittleEndian<quint32>(std::underlying_type_t<QSSGLightmapIODataTag>(entry.dataTag));
        const quint32 padding = qToLittleEndian<quint32>(entry.padding);
        if (!writeType(stream, keyOffset) || !writeType(stream, keySize) || !writeType(stream, dataOffset)
            || !writeType(stream, dataSize) || !writeType(stream, dataTag) || !writeType(stream, padding)) {
            qWarning() << "QSSGLightmapIO: Failed to write entry";
            return false;
        }
    }

    // Write footer
    if (!writeType(stream, indexOffset) || !writeType(stream, indexCount)) {
        qWarning() << "QSSGLightmapIO: Failed to write footer";
        return false;
    }
    stream->close();
    return true;
}

QByteArray QSSGLightmapIOPrivate::readKey(const IndexKey &indexKey) const
{
    Q_ASSERT(stream);
    Q_ASSERT(entryCount >= 0);
    Q_ASSERT(indexOffset >= 0);
    Q_ASSERT(fileVersion >= 0);
    Q_ASSERT(fileSize >= 0);

    // Perform binary search by reading one IndexEntry at a time
    qint64 low = 0;
    qint64 high = entryCount;
    IndexEntry entry;
    auto [dataTag, keySize, key] = indexKey;
    bool found = false;
    qint64 matchOffset = 0;
    qint64 matchSize = 0;

    while (low < high) {
        qint64 mid = (low + high) / 2;
        qint64 offset = indexOffset + mid * sizeof(IndexEntry);

        if (!stream->seek(offset)) {
            qWarning() << "Failed to seek to index entry";
            return {};
        }

        if (stream->read(reinterpret_cast<char *>(&entry), sizeof(IndexEntry)) != sizeof(IndexEntry)) {
            qWarning() << "Failed to read index entry";
            return {};
        }

        // Sort by dataTag, keySize and name (matching the order of IndexKey)
        int cmp = qint64(entry.dataTag) - qint64(dataTag);
        if (cmp == 0) {
            cmp = entry.keySize - keySize;
        }
        if (cmp == 0) {
            if (!stream->seek(entry.keyOffset)) {
                qWarning() << "Failed to seek to key entry";
                return {};
            }
            const QByteArray entryKey = stream->read(entry.keySize);
            if (entryKey.size() != entry.keySize) {
                qWarning() << "Failed to read to key entry";
                return {};
            }
            for (int i = 0, n = entry.keySize; i < n; ++i) {
                cmp = int(entryKey[i]) - int(key[i]);
                if (cmp != 0)
                    break;
            }
        }

        if (cmp < 0) {
            low = mid + 1;
        } else if (cmp > 0) {
            high = mid;
        } else {
            // Found
            found = true;
            matchOffset = qFromLittleEndian(entry.dataOffset);
            matchSize = qFromLittleEndian(entry.dataSize);
            break;
        }
    }

    if (!found) {
        qWarning() << "Key not found:" << key;
        return {};
    }

    if (matchOffset + matchSize > fileSize) {
        qWarning() << "Asset data out of bounds";
        return {};
    }

    // Seek and read asset
    if (!stream->seek(matchOffset)) {
        qWarning() << "Failed to seek to asset data";
        return {};
    }

    QByteArray assetData = stream->read(matchSize);
    if (assetData.size() != static_cast<int>(matchSize)) {
        qWarning() << "Failed to read full asset data";
        return {};
    }

    return assetData;
}

QList<std::pair<QString, QSSGLightmapIODataTag>> QSSGLightmapIOPrivate::getKeys() const
{
    Q_ASSERT(stream);
    Q_ASSERT(entryCount >= 0);
    Q_ASSERT(indexOffset >= 0);
    Q_ASSERT(fileVersion >= 0);
    Q_ASSERT(fileSize >= 0);

    QList<std::pair<QString, QSSGLightmapIODataTag>> keys;
    keys.resize(entryCount);

    IndexEntry entry;

    for (int i = 0; i < entryCount; ++i) {
        const qint64 offset = indexOffset + i * sizeof(IndexEntry);

        if (!stream->seek(offset)) {
            qWarning() << "Failed to seek to index entry";
            return {};
        }

        if (stream->read(reinterpret_cast<char *>(&entry), sizeof(IndexEntry)) != sizeof(IndexEntry)) {
            qWarning() << "Failed to read index entry";
            return {};
        }
        if (!stream->seek(entry.keyOffset)) {
            qWarning() << "Failed to seek to key entry";
            return {};
        }
        const QByteArray entryKey = stream->read(entry.keySize);
        if (entryKey.size() != entry.keySize) {
            qWarning() << "Failed to read to key entry";
            return {};
        }

        keys[i] = std::make_pair(QString::fromUtf8(entryKey), static_cast<QSSGLightmapIODataTag>(entry.dataTag));
    }

    std::sort(keys.begin(), keys.end(), [](const auto &a, const auto &b) { return a.first < b.first; });

    return keys;
}

/////////////////////////////// Loader /////////////////////////////////

QSSGLightmapLoader::QSSGLightmapLoader() : d(new QSSGLightmapIOPrivate) { };

QSSGLightmapLoader::~QSSGLightmapLoader()
{
    if (d->stream)
        d->stream->close();
    delete d;
}

QSharedPointer<QSSGLightmapLoader> QSSGLightmapLoader::open(const QSharedPointer<QIODevice> &stream)
{
    if (!stream) {
        qWarning() << "Failed to open lightmap: invalid stream";
        return nullptr;
    }

    auto loader = QSharedPointer<QSSGLightmapLoader>(new QSSGLightmapLoader);
    loader->d->stream = stream;

    if (!loader->d->decodeHeaders()) {
        if (loader->d->stream->isOpen())
            loader->d->stream->close();
        return {};
    }

    return loader;
}

QSharedPointer<QSSGLightmapLoader> QSSGLightmapLoader::open(const QString &path)
{
    if (QSharedPointer<QIODevice> source = QSSGInputUtil::getStreamForFile(path))
        return open(source);

    return nullptr;
}

QByteArray QSSGLightmapLoader::readF32Image(const QString &key, QSSGLightmapIODataTag tag) const
{
    QByteArray buffer = d->readKey(keyToIndexKey(key, tag));
    convertEndian(buffer, sizeof(float));
    return buffer;
}

QByteArray QSSGLightmapLoader::readU32Image(const QString &key, QSSGLightmapIODataTag tag) const
{
    QByteArray buffer = d->readKey(keyToIndexKey(key, tag));
    convertEndian(buffer, sizeof(quint32));
    return buffer;
}

QByteArray QSSGLightmapLoader::readData(const QString &key, QSSGLightmapIODataTag tag) const
{
    return d->readKey(keyToIndexKey(key, tag));
}

QVariantMap QSSGLightmapLoader::readMetadata(const QString &key) const
{
    QByteArray metadataBuffer = d->readKey(keyToIndexKey(key, QSSGLightmapIODataTag::Metadata));
    QVariantMap metadata = byteArrayToMap(metadataBuffer);
    return metadata;
}

QList<std::pair<QString, QSSGLightmapIODataTag>> QSSGLightmapLoader::getKeys() const
{
    return d->getKeys();
}

inline int calculateLine(int width, int bitdepth)
{
    return ((width * bitdepth) + 7) / 8;
}
inline int calculatePitch(int line)
{
    return (line + 3) & ~3;
}

QSSGLoadedTexture *QSSGLightmapLoader::createTexture(QSharedPointer<QIODevice> stream,
                                                     const QSSGRenderTextureFormat &format,
                                                     const QString &key)
{
    QSharedPointer<QSSGLightmapLoader> loader = QSSGLightmapLoader::open(stream);
    if (!loader)
        return nullptr;

    QVariantMap metadata = loader->readMetadata(key);
    if (metadata.isEmpty())
        return nullptr;

    bool ok = true;
    const int w = metadata[QStringLiteral("width")].toInt(&ok);
    Q_ASSERT(ok);
    const int h = metadata[QStringLiteral("height")].toInt(&ok);
    Q_ASSERT(ok);
    QSize pixelSize = QSize(w, h);
    QByteArray imageFP32 = loader->readF32Image(key, QSSGLightmapIODataTag::Texture_Final);
    if (imageFP32.isEmpty())
        return nullptr;

    // Setup Output container
    const int bytesPerPixel = format.getSizeofFormat();
    const int bitCount = bytesPerPixel * 8;
    const int pitch = calculatePitch(calculateLine(pixelSize.width(), bitCount));
    const size_t dataSize = pixelSize.height() * pitch;
    QSSG_CHECK_X(dataSize <= std::numeric_limits<quint32>::max(), "Requested data size exceeds 4GB limit!");
    QSSGLoadedTexture *imageData = new QSSGLoadedTexture;
    imageData->dataSizeInBytes = quint32(dataSize);
    imageData->data = ::malloc(imageData->dataSizeInBytes);
    imageData->width = pixelSize.width();
    imageData->height = pixelSize.height();
    imageData->format = format;
    imageData->components = format.getNumberOfComponent();
    imageData->isSRGB = false;

    std::array<float, 4> *source = reinterpret_cast<std::array<float, 4> *>(imageFP32.data());
    quint8 *target = reinterpret_cast<quint8 *>(imageData->data);

    int idx = 0;
    float rgbaF32[4];

    for (int y = imageData->height - 1; y >= 0; --y) {
        for (int x = 0; x < imageData->width; x++) {
            const int i = y * imageData->width + x;
            const int lh = i * 4 * sizeof(float);
            const int rh = imageFP32.size();
            Q_ASSERT(lh < rh);
            std::array<float, 4> v = source[i];
            rgbaF32[0] = v[0];
            rgbaF32[1] = v[1];
            rgbaF32[2] = v[2];
            rgbaF32[3] = v[3];

            format.encodeToPixel(rgbaF32, target, idx * bytesPerPixel);
            ++idx;
        }
    }

    return imageData;
}

///////////////////////////// Writer ///////////////////////////////////

QSSGLightmapWriter::QSSGLightmapWriter() : d(new QSSGLightmapIOPrivate) { }

QSSGLightmapWriter::~QSSGLightmapWriter()
{
    if (d->stream->isOpen()) {
        qWarning() << "Lightmap file open on destruction, closing";
        close();
    }
    delete d;
}

QSharedPointer<QSSGLightmapWriter> QSSGLightmapWriter::open(const QString &path)
{
    return open(QSharedPointer<QIODevice>(new QFile(path)));
}

QSharedPointer<QSSGLightmapWriter> QSSGLightmapWriter::open(const QSharedPointer<QIODevice> &stream)
{
    if (!stream) {
        qWarning() << "Failed to open lightmap file";
        return nullptr;
    }

    QSharedPointer<QSSGLightmapWriter> writer = QSharedPointer<QSSGLightmapWriter>(new QSSGLightmapWriter);
    if (!stream->isOpen() && !stream->open(QIODeviceBase::WriteOnly)) {
        qWarning() << "Failed to open lightmap file";
        return nullptr;
    }

    writer->d->stream = stream;
    if (!writer->d->writeHeader())
        return nullptr;

    return writer;
}

bool QSSGLightmapWriter::writeF32Image(const QString &key, QSSGLightmapIODataTag tag, const QByteArray &imageFP32)
{
    QByteArray buffer = QByteArray(imageFP32.constData(), imageFP32.size());
    convertEndian(buffer, sizeof(float));
    return d->writeData(key, tag, buffer);
}

bool QSSGLightmapWriter::writeU32Image(const QString &key, QSSGLightmapIODataTag tag, const QByteArray &imageU32)
{
    QByteArray buffer = QByteArray(imageU32.constData(), imageU32.size());
    convertEndian(buffer, sizeof(quint32));
    return d->writeData(key, tag, buffer);
}

bool QSSGLightmapWriter::writeData(const QString &key, QSSGLightmapIODataTag tag, const QByteArray &buffer)
{
    return d->writeData(key, tag, buffer);
}

bool QSSGLightmapWriter::writeMetadata(const QString &key, const QVariantMap &metadata)
{
    return d->writeData(key, QSSGLightmapIODataTag::Metadata, mapToByteArray(metadata));
}

bool QSSGLightmapWriter::close() const
{
    return d->writeFooter();
}

QT_END_NAMESPACE
