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

#include <QtTest>

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

#include <QtQuick3D/private/qquick3dprincipledmaterial_p.h>
#include <QtQuick3D/private/qquick3ddefaultmaterial_p.h>
#include <QtQuick3D/private/qquick3dcustommaterial_p.h>
#include <QtQuick3D/private/qquick3deffect_p.h>
#include <QtQuick3D/private/qquick3dmodel_p.h>
#include <QtQuick3D/private/qquick3dinstancing_p.h>

#include <parser.h>

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

private Q_SLOTS:
    void initTestCase();
    void tst_principledMaterialComponent();
    void tst_defaultMaterialComponent();
    void tst_customMaterialComponent();
    void tst_customMaterialUniforms_data();
    void tst_customMaterialUniforms();
    void tst_effectComponent();
    void tst_effectUniforms_data();
    void tst_effectUniforms();
    void tst_componentResolving();
    void tst_instancing();

private:
    MaterialParser::SceneData lastSceneData;
};

void Shadergen::initTestCase()
{
}

void Shadergen::tst_principledMaterialComponent()
{
    QFile file(QLatin1String(":/qml/PrincipledMaterialA.qml"));
    QVERIFY(file.open(QIODevice::ReadOnly));
    QByteArray data = file.readAll();

    MaterialParser::SceneData sceneData;
    MaterialParser::parseQmlData(data, file.fileName(), sceneData);

    QCOMPARE(sceneData.materials.size(), 1);
    QQuick3DPrincipledMaterial *mat = qobject_cast<QQuick3DPrincipledMaterial *>(sceneData.materials.at(0));
    QVERIFY(qFuzzyCompare(mat->metalness(), 0.1f));
    QVERIFY(qFuzzyCompare(mat->roughness(), 0.2f));
    QCOMPARE(mat->baseColor(), QColor("green"));
}

void Shadergen::tst_defaultMaterialComponent()
{
    QFile file(QLatin1String(":/qml/DefaultMaterialA.qml"));
    QVERIFY(file.open(QIODevice::ReadOnly));
    QByteArray data = file.readAll();

    MaterialParser::SceneData sceneData;
    MaterialParser::parseQmlData(data, file.fileName(), sceneData);

    QCOMPARE(sceneData.materials.size(), 1);
    QQuick3DDefaultMaterial *mat = qobject_cast<QQuick3DDefaultMaterial *>(sceneData.materials.at(0));
    QVERIFY(qFuzzyCompare(mat->specularAmount(), 0.1f));
    QVERIFY(qFuzzyCompare(mat->specularRoughness(), 0.2f));
    QCOMPARE(mat->diffuseColor(), QColor("pink"));
}

void Shadergen::tst_customMaterialComponent()
{
    QFile file(QLatin1String(":/qml/CustomMaterialA.qml"));
    QVERIFY(file.open(QIODevice::ReadOnly));
    QByteArray data = file.readAll();

    auto &sceneData = lastSceneData;
    MaterialParser::parseQmlData(data, file.fileName(), sceneData);

    QCOMPARE(sceneData.materials.size(), 1);
    QQuick3DCustomMaterial *mat = qobject_cast<QQuick3DCustomMaterial *>(sceneData.materials.at(0));
    QCOMPARE(mat->shadingMode(), QQuick3DCustomMaterial::ShadingMode::Unshaded);
}

