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

#include <QtTest>

#include <QtCore/qbytearray.h>
#include <QtCore/qfile.h>

#include <QtGui/private/qshader_p.h>

#include <QtShaderTools/private/qshaderbaker_p.h>

#include <QtQuick3DUtils/private/qqsbcollection_p.h>

static const char *shaderDescription() { return "ShaderDescription"; }
static const char *tempOutFileName() { return "qsbctstfile.qsbc"; }

static const char *vertSource() {
    return "#version 310 es\n"
           "layout (location = 0) in vec3 pos;\n"
           "layout (location = 1) out vec4 vertColor;\n"
           "void main()\n"
           "{\n"
           "    gl_Position = vec4(pos, 1.0);\n"
           "    vertColor = vec4(0.0, 1.0, 0.0, 1.0);\n"
           "}\n";
}
static const char *fragSource() {
    return "#version 310 es\n"
            "precision lowp float;\n"
            "layout (location = 0) out vec4 glColor;\n"
            "layout (location = 1) in vec4 vertColor;\n"
            "void main()\n"
            "{\n"
            "    glColor = vertColor;\n"
            "}\n";
}

class ShaderCollection : public QObject
{
    Q_OBJECT
public:
    ShaderCollection() = default;
    ~ShaderCollection() = default;

private Q_SLOTS:
    void initTestCase();
    void test_readWriteToFile();
    void test_readWriteToBuffer();
    void test_readWriteOpenDevice();
    void test_mapModes();
    void test_inMemoryCollection();

private:
    QShader vert;
    QShader frag;
    QQsbCollection::FeatureSet featureSet;
};

void ShaderCollection::initTestCase()
{
    QShaderBaker baker;
    baker.setGeneratedShaders({{ QShader::GlslShader, QShaderVersion(100, QShaderVersion::GlslEs) }});
    baker.setGeneratedShaderVariants({ QShader::StandardShader });
    baker.setSourceString(vertSource(), QShader::VertexStage);
    vert = baker.bake();
    QVERIFY2(vert.isValid(), qPrintable(baker.errorMessage()));

    baker.setSourceString(fragSource(), QShader::FragmentStage);
    frag = baker.bake();
    QVERIFY2(frag.isValid(), qPrintable(baker.errorMessage()));

    featureSet.insert("feature1", true);
    featureSet.insert("feature2", false);
}

void ShaderCollection::test_readWriteToFile()
{
    const QByteArray hkey = QByteArrayLiteral("12345");
    const auto tempFile = QDir::tempPath() + QDir::separator() + tempOutFileName();
    QQsbCollection::Entry entry;
    {
        QQsbIODeviceCollection qsbc(tempFile);
        QVERIFY(qsbc.map(QQsbIODeviceCollection::Write));
        entry = qsbc.addEntry(hkey, { QByteArray(shaderDescription()), featureSet, vert, frag });
        QVERIFY(entry.value != -1);
        QCOMPARE(entry.key, hkey);
        const auto entries = qsbc.availableEntries();
        QCOMPARE(entries.size(), 1);
        QCOMPARE(*entries.begin(), entry);
        qsbc.unmap();
    }

    {
        QQsbIODeviceCollection qsbc(tempFile);
        QVERIFY(qsbc.map(QQsbIODeviceCollection::Read));
        const auto entries = qsbc.availableEntries();
        QCOMPARE(entries.size(), 1);
        QCOMPARE(*entries.begin(), entry);
        QQsbCollection::EntryDesc entryDesc;
        QVERIFY(qsbc.extractEntry(entry, entryDesc));
        QCOMPARE(entryDesc.materialKey, QByteArray(shaderDescription()));
        QCOMPARE(entryDesc.vertShader, vert);
        QCOMPARE(entryDesc.fragShader, frag);
        QCOMPARE(entryDesc.featureSet, featureSet);
        qsbc.unmap();
    }
}

void ShaderCollection::test_readWriteToBuffer()
{
    QBuffer buffer;
    const QByteArray hkey = QByteArrayLiteral("12345");
    QQsbCollection::Entry entry;
    {
        QVERIFY(buffer.buffer().isEmpty());
        QQsbIODeviceCollection qsbc(buffer);
        QVERIFY(qsbc.map(QQsbIODeviceCollection::Write));
        entry = qsbc.addEntry(hkey, { QByteArray(shaderDescription()), featureSet, vert, frag });
        QVERIFY(entry.value != -1);
        QCOMPARE(entry.key, hkey);
        qsbc.unmap();
        QVERIFY(!buffer.buffer().isEmpty());
    }

    {
        QQsbIODeviceCollection qsbc(buffer);
        QVERIFY(qsbc.map(QQsbIODeviceCollection::Read));
        QQsbCollection::EntryDesc entryDesc;
        QVERIFY(qsbc.extractEntry(entry, entryDesc));
        QCOMPARE(entryDesc.materialKey, QByteArray(shaderDescription()));
        QCOMPARE(entryDesc.vertShader, vert);
        QCOMPARE(entryDesc.fragShader, frag);
        QCOMPARE(entryDesc.featureSet, featureSet);
        qsbc.unmap();
    }
}

