// Copyright (C) 2014 Klaralvdalens Datakonsult AB (KDAB).
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only

#include <QtTest/QtTest>
#include <QMatrix4x4>
#include <Qt3DCore/QEntity>
#include <Qt3DCore/QTransform>
#include <Qt3DRender/QMaterial>
#include <Qt3DExtras/QForwardRenderer>
#include <Qt3DExtras/QPhongMaterial>
#include <Qt3DExtras/QCylinderMesh>
#include <Qt3DRender/QRenderSettings>
#include <Qt3DRender/QGeometryRenderer>
#include <Qt3DRender/private/managers_p.h>

#include <Qt3DCore/private/qresourcemanager_p.h>
#include <Qt3DRender/qcamera.h>
#include <Qt3DRender/qrenderaspect.h>
#include <Qt3DRender/private/qrenderaspect_p.h>
#include <Qt3DRender/private/nodemanagers_p.h>
#include <Qt3DRender/private/updateworldtransformjob_p.h>
#include <Qt3DQuick/QQmlAspectEngine>
#include <Qt3DCore/private/qaspectjobmanager_p.h>
#include <Qt3DCore/private/qaspectengine_p.h>
#include <Qt3DCore/private/qaspectmanager_p.h>
#include <Qt3DCore/private/qnodevisitor_p.h>
#include <Qt3DCore/private/qnode_p.h>
#include <renderer_p.h>
#include <QQmlComponent>
#include <QScopedPointer>

QT_BEGIN_NAMESPACE

namespace Qt3DRender {

    class QRenderAspectTester : public Qt3DRender::QRenderAspect
    {
        Q_OBJECT
    public:
        QRenderAspectTester(bool withWindow = false)
            : Qt3DRender::QRenderAspect(QRenderAspect::Manual)
            , m_jobManager(new Qt3DCore::QAspectJobManager())
        {
            Qt3DCore::QAbstractAspectPrivate::get(this)->m_jobManager = m_jobManager.data();
            if (withWindow) {
                m_window.reset(new QWindow());
                m_window->resize(1024, 768);

                QSurfaceFormat format;
                if (QOpenGLContext::openGLModuleType() == QOpenGLContext::LibGL) {
                    format.setVersion(4, 3);
                    format.setProfile(QSurfaceFormat::CoreProfile);
                }
                format.setDepthBufferSize( 24 );
                format.setSamples( 4 );
                format.setStencilBufferSize(8);
                m_window->setFormat(format);
                m_window->create();

                QRenderAspect::onRegistered();
            }
        }

        std::vector<Qt3DCore::QAspectJobPtr> worldTransformJob()
        {
            auto renderer = static_cast<Render::OpenGL::Renderer *>(d_func()->m_renderer);
            auto daspect = Qt3DRender::QRenderAspectPrivate::get(renderer->aspect());
            daspect->m_worldTransformJob->setRoot(d_func()->m_renderer->sceneRoot());
            return {daspect->m_worldTransformJob};
        }

        std::vector<Qt3DCore::QAspectJobPtr> updateBoundingJob()
        {
            auto renderer = static_cast<Render::OpenGL::Renderer *>(d_func()->m_renderer);
            auto daspect = Qt3DRender::QRenderAspectPrivate::get(renderer->aspect());
            daspect->m_updateWorldBoundingVolumeJob->setManager(d_func()->m_renderer->nodeManagers()->renderNodesManager());
            return {daspect->m_updateWorldBoundingVolumeJob};
        }

        std::vector<Qt3DCore::QAspectJobPtr> calculateBoundingVolumeJob()
        {
            auto renderer = static_cast<Render::OpenGL::Renderer *>(d_func()->m_renderer);
            auto daspect = Qt3DRender::QRenderAspectPrivate::get(renderer->aspect());
            daspect->m_calculateBoundingVolumeJob->setRoot(d_func()->m_renderer->sceneRoot());
            return {daspect->m_calculateBoundingVolumeJob};
        }

