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

#include <QTest>
#include <QQuickView>

#include <private/qquick3dviewport_p.h>
#include <ssg/qssgrendercontextcore.h>
#include <private/qssgrenderbuffermanager_p.h>
#include <private/qquick3dresourceloader_p.h>

#if QT_CONFIG(vulkan)
#include <QVulkanInstance>
#endif

#include <QThread>

#include "../shared/util.h"

static inline void renderNextFrame(QQuick3DTestOffscreenRenderer *renderer, bool *readCompleted, QRhiReadbackResult *readResult, QImage *result)
{
    renderer->qmlEngine->collectGarbage();
    QGuiApplication::processEvents();
    renderer->renderControl->polishItems();
    renderer->renderControl->beginFrame();
    renderer->renderControl->sync();
    renderer->renderControl->render();
    renderer->enqueueReadback(readCompleted, readResult, result);
    renderer->renderControl->endFrame();
}

class tst_BufferManager : public QQuick3DDataTest
{
    Q_OBJECT

private slots:
    void initTestCase() override;
    void staticScene_data();
    void staticScene();
    void dynamicScene();

private:
    bool initRenderer(QQuick3DTestOffscreenRenderer *renderer, const QString &filename);
#if QT_CONFIG(vulkan)
    QVulkanInstance vulkanInstance;
#endif
};

void tst_BufferManager::initTestCase()
{
    QQuick3DDataTest::initTestCase();
    if (!initialized())
        return;

#if QT_CONFIG(vulkan)
    vulkanInstance.setLayers({ "VK_LAYER_LUNARG_standard_validation" });
    vulkanInstance.create(); // may fail, which is fine is Vulkan is not used in the first place
#endif
}

void tst_BufferManager::staticScene_data()
{
    QTest::addColumn<QString>("qmlSource");
    QTest::addColumn<int>("pathTextureCount");
    QTest::addColumn<int>("sgTextureCount");
    QTest::addColumn<int>("customTextureCount");
    QTest::addColumn<int>("meshCount");
    QTest::addColumn<int>("customMeshCount");

    QTest::newRow("empty scene") << "empty.qml" << 0 << 0 << 0 << 0 << 0;

    // Path Meshes
    QTest::newRow("unshared path meshes") << "pathedMeshesUnshared.qml" << 0 << 0 << 0 << 2 << 0;
    QTest::newRow("shared path meshes") << "pathedMeshesShared.qml" << 0 << 0 << 0 << 2 << 0;

    // Custom Meshes
    QTest::newRow("unused Custom Geometry") << "customGeometryUnused.qml" << 0 << 0 << 0 << 0 << 0;
    QTest::newRow("unshared Custom Geometry") << "customGeometryUnshared.qml" << 0 << 0 << 0 << 0 << 3;
    QTest::newRow("shared Custom Geometry") << "customGeometryShared.qml" << 0 << 0 << 0 << 0 << 3;

    // Path Textures
    QTest::newRow("unused path textures") << "pathedTexturesUnused.qml" << 0 << 0 << 0 << 0 << 0;
    QTest::newRow("unshared path textures") << "pathedTexturesUnshared.qml" << 5 << 0 << 0 << 1 << 0;
    QTest::newRow("shared path textures") << "pathedTexturesShared.qml" << 3 << 0 << 0 << 1 << 0;
    QTest::newRow("mipmodes path textures") << "pathedTexturesMipModes.qml" << 3 << 0 << 0 << 1 << 0;

    // SG (Quick) Textures
    QTest::newRow("unused SG textures") << "sgTexturesUnused.qml" << 0 << 0 << 0 << 0 << 0;
    QTest::newRow("unshared SG textures") << "sgTexturesUnshared.qml" << 0 << 4 << 0 << 1 << 0;
    QTest::newRow("shared SG textures") << "sgTexturesShared.qml" << 0 << 3 << 0 << 1 << 0;

    // Custom Textures
    QTest::newRow("unused Custom Textures") << "customTexturesUnused.qml" << 0 << 0 << 0 << 0 << 0;
    QTest::newRow("unshared Custom Textures") << "customTexturesUnshared.qml" << 0 << 0 << 3 << 1 << 0;
    QTest::newRow("unshared Custom Textures") << "customTexturesShared.qml" << 0 << 0 << 2 << 1 << 0;

    // ResourceLoader
    QTest::newRow("only ResourceLoader") << "resourceLoaderOnly.qml" << 3 << 1 << 1 << 4 << 3;
    QTest::newRow("mixed ResourceLoader") << "resourceLoaderMixed.qml" << 5 << 1 << 2 << 5 << 4;
    QTest::newRow("multiple ResourceLoader") << "resourceLoaderMultiple.qml" << 3 << 1 << 2 << 5 << 4;
}