void Shadergen::tst_customMaterialUniforms_data()
{
    QTest::addColumn<QMetaType>("type");
    QTest::addColumn<QString>("name");
    QTest::addColumn<QVariant>("value");

    QTest::newRow("bool0") << QMetaType(QMetaType::Bool) << "uBoolFalse" << QVariant::fromValue(false);
    QTest::newRow("bool1") << QMetaType(QMetaType::Bool) << "uBoolTrue" << QVariant::fromValue(true);
    QTest::newRow("int") << QMetaType(QMetaType::Double) /*Not a typo*/ << "uInt" << QVariant::fromValue(int(33));
    QTest::newRow("real") << QMetaType(QMetaType::Double) << "uReal" << QVariant::fromValue(qreal(3.3));
    QTest::newRow("pointS") << QMetaType(QMetaType::QPointF) << "uPointS" << QVariant::fromValue(QPointF(0, 1));
    QTest::newRow("pointF") << QMetaType(QMetaType::QPointF) << "uPointF" << QVariant::fromValue(QPointF(1, 0));
    QTest::newRow("sizeS") << QMetaType(QMetaType::QSizeF) << "uSizeS" << QVariant::fromValue(QSizeF(1, 1));
    QTest::newRow("sizeF") << QMetaType(QMetaType::QSizeF) << "uSizeF" << QVariant::fromValue(QSizeF(2, 2));
    QTest::newRow("rectS") << QMetaType(QMetaType::QRectF) << "uRectS" << QVariant::fromValue(QRectF(0, 1, 100, 101));
    QTest::newRow("rectF") << QMetaType(QMetaType::QRectF) << "uRectF" << QVariant::fromValue(QRectF(1, 0, 101, 100));
    QTest::newRow("vec2") << QMetaType(QMetaType::QVector2D) << "uVec2" << QVariant::fromValue(QVector2D(1, 2));
    QTest::newRow("vec3") << QMetaType(QMetaType::QVector3D) << "uVec3" << QVariant::fromValue(QVector3D(1, 2, 3));
    QTest::newRow("vec4") << QMetaType(QMetaType::QVector4D) << "uVec4" << QVariant::fromValue(QVector4D(1, 2, 3, 4));
    QTest::newRow("quat") << QMetaType(QMetaType::QQuaternion) << "uQuat" << QVariant::fromValue(QQuaternion(1, 2, 3, 4));
    QTest::newRow("m44") << QMetaType(QMetaType::QMatrix4x4) << "uM44"
                         << QVariant::fromValue(QMatrix4x4(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16));
    QTest::newRow("Color1") << QMetaType(QMetaType::QColor) << "uColor1" << QVariant::fromValue(QColor("green"));
    QTest::newRow("Color2") << QMetaType(QMetaType::QColor) << "uColor2" << QVariant::fromValue(QColor("#ff0000"));
    QTest::newRow("Texture") << QMetaType(qMetaTypeId<QQuick3DTexture *>()) << "uTex"
                             << QVariant::fromValue((QQuick3DTexture *)nullptr);
    QTest::newRow("TextureInput") << QMetaType(qMetaTypeId<QQuick3DShaderUtilsTextureInput *>()) << "uTexInput"
                                  << QVariant::fromValue((QQuick3DShaderUtilsTextureInput *)nullptr);
}

void Shadergen::tst_customMaterialUniforms()
{
    // Test that public QML members are added to the object
    const auto &sceneData = lastSceneData;
    QCOMPARE(sceneData.materials.size(), 1);
    QQuick3DCustomMaterial *mat = qobject_cast<QQuick3DCustomMaterial *>(sceneData.materials.at(0));
    QCOMPARE(mat->shadingMode(), QQuick3DCustomMaterial::ShadingMode::Unshaded);

    QFETCH(QMetaType, type);
    QFETCH(QString, name);
    QFETCH(QVariant, value);

    const auto prop = mat->property(name.toLatin1().constData());
    QVERIFY(prop.isValid());
    QCOMPARE(prop.metaType(), type);
    if (type == QMetaType(QMetaType::Double)) {
        QVERIFY(qFuzzyCompare(prop.toDouble(), value.toDouble()));
    } else if (type.id() >= QMetaType::User) {
        const bool ok = (prop.metaType().id() == qMetaTypeId<QQuick3DTexture *>()) ||
                (prop.metaType().id() == qMetaTypeId<QQuick3DShaderUtilsTextureInput *>());
        if (!ok) {
            qDebug() << "Texture: " << qMetaTypeId<QQuick3DTexture *>();
            qDebug() << "TextureInput: " << qMetaTypeId<QQuick3DShaderUtilsTextureInput *>();
            qDebug() << prop.metaType().id();
        }
        QVERIFY(ok);
    } else {
        QCOMPARE(prop, value);
    }
}