        std::vector<Qt3DCore::QAspectJobPtr> framePreparationJob()
        {
            static_cast<Render::OpenGL::Renderer *>(d_func()->m_renderer)->m_updateShaderDataTransformJob->setManagers(d_func()->m_renderer->nodeManagers());
            return {static_cast<Render::OpenGL::Renderer *>(d_func()->m_renderer)->m_updateShaderDataTransformJob};
        }

        std::vector<Qt3DCore::QAspectJobPtr> frameCleanupJob()
        {
            static_cast<Render::OpenGL::Renderer *>(d_func()->m_renderer)->m_cleanupJob->setRoot(d_func()->m_renderer->sceneRoot());
            return {static_cast<Render::OpenGL::Renderer *>(d_func()->m_renderer)->m_cleanupJob};
        }

        std::vector<Qt3DCore::QAspectJobPtr> renderBinJobs()
        {
            return d_func()->m_renderer->renderBinJobs();
        }

        void onRootEntityChanged(Qt3DCore::QEntity *root)
        {
            if (!m_window) {
                QList<Qt3DCore::NodeTreeChange> nodes;
                Qt3DCore::QNodeVisitor v;
                v.traverse(root, [&nodes](Qt3DCore::QNode *node) {
                    Qt3DCore::QNodePrivate *d = Qt3DCore::QNodePrivate::get(node);
                    d->m_typeInfo = const_cast<QMetaObject*>(Qt3DCore::QNodePrivate::findStaticMetaObject(node->metaObject()));
                    d->m_hasBackendNode = true;
                    nodes.push_back({
                        node->id(),
                        Qt3DCore::QNodePrivate::get(node)->m_typeInfo,
                        Qt3DCore::NodeTreeChange::Added,
                        node
                    });
                });

                for (const auto &node: nodes)
                    d_func()->createBackendNode(node);

                static_cast<Qt3DRender::Render::OpenGL::Renderer *>(d_func()->m_renderer)->m_renderSceneRoot =
                        d_func()->m_renderer->nodeManagers()
                        ->lookupResource<Qt3DRender::Render::Entity, Qt3DRender::Render::EntityManager>(root->id());
            }
        }

    private:
        QScopedPointer<Qt3DCore::QAspectJobManager> m_jobManager;
        QScopedPointer<QWindow> m_window;
    };

}

QT_END_NAMESPACE

using namespace Qt3DRender;

Qt3DCore::QEntity *loadFromQML(const QUrl &source)
{
    QQmlEngine qmlEngine;
    QQmlComponent *component = new QQmlComponent(&qmlEngine, source);

    while (component->isLoading())
        QThread::usleep(250);

    QObject *rootObj = component->create();
    return qobject_cast<Qt3DCore::QEntity *>(rootObj);
}