void tst_BufferManager::staticScene()
{
    QFETCH(QString, qmlSource);
    QFETCH(int, pathTextureCount);
    QFETCH(int, sgTextureCount);
    QFETCH(int, customTextureCount);
    QFETCH(int, meshCount);
    QFETCH(int, customMeshCount);

    QQuick3DTestOffscreenRenderer renderer;
    QVERIFY(initRenderer(&renderer, qmlSource));

    if (renderer.quickWindow->rendererInterface()->graphicsApi() == QSGRendererInterface::OpenGL) {
#ifdef Q_OS_MACOS
        QSKIP("Skipping test due to sofware OpenGL renderer problems on macOS");
#endif
    }

    bool readCompleted = false;
    QRhiReadbackResult readResult;
    QImage result;

    renderNextFrame(&renderer, &readCompleted, &readResult, &result);

    const auto &context = QQuick3DSceneManager::getOrSetWindowAttachment(*renderer.quickWindow)->rci();
    QVERIFY(context);

    const auto &bufferManager = context->bufferManager();

    QCOMPARE(bufferManager->getImageMap().size(), pathTextureCount);
    QCOMPARE(bufferManager->getSGImageMap().size(), sgTextureCount);
    QCOMPARE(bufferManager->getCustomTextureMap().size(), customTextureCount);
    QCOMPARE(bufferManager->getMeshMap().size(), meshCount);
    QCOMPARE(bufferManager->getCustomMeshMap().size(), customMeshCount);
}