void Shadergen::tst_effectComponent()
{
    QFile file(QLatin1String(":/qml/EffectA.qml"));
    QVERIFY(file.open(QIODevice::ReadOnly));
    QByteArray data = file.readAll();

    MaterialParser::SceneData sceneData;
    MaterialParser::parseQmlData(data, file.fileName(), sceneData);

    QCOMPARE(sceneData.effects.size(), 1);
    QQuick3DEffect *effect = sceneData.effects.at(0);
    auto passes = effect->passes();
    QCOMPARE(passes.count(&passes), 1);
    auto pass = passes.at(&passes, 0);
    QVERIFY(pass);
    auto shaders = pass->shaders();
    {
        const auto count = shaders.count(&shaders);
        QCOMPARE(count, 2);
        // Vertex shader
        auto shader = shaders.at(&shaders, 0);
        // Shader url
        QVERIFY(!shader->shader.isEmpty());
        QCOMPARE(shader->stage, QQuick3DShaderUtilsShader::Stage::Vertex);

        // Fragment shader
        shader = shaders.at(&shaders, 1);
        // Shader url
        QVERIFY(!shader->shader.isEmpty());
        QCOMPARE(shader->stage, QQuick3DShaderUtilsShader::Stage::Fragment);
    }

    lastSceneData = std::move(sceneData);
}

void Shadergen::tst_effectUniforms_data()
{
    QTest::addColumn<QMetaType>("type");
    QTest::addColumn<QString>("name");
    QTest::addColumn<QVariant>("value");

    QTest::newRow("bool0") << QMetaType(QMetaType::Bool) << "uBoolFalse" << QVariant::fromValue(false);
    QTest::newRow("bool1") << QMetaType(QMetaType::Bool) << "uBoolTrue" << QVariant::fromValue(true);
    QTest::newRow("int") << QMetaType(QMetaType::Double) /*Not a typo*/ << "uInt" << QVariant::fromValue(int(33));
    QTest::newRow("real") << QMetaType(QMetaType::Double) << "uReal" << QVariant::fromValue(qreal(3.3));
    QTest::newRow("pointS") << QMetaType(QMetaType::QPointF) << "uPointS" << QVariant::fromValue(QPointF(0, 1));
    QTest::newRow("pointF") << QMetaType(QMetaType::QPointF) << "uPointF" << QVariant::fromValue(QPointF(1, 0));
    QTest::newRow("sizeS") << QMetaType(QMetaType::QSizeF) << "uSizeS" << QVariant::fromValue(QSizeF(1, 1));
    QTest::newRow("sizeF") << QMetaType(QMetaType::QSizeF) << "uSizeF" << QVariant::fromValue(QSizeF(2, 2));
    QTest::newRow("rectS") << QMetaType(QMetaType::QRectF) << "uRectS" << QVariant::fromValue(QRectF(0, 1, 100, 101));
    QTest::newRow("rectF") << QMetaType(QMetaType::QRectF) << "uRectF" << QVariant::fromValue(QRectF(1, 0, 101, 100));
    QTest::newRow("vec2") << QMetaType(QMetaType::QVector2D) << "uVec2" << QVariant::fromValue(QVector2D(1, 2));
    QTest::newRow("vec3") << QMetaType(QMetaType::QVector3D) << "uVec3" << QVariant::fromValue(QVector3D(1, 2, 3));
    QTest::newRow("vec4") << QMetaType(QMetaType::QVector4D) << "uVec4" << QVariant::fromValue(QVector4D(1, 2, 3, 4));
    QTest::newRow("quat") << QMetaType(QMetaType::QQuaternion) << "uQuat" << QVariant::fromValue(QQuaternion(1, 2, 3, 4));
    QTest::newRow("m44") << QMetaType(QMetaType::QMatrix4x4) << "uM44"
                         << QVariant::fromValue(QMatrix4x4(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16));
    QTest::newRow("Color1") << QMetaType(QMetaType::QColor) << "uColor1" << QVariant::fromValue(QColor("green"));
    QTest::newRow("Color2") << QMetaType(QMetaType::QColor) << "uColor2" << QVariant::fromValue(QColor("#ff0000"));
    QTest::newRow("TextureInput") << QMetaType(qMetaTypeId<QQuick3DShaderUtilsTextureInput *>()) << "uTexInput"
                                  << QVariant::fromValue((QQuick3DShaderUtilsTextureInput *)nullptr);
}