Qt3DCore::QEntity *buildBigScene()
{
    Qt3DCore::QEntity *root = new Qt3DCore::QEntity();

    // Camera
    Qt3DRender::QCamera *cameraEntity = new Qt3DRender::QCamera(root);
    cameraEntity->setObjectName(QStringLiteral("cameraEntity"));
    cameraEntity->lens()->setPerspectiveProjection(45.0f, 16.0f/9.0f, 0.1f, 1000.0f);
    cameraEntity->setPosition(QVector3D(0, -250.0f, -50.0f));
    cameraEntity->setUpVector(QVector3D(0, 1, 0));
    cameraEntity->setViewCenter(QVector3D(0, 0, 0));

    // FrameGraph
    Qt3DRender::QRenderSettings* renderSettings = new Qt3DRender::QRenderSettings();
    Qt3DExtras::QForwardRenderer *forwardRenderer = new Qt3DExtras::QForwardRenderer();
    forwardRenderer->setCamera(cameraEntity);
    forwardRenderer->setClearColor(Qt::black);
    renderSettings->setActiveFrameGraph(forwardRenderer);
    root->addComponent(renderSettings);

    const float radius = 100.0f;
    const int max = 1000;
    const float det = 1.0f / max;
    // Scene
    for (int i = 0; i < max; i++) {
        Qt3DCore::QEntity *e = new Qt3DCore::QEntity();
        Qt3DCore::QTransform *transform = new Qt3DCore::QTransform();
        Qt3DExtras::QCylinderMesh *mesh = new Qt3DExtras::QCylinderMesh();
        Qt3DExtras::QPhongMaterial *material = new Qt3DExtras::QPhongMaterial();
        mesh->setRings(50.0f);
        mesh->setSlices(30.0f);
        mesh->setRadius(2.5f);
        mesh->setLength(5.0f);

        const float angle = M_PI * 2.0f * i * det * 10.;

        material->setDiffuse(QColor(qFabs(qCos(angle)) * 255, 204, 75));
        material->setAmbient(Qt::black);
        material->setSpecular(Qt::white);
        material->setShininess(150.0f);

        QMatrix4x4 m;
        m.translate(QVector3D(radius * qCos(angle), 200.* i * det, radius * qSin(angle)));
        m.rotate(30.0f * i, QVector3D(1.0f, 0.0f, 0.0f));
        m.rotate(45.0f * i, QVector3D(0.0f, 0.0f, 1.0f));
        transform->setMatrix(m);

        e->addComponent(transform);
        e->addComponent(mesh);
        e->addComponent(material);
        e->setParent(root);
    }

    return root;
}

class tst_benchJobs : public QObject
{
    Q_OBJECT

private:
    Qt3DCore::QEntity *m_bigSceneRoot;

public:
    tst_benchJobs()
        : m_bigSceneRoot(buildBigScene())
    {}

private Q_SLOTS:

    void updateTransformJob_data()
    {
        QTest::addColumn<Qt3DCore::QEntity*>("rootEntity");
        QTest::newRow("bigscene") << m_bigSceneRoot;
    }

    void updateTransformJob()
    {
        // GIVEN
        QFETCH(Qt3DCore::QEntity*, rootEntity);
        QRenderAspectTester aspect;

        Qt3DCore::QAbstractAspectPrivate::get(&aspect)->setRootAndCreateNodes(qobject_cast<Qt3DCore::QEntity *>(rootEntity), {});

        // WHEN
        std::vector<Qt3DCore::QAspectJobPtr> jobs = aspect.worldTransformJob();

        QBENCHMARK {
            Qt3DCore::QAbstractAspectPrivate::get(&aspect)->jobManager()->enqueueJobs(jobs);
            Qt3DCore::QAbstractAspectPrivate::get(&aspect)->jobManager()->waitForAllJobs();
        }
    }

    void updateBoundingJob_data()
    {
        QTest::addColumn<Qt3DCore::QEntity*>("rootEntity");
        QTest::newRow("bigscene") << m_bigSceneRoot;
    }

    void updateBoundingJob()
    {
        // GIVEN
        QFETCH(Qt3DCore::QEntity*, rootEntity);
        QRenderAspectTester aspect;

        Qt3DCore::QAbstractAspectPrivate::get(&aspect)->setRootAndCreateNodes(qobject_cast<Qt3DCore::QEntity *>(rootEntity), {});

        // WHEN
        std::vector<Qt3DCore::QAspectJobPtr> jobs = aspect.updateBoundingJob();

        QBENCHMARK {
            Qt3DCore::QAbstractAspectPrivate::get(&aspect)->jobManager()->enqueueJobs(jobs);
            Qt3DCore::QAbstractAspectPrivate::get(&aspect)->jobManager()->waitForAllJobs();
        }
    }

    void calculateBoundingVolumeJob_data()
    {
        QTest::addColumn<Qt3DCore::QEntity*>("rootEntity");
        QTest::newRow("bigscene") << m_bigSceneRoot;
    }