void tst_BufferManager::dynamicScene()
{
    QQuick3DTestOffscreenRenderer renderer;
    QVERIFY(initRenderer(&renderer, QString("dynamic.qml")));

    if (renderer.quickWindow->rendererInterface()->graphicsApi() == QSGRendererInterface::OpenGL) {
#ifdef Q_OS_MACOS
        QSKIP("Skipping test due to sofware OpenGL renderer problems on macOS");
#endif
    }

    bool readCompleted = false;
    QRhiReadbackResult readResult;
    QImage result;

    renderNextFrame(&renderer, &readCompleted, &readResult, &result);

    const auto &context = QQuick3DSceneManager::getOrSetWindowAttachment(*renderer.quickWindow)->rci();
    QVERIFY(context);

    const auto &bufferManager = context->bufferManager();

    // Check for the initial state (1 static path model)
    QCOMPARE(bufferManager->getImageMap().size(), 0);
    QCOMPARE(bufferManager->getSGImageMap().size(), 0);
    QCOMPARE(bufferManager->getCustomTextureMap().size(), 0);
    QCOMPARE(bufferManager->getMeshMap().size(), 1);
    QCOMPARE(bufferManager->getCustomMeshMap().size(), 0);

    // Get the test controller object
    const auto controller = renderer.rootItem->property("controller").value<QQuick3DNode*>();
    QVERIFY(controller);

    auto addModel = [controller](const QString &path) -> QQuick3DModel* {
        QQuick3DModel *model = nullptr;
        QMetaObject::invokeMethod(controller, "addModel", Q_RETURN_ARG(QQuick3DModel*, model), Q_ARG(QString, path));
        return model;
    };

    auto addDynamicModel = [controller]() -> QQuick3DModel* {
        QQuick3DModel *model = nullptr;
        QMetaObject::invokeMethod(controller, "addDynamicModel", Q_RETURN_ARG(QQuick3DModel*, model));
        return model;
    };

    auto addTexture = [controller](const QString &path) -> QQuick3DTexture* {
        QQuick3DTexture *texture = nullptr;
        QMetaObject::invokeMethod(controller, "addTexture", Q_RETURN_ARG(QQuick3DTexture*, texture), Q_ARG(QString, path));
        return texture;
    };

    auto addDynamicTexture = [controller]() -> QQuick3DTexture* {
        QQuick3DTexture *texture = nullptr;
        QMetaObject::invokeMethod(controller, "addDynamicTexture", Q_RETURN_ARG(QQuick3DTexture*, texture));
        return texture;
    };

    auto addQmlTexture = [controller]() -> QQuick3DModel* {
        QQuick3DModel *model = nullptr;
        QMetaObject::invokeMethod(controller, "addQmlTexture", Q_RETURN_ARG(QQuick3DModel*, model));
        return model;
    };

    auto addQmlSharedTexture = [controller]() -> QQuick3DModel* {
        QQuick3DModel *model = nullptr;
        QMetaObject::invokeMethod(controller, "addQmlSharedTexture", Q_RETURN_ARG(QQuick3DModel*, model));
        return model;
    };

    auto addResourceLoader = [controller]() -> QQuick3DResourceLoader* {
        QQuick3DResourceLoader *loader = nullptr;
        QMetaObject::invokeMethod(controller, "addResourceLoader", Q_RETURN_ARG(QQuick3DResourceLoader*, loader));
        return loader;
    };

    // models (path)
    // add another sphere (count should stay 1)
    auto sphereModel = addModel("#Sphere");
    renderNextFrame(&renderer, &readCompleted, &readResult, &result);
    QCOMPARE(bufferManager->getMeshMap().size(), 1);
    // add a cube (count == 2)
    QQuick3DModel *model = addModel("#Cube");
    renderNextFrame(&renderer, &readCompleted, &readResult, &result);
    QCOMPARE(bufferManager->getMeshMap().size(), 2);
    // remove the cube model (count == 1)
    QMetaObject::invokeMethod(controller, "removeModel", Qt::DirectConnection);
    delete model;
    renderNextFrame(&renderer, &readCompleted, &readResult, &result);
    QCOMPARE(bufferManager->getMeshMap().size(), 1);
    QMetaObject::invokeMethod(controller, "removeModel");
    delete sphereModel;
    renderNextFrame(&renderer, &readCompleted, &readResult, &result);
    QCOMPARE(bufferManager->getMeshMap().size(), 1);

    // models (customGeometry)
    auto dynamicModel = addDynamicModel();
    renderNextFrame(&renderer, &readCompleted, &readResult, &result);
    QCOMPARE(bufferManager->getCustomMeshMap().size(), 1);
    auto dynamicModel2 = addDynamicModel();
    renderNextFrame(&renderer, &readCompleted, &readResult, &result);
    QCOMPARE(bufferManager->getCustomMeshMap().size(), 2);

    QMetaObject::invokeMethod(controller, "removeDynamicModel");
    delete dynamicModel2;
    renderNextFrame(&renderer, &readCompleted, &readResult, &result);
    QCOMPARE(bufferManager->getCustomMeshMap().size(), 1);

    QMetaObject::invokeMethod(controller, "removeDynamicModel");
    delete dynamicModel;
    renderNextFrame(&renderer, &readCompleted, &readResult, &result);
    QCOMPARE(bufferManager->getCustomMeshMap().size(), 0);


    // textures (path)
    QCOMPARE(bufferManager->getImageMap().size(), 0);
    auto texture = addTexture("noise1.jpg");
    renderNextFrame(&renderer, &readCompleted, &readResult, &result);
    QCOMPARE(bufferManager->getImageMap().size(), 1);
    QMetaObject::invokeMethod(controller, "removeTexture");
    delete texture;
    renderNextFrame(&renderer, &readCompleted, &readResult, &result);
    QCOMPARE(bufferManager->getImageMap().size(), 0);

    // textures (custom)
    QCOMPARE(bufferManager->getCustomTextureMap().size(), 0);
    auto dynamicTexture = addDynamicTexture();
    renderNextFrame(&renderer, &readCompleted, &readResult, &result);
    QCOMPARE(bufferManager->getCustomTextureMap().size(), 1);
    QMetaObject::invokeMethod(controller, "removeDynamicTexture");
    delete dynamicTexture;
    renderNextFrame(&renderer, &readCompleted, &readResult, &result);
    QCOMPARE(bufferManager->getCustomTextureMap().size(), 0);

    // textures (sg)
    QCOMPARE(bufferManager->getSGImageMap().size(), 0);
    auto qmlTexture = addQmlTexture();
    renderNextFrame(&renderer, &readCompleted, &readResult, &result);
    QCOMPARE(bufferManager->getSGImageMap().size(), 1);
    auto qmlTexture2 = addQmlTexture();
    renderNextFrame(&renderer, &readCompleted, &readResult, &result);
    QCOMPARE(bufferManager->getSGImageMap().size(), 2);
    QMetaObject::invokeMethod(controller, "removeQmlTexture");
    delete qmlTexture2;
    renderNextFrame(&renderer, &readCompleted, &readResult, &result);
    QCOMPARE(bufferManager->getSGImageMap().size(), 1);
    QMetaObject::invokeMethod(controller, "removeQmlTexture");
    delete qmlTexture;
    renderNextFrame(&renderer, &readCompleted, &readResult, &result);
    QCOMPARE(bufferManager->getSGImageMap().size(), 0);

    QCOMPARE(bufferManager->getSGImageMap().size(), 0);
    auto qmlSharedTexture = addQmlSharedTexture();
    renderNextFrame(&renderer, &readCompleted, &readResult, &result);
    QCOMPARE(bufferManager->getSGImageMap().size(), 1);
    auto qmlSharedTexture2 = addQmlSharedTexture();
    renderNextFrame(&renderer, &readCompleted, &readResult, &result);
    QCOMPARE(bufferManager->getSGImageMap().size(), 1);
    QMetaObject::invokeMethod(controller, "removeQmlSharedTexture");
    delete qmlSharedTexture2;
    renderNextFrame(&renderer, &readCompleted, &readResult, &result);
    QCOMPARE(bufferManager->getSGImageMap().size(), 1);
    QMetaObject::invokeMethod(controller, "removeQmlSharedTexture");
    delete qmlSharedTexture;
    renderNextFrame(&renderer, &readCompleted, &readResult, &result);
    QCOMPARE(bufferManager->getSGImageMap().size(), 0);

    // Make sure scene is in its initial state
    QCOMPARE(bufferManager->getImageMap().size(), 0);
    QCOMPARE(bufferManager->getSGImageMap().size(), 0);
    QCOMPARE(bufferManager->getCustomTextureMap().size(), 0);
    QCOMPARE(bufferManager->getMeshMap().size(), 1);
    QCOMPARE(bufferManager->getCustomMeshMap().size(), 0);

    // Add a ResourceLoader
    auto resourceLoader = addResourceLoader();
    renderNextFrame(&renderer, &readCompleted, &readResult, &result);
    QCOMPARE(bufferManager->getImageMap().size(), 3);
    QCOMPARE(bufferManager->getSGImageMap().size(), 1);
    QCOMPARE(bufferManager->getCustomTextureMap().size(), 1);
    QCOMPARE(bufferManager->getMeshMap().size(), 5);
    QCOMPARE(bufferManager->getCustomMeshMap().size(), 1);
    // Remove the ResourceLoader
    delete resourceLoader;
    renderNextFrame(&renderer, &readCompleted, &readResult, &result);
    QCOMPARE(bufferManager->getImageMap().size(), 0);
    QCOMPARE(bufferManager->getSGImageMap().size(), 0);
    QCOMPARE(bufferManager->getCustomTextureMap().size(), 0);
    QCOMPARE(bufferManager->getMeshMap().size(), 1);
    QCOMPARE(bufferManager->getCustomMeshMap().size(), 0);
}

bool tst_BufferManager::initRenderer(QQuick3DTestOffscreenRenderer *renderer, const QString &filename)
{
    const bool initSuccess = renderer->init(testFileUrl(filename),
#if QT_CONFIG(vulkan)
                                            &vulkanInstance
#else
                                            nullptr
#endif
    );
    return initSuccess;
}

QTEST_MAIN(tst_BufferManager)
#include "tst_buffermanager.moc"