void ShaderCollection::test_readWriteOpenDevice()
{
    QBuffer buffer;
    const QByteArray hkey = QByteArrayLiteral("12345");
    QQsbCollection::Entry entry;
    {
        buffer.open(QIODevice::WriteOnly | QIODevice::Truncate);
        QCOMPARE(buffer.isOpen(), true);
        QVERIFY(buffer.buffer().isEmpty());
        QQsbIODeviceCollection qsbc(buffer);
        QVERIFY(qsbc.map(QQsbIODeviceCollection::Write));
        entry = qsbc.addEntry(hkey, { QByteArray(shaderDescription()), featureSet, vert, frag });
        QVERIFY(entry.value != -1);
        QCOMPARE(entry.key, hkey);
        qsbc.unmap();
        QVERIFY(!buffer.buffer().isEmpty());
    }

    QCOMPARE(buffer.isOpen(), false);

    {
        buffer.open(QIODevice::ReadOnly);
        QCOMPARE(buffer.isOpen(), true);
        QQsbIODeviceCollection qsbc(buffer);
        QVERIFY(qsbc.map(QQsbIODeviceCollection::Read));
        QQsbCollection::EntryDesc entryDesc;
        QVERIFY(qsbc.extractEntry(entry, entryDesc));
        QCOMPARE(entryDesc.materialKey, QByteArray(shaderDescription()));
        QCOMPARE(entryDesc.vertShader, vert);
        QCOMPARE(entryDesc.fragShader, frag);
        QCOMPARE(entryDesc.featureSet, featureSet);
        qsbc.unmap();
    }
}

void ShaderCollection::test_mapModes()
{
    {
        QBuffer buffer;
        const QByteArray hkey = QByteArrayLiteral("12345");
        QQsbCollection::Entry entry;
        {
            buffer.open(QIODevice::ReadWrite | QIODevice::Truncate);
            QCOMPARE(buffer.isOpen(), true);
            QVERIFY(buffer.buffer().isEmpty());
            QQsbIODeviceCollection qsbc(buffer);
            QVERIFY(qsbc.map(QQsbIODeviceCollection::Write));
            entry = qsbc.addEntry(hkey, { QByteArray(shaderDescription()), featureSet, vert, frag });
            QVERIFY(entry.value != -1);
            QCOMPARE(entry.key, hkey);
            qsbc.unmap();
            QVERIFY(!buffer.buffer().isEmpty());
        }

        QCOMPARE(buffer.isOpen(), false);

        {
            buffer.open(QIODevice::ReadOnly);
            QCOMPARE(buffer.isOpen(), true);
            QQsbIODeviceCollection qsbc(buffer);
            QVERIFY(qsbc.map(QQsbIODeviceCollection::Read));
            QQsbCollection::EntryDesc entryDesc;
            QVERIFY(qsbc.extractEntry(entry, entryDesc));
            QCOMPARE(entryDesc.materialKey, QByteArray(shaderDescription()));
            QCOMPARE(entryDesc.vertShader, vert);
            QCOMPARE(entryDesc.fragShader, frag);
            QCOMPARE(entryDesc.featureSet, featureSet);
            qsbc.unmap();
        }
    }

    {
        QBuffer buffer;
        const QByteArray hkey = QByteArrayLiteral("12345");
        QQsbCollection::Entry entry;

        { // Test invalid input (missing Truncate)
            buffer.open(QIODevice::ReadWrite);
            QCOMPARE(buffer.isOpen(), true);
            QVERIFY(buffer.buffer().isEmpty());
            QQsbIODeviceCollection qsbc(buffer);
            QCOMPARE(qsbc.map(QQsbIODeviceCollection::Write), false);
            entry = qsbc.addEntry(hkey, { QByteArray(shaderDescription()), featureSet, vert, frag });
            QCOMPARE(entry.isValid(), false);
            qsbc.unmap();
            QVERIFY(buffer.buffer().isEmpty());
        }

        { // Test invalid input (Text)
            buffer.open(QIODevice::ReadWrite | QIODevice::Truncate | QIODevice::Text);
            QCOMPARE(buffer.isOpen(), true);
            QVERIFY(buffer.buffer().isEmpty());
            QQsbIODeviceCollection qsbc(buffer);
            QCOMPARE(qsbc.map(QQsbIODeviceCollection::Write), false);
            entry = qsbc.addEntry(hkey, { QByteArray(shaderDescription()), featureSet, vert, frag });
            QCOMPARE(entry.isValid(), false);
            qsbc.unmap();
            QVERIFY(buffer.buffer().isEmpty());
        }
    }
}

void ShaderCollection::test_inMemoryCollection()
{
    QQsbInMemoryCollection qsbc;
    QVERIFY(qsbc.availableEntries().isEmpty());

    const QByteArray hkey = QByteArrayLiteral("12345");
    const QByteArray otherKey = QByteArrayLiteral("12346");
    QQsbCollection::Entry entry;
    QVERIFY(!entry.isValid());

    {
        entry = qsbc.addEntry(hkey, { QByteArray(shaderDescription()), featureSet, vert, frag });
        QVERIFY(entry.isValid());
        QVERIFY(qsbc.availableEntries().contains(entry));
        qsbc.clear();
        QVERIFY(qsbc.availableEntries().isEmpty());
    }

    {
        entry = qsbc.addEntry(hkey, { QByteArray(shaderDescription()), featureSet, vert, frag });
        QVERIFY(entry.isValid());
        entry = qsbc.addEntry(hkey, { QByteArray(shaderDescription()), featureSet, vert, frag });
        QVERIFY(!entry.isValid()); // already added, this must fail
        entry = qsbc.addEntry(otherKey, { QByteArray(shaderDescription()), featureSet, vert, frag });
        QVERIFY(entry.isValid());
        QVERIFY(qsbc.availableEntries().count() == 2);

        QQsbCollection::EntryDesc entryDesc;
        QVERIFY(qsbc.extractEntry(entry, entryDesc));
        QCOMPARE(entryDesc.materialKey, QByteArray(shaderDescription()));
        QCOMPARE(entryDesc.vertShader, vert);
        QCOMPARE(entryDesc.fragShader, frag);
        QCOMPARE(entryDesc.featureSet, featureSet);
    }

}

QTEST_APPLESS_MAIN(ShaderCollection)

#include "tst_shadercollection.moc"