    void calculateBoundingVolumeJob()
    {
        // GIVEN
        QFETCH(Qt3DCore::QEntity*, rootEntity);
        QRenderAspectTester aspect;

        Qt3DCore::QAbstractAspectPrivate::get(&aspect)->setRootAndCreateNodes(qobject_cast<Qt3DCore::QEntity *>(rootEntity), {});

        // WHEN
        std::vector<Qt3DCore::QAspectJobPtr> jobs = aspect.calculateBoundingVolumeJob();

        QBENCHMARK {
            Qt3DCore::QAbstractAspectPrivate::get(&aspect)->jobManager()->enqueueJobs(jobs);
            Qt3DCore::QAbstractAspectPrivate::get(&aspect)->jobManager()->waitForAllJobs();
        }
    }

    void framePreparationJob_data()
    {
        QTest::addColumn<Qt3DCore::QEntity*>("rootEntity");
        QTest::newRow("bigscene") << m_bigSceneRoot;
    }

    void framePreparationJob()
    {
        // GIVEN
        QFETCH(Qt3DCore::QEntity*, rootEntity);
        QRenderAspectTester aspect;

        Qt3DCore::QAbstractAspectPrivate::get(&aspect)->setRootAndCreateNodes(qobject_cast<Qt3DCore::QEntity *>(rootEntity), {});

        // WHEN
        std::vector<Qt3DCore::QAspectJobPtr> jobs = aspect.framePreparationJob();

        QBENCHMARK {
            Qt3DCore::QAbstractAspectPrivate::get(&aspect)->jobManager()->enqueueJobs(jobs);
            Qt3DCore::QAbstractAspectPrivate::get(&aspect)->jobManager()->waitForAllJobs();
        }
    }

    void frameCleanupJob_data()
    {
        QTest::addColumn<Qt3DCore::QEntity*>("rootEntity");
        QTest::newRow("bigscene") << m_bigSceneRoot;
    }

    void frameCleanupJob()
    {
        // GIVEN
        QFETCH(Qt3DCore::QEntity*, rootEntity);
        QRenderAspectTester aspect;

        Qt3DCore::QAbstractAspectPrivate::get(&aspect)->setRootAndCreateNodes(qobject_cast<Qt3DCore::QEntity *>(rootEntity), {});

        // WHEN
        std::vector<Qt3DCore::QAspectJobPtr> jobs = aspect.frameCleanupJob();
        QBENCHMARK {
            Qt3DCore::QAbstractAspectPrivate::get(&aspect)->jobManager()->enqueueJobs(jobs);
            Qt3DCore::QAbstractAspectPrivate::get(&aspect)->jobManager()->waitForAllJobs();
        }
    }

    /*  Note: The renderer still needs to be simplified to run
        these jobs
    void renderBinJobs_data()
    {
        QTest::addColumn<Qt3DCore::QEntity*>("rootEntity");
        QTest::newRow("bigscene") << m_bigSceneRoot;
    }

    void renderBinJobs()
    {
        // GIVEN
        QFETCH(Qt3DCore::QEntity*, rootEntity);
        QScopedPointer<QThread> aspectThread(new QThread);
        QRenderAspectTester aspect(true);
        aspect.moveToThread(aspectThread.data());

        qDebug() << 1;

        Qt3DCore::QAbstractAspectPrivate::get(&aspect)->registerAspect(qobject_cast<Qt3DCore::QEntity *>(rootEntity));
        qDebug() << 2;

        // WHEN
        QList<Qt3DCore::QAspectJobPtr> jobs = aspect.renderBinJobs();
        qDebug() << 3;
        QBENCHMARK {
            qDebug() << 4;
            Qt3DCore::QAbstractAspectPrivate::get(&aspect)->jobManager()->enqueueJobs(jobs);
            Qt3DCore::QAbstractAspectPrivate::get(&aspect)->jobManager()->waitForAllJobs();
        }
    }
    */
};

QTEST_MAIN(tst_benchJobs)

#include "tst_bench_jobs.moc"