void Shadergen::tst_effectUniforms()
{
    // Test that public QML members are added to the object
    const auto &sceneData = lastSceneData;
    QCOMPARE(sceneData.effects.size(), 1);
    QQuick3DEffect *effect = qobject_cast<QQuick3DEffect *>(sceneData.effects.at(0));

    QFETCH(QMetaType, type);
    QFETCH(QString, name);
    QFETCH(QVariant, value);

    const auto prop = effect->property(name.toLatin1().constData());
    QVERIFY(prop.isValid());
    QCOMPARE(prop.metaType(), type);
    if (type == QMetaType(QMetaType::Double)) {
        QVERIFY(qFuzzyCompare(prop.toDouble(), value.toDouble()));
    } else if (type.id() >= QMetaType::User) {
        const bool ok = (prop.metaType().id() == qMetaTypeId<QQuick3DShaderUtilsTextureInput *>());
        if (!ok) {
            qDebug() << "TextureInput: " << qMetaTypeId<QQuick3DShaderUtilsTextureInput *>();
            qDebug() << prop.metaType().id();
        }
        QVERIFY(ok);
    } else {
        QCOMPARE(prop, value);
    }
}

void Shadergen::tst_componentResolving()
{
    QVector<QString> filePaths { ":/qml/main.qml", ":/qml/ModelA.qml", ":/qml/DefaultMaterialA.qml" };

    MaterialParser::SceneData sceneData;
    MaterialParser::parseQmlFiles(filePaths, QString(), sceneData, false);

    // main.qml(ModelA, Model, Model, ModelA, ModelA, ModelA, ModelA). Component should not be added as it's used
    const auto &models = sceneData.models;
    // The correct number should be 7 here, but we don't track if a component is
    // actually used so it gets pushed into the same list as the regular instances.
    QCOMPARE(models.size(), 8);

    // We skip testing the component here and start at index 1

    { // Model component (ModelA) with inherited material (1)
        const auto &model = models.at(1);
        auto materials = model->materials();
        QCOMPARE(materials.count(&materials), 1);
    }

    { // Model with refed component material (DefaultMaterialA) (1)
        const auto &model = models.at(2);
        auto materials = model->materials();
        QCOMPARE(materials.count(&materials), 1);
    }

    { // Model with inline component material (DefaultMaterialA) (1)
        const auto &model = models.at(3);
        auto materials = model->materials();
        QCOMPARE(materials.count(&materials), 1);
    }

    { // Model component (ModelA) overrides inherited material with refed component material (DefaultMaterialA) (1)
        const auto &model = models.at(4);
        auto materials = model->materials();
        QCOMPARE(materials.count(&materials), 1);
    }

    { // Model component (ModelA) overrides inherited material inline (DefaultMaterial) (1)
        const auto &model = models.at(5);
        auto materials = model->materials();
        QCOMPARE(materials.count(&materials), 1);
    }

    { // Model component (ModelA) override inherited material with refed materials (2)
        const auto &model = models.at(6);
        auto materials = model->materials();
        QCOMPARE(materials.count(&materials), 2);
    }

    { // Model component (ModelA) override inherited material with inline materials (2)
        const auto &model = models.at(7);
        auto materials = model->materials();
        QCOMPARE(materials.count(&materials), 2);
    }
}

void Shadergen::tst_instancing()
{
    QVector<QString> filePaths { ":/qml/instancing.qml" };

    MaterialParser::SceneData sceneData;
    MaterialParser::parseQmlFiles(filePaths, QString(), sceneData, false);

    // instancing.qml(Model).
    const auto &models = sceneData.models;
    QCOMPARE(models.size(), 1);

    {
        const auto &model = models.at(0);
        auto instancing = model->instancing();
        QVERIFY(instancing != nullptr);
        int count = -1;
        const auto buffer = instancing->instanceBuffer(&count);
        (void)buffer;
        QCOMPARE(count, 2);
    }
}

QTEST_APPLESS_MAIN(Shadergen)

#include "tst_shadergen.moc"
