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


#include <QtTest/QTest>
#include <QObject>
#include <QSignalSpy>

#include <qbackendnodetester.h>

#include <Qt3DCore/QEntity>
#include <Qt3DCore/private/qaspectjobmanager_p.h>
#include <Qt3DCore/private/qnodevisitor_p.h>
#include <Qt3DCore/qtransform.h>

#include <Qt3DRender/qcamera.h>
#include <Qt3DRender/qcameralens.h>
#include <Qt3DRender/qrenderaspect.h>
#include <Qt3DRender/private/entity_p.h>
#include <Qt3DRender/private/qcamera_p.h>
#include <Qt3DRender/private/cameralens_p.h>
#include <Qt3DRender/private/managers_p.h>
#include <Qt3DRender/private/nodemanagers_p.h>
#include <Qt3DRender/private/transform_p.h>
#include <Qt3DRender/private/qrenderaspect_p.h>
#include <Qt3DRender/private/updateworldtransformjob_p.h>

QT_BEGIN_NAMESPACE

namespace Qt3DRender {

QList<Qt3DCore::QNode *> getNodesForCreation(Qt3DCore::QNode *root)
{
    using namespace Qt3DCore;

    QList<QNode *> nodes;
    Qt3DCore::QNodeVisitor visitor;
    visitor.traverse(root, [&nodes](QNode *node) {
        nodes.append(node);

        // Store the metaobject of the node in the QNode so that we have it available
        // to us during destruction in the QNode destructor. This allows us to send
        // the QNodeId and the metaobject as typeinfo to the backend aspects so they
        // in turn can find the correct QBackendNodeMapper object to handle the destruction
        // of the corresponding backend nodes.
        QNodePrivate *d = QNodePrivate::get(node);
        d->m_typeInfo = const_cast<QMetaObject*>(QNodePrivate::findStaticMetaObject(node->metaObject()));

        // Mark this node as having been handled for creation so that it is picked up
        d->m_hasBackendNode = true;
    });

    return nodes;
}

QList<Qt3DCore::NodeTreeChange> nodeTreeChangesForNodes(const QList<Qt3DCore::QNode *> nodes)
{
    QList<Qt3DCore::NodeTreeChange> nodeTreeChanges;
    nodeTreeChanges.reserve(nodes.size());

    for (Qt3DCore::QNode *n : nodes) {
        nodeTreeChanges.push_back({
                                      n->id(),
                                      Qt3DCore::QNodePrivate::get(n)->m_typeInfo,
                                      Qt3DCore::NodeTreeChange::Added,
                                      n
                                  });
    }

    return nodeTreeChanges;
}

class TestAspect : public Qt3DRender::QRenderAspect
{
public:
    TestAspect(Qt3DCore::QNode *root)
        : Qt3DRender::QRenderAspect(Qt3DRender::QRenderAspect::Manual)
        , m_sceneRoot(nullptr)
    {
        QRenderAspect::onRegistered();

        const QList<Qt3DCore::QNode *> nodes = getNodesForCreation(root);
        d_func()->setRootAndCreateNodes(qobject_cast<Qt3DCore::QEntity *>(root), nodeTreeChangesForNodes(nodes));

        Render::Entity *rootEntity = nodeManagers()->lookupResource<Render::Entity, Render::EntityManager>(rootEntityId());
        Q_ASSERT(rootEntity);
        m_sceneRoot = rootEntity;
    }

    ~TestAspect();

    void onRegistered() override { QRenderAspect::onRegistered(); }
    void onUnregistered() override { QRenderAspect::onUnregistered(); }

    Qt3DRender::Render::NodeManagers *nodeManagers() const { return d_func()->m_renderer->nodeManagers(); }
    Qt3DRender::Render::FrameGraphNode *frameGraphRoot() const { return d_func()->m_renderer->frameGraphRoot(); }
    Qt3DRender::Render::RenderSettings *renderSettings() const { return d_func()->m_renderer->settings(); }
    Qt3DRender::Render::Entity *sceneRoot() const { return m_sceneRoot; }

private:
    Render::Entity *m_sceneRoot;
};

TestAspect::~TestAspect()
{
    QRenderAspect::onUnregistered();
}

} // namespace Qt3DRender

QT_END_NAMESPACE

namespace {

void runRequiredJobs(Qt3DRender::TestAspect *test)
{
    Qt3DRender::Render::UpdateWorldTransformJob updateWorldTransform;
    updateWorldTransform.setRoot(test->sceneRoot());
    updateWorldTransform.setManagers(test->nodeManagers());
    updateWorldTransform.run();
}

void fuzzyCompareMatrix(const QMatrix4x4 &a, const QMatrix4x4 &b)
{
    for (int i = 0; i < 4; i++) {
        for (int j = 0; j < 4; j++) {
            // Fuzzy comparison. qFuzzyCompare is not suitable because zero is compared to small numbers.
            QVERIFY(qAbs(a(i, j) - b(i, j)) < 1.0e-6f);
        }
    }
}

} // anonymous

class tst_QCamera : public Qt3DCore::QBackendNodeTester
{
    Q_OBJECT

private Q_SLOTS:

    void initTestCase()
    {
    }

    void checkDefaultConstruction()
    {
        // GIVEN
        Qt3DRender::QCamera camera;

        // THEN
        QCOMPARE(camera.projectionType(), Qt3DRender::QCameraLens::PerspectiveProjection);
        QCOMPARE(camera.nearPlane(), 0.1f);
        QCOMPARE(camera.farPlane(), 1024.0f);
        QCOMPARE(camera.fieldOfView(), 25.0f);
        QCOMPARE(camera.aspectRatio(), 1.0f);
        QCOMPARE(camera.left(), -0.5f);
        QCOMPARE(camera.right(), 0.5f);
        QCOMPARE(camera.bottom(), -0.5f);
        QCOMPARE(camera.top(), 0.5f);
        QCOMPARE(camera.exposure(), 0.0f);
    }

    void checkTransformWithParent()
    {
        // GIVEN
        QScopedPointer<Qt3DCore::QEntity> root(new Qt3DCore::QEntity);

        QScopedPointer<Qt3DCore::QEntity> parent(new Qt3DCore::QEntity(root.data()));
        Qt3DCore::QTransform *parentTransform = new Qt3DCore::QTransform();
        parentTransform->setTranslation(QVector3D(10, 9, 8));
        parentTransform->setRotationX(10.f);
        parentTransform->setRotationY(20.f);
        parentTransform->setRotationZ(30.f);
        parent->addComponent(parentTransform);

        QScopedPointer<Qt3DRender::QCamera> camera(new Qt3DRender::QCamera(parent.data()));

        QScopedPointer<Qt3DRender::TestAspect> test(new Qt3DRender::TestAspect(root.data()));
        runRequiredJobs(test.data());

        Qt3DRender::Render::Entity *cameraEntity = test->nodeManagers()->lookupResource<Qt3DRender::Render::Entity, Qt3DRender::Render::EntityManager>(camera->id());

        // THEN
        QVERIFY(qFuzzyCompare(convertToQMatrix4x4(*cameraEntity->worldTransform()), convertToQMatrix4x4(parentTransform->matrix())));
    }

    void checkTransformWithParentAndLookAt()
    {
        // GIVEN
        QScopedPointer<Qt3DCore::QEntity> root(new Qt3DCore::QEntity);

        QScopedPointer<Qt3DCore::QEntity> parent(new Qt3DCore::QEntity(root.data()));
        Qt3DCore::QTransform *parentTransform = new Qt3DCore::QTransform();
        parentTransform->setRotationZ(90.f);
        parent->addComponent(parentTransform);

        QScopedPointer<Qt3DRender::QCamera> camera(new Qt3DRender::QCamera(parent.data()));
        camera->setPosition(QVector3D(1.f, 0.f, 0.f));
        camera->setViewCenter(QVector3D(1.f, 0.f, -1.f)); // look in -z
        camera->setUpVector(QVector3D(0.f, 1.f, 0.f));

        QScopedPointer<Qt3DRender::TestAspect> test(new Qt3DRender::TestAspect(root.data()));
        runRequiredJobs(test.data());

        Qt3DRender::Render::Entity *cameraEntity = test->nodeManagers()->lookupResource<Qt3DRender::Render::Entity, Qt3DRender::Render::EntityManager>(camera->id());

        // THEN
        QMatrix4x4 m;
        m.translate(0.f, 1.f, 0.f); // 90 deg z-rotation + x-translation = y-translation
        m.rotate(90.f, QVector3D(0.f, 0.f, 1.f));

        const QMatrix4x4 worldTransform = convertToQMatrix4x4(*cameraEntity->worldTransform());
        fuzzyCompareMatrix(worldTransform, m);

    }

    void checkTransformOfChild()
    {
        // GIVEN
        QScopedPointer<Qt3DCore::QEntity> root(new Qt3DCore::QEntity);

        QScopedPointer<Qt3DCore::QEntity> parent(new Qt3DCore::QEntity(root.data()));
        Qt3DCore::QTransform *parentTransform = new Qt3DCore::QTransform();
        parentTransform->setTranslation(QVector3D(10, 9, 8));
        parent->addComponent(parentTransform);

        QScopedPointer<Qt3DRender::QCamera> camera(new Qt3DRender::QCamera(parent.data()));
        camera->setPosition(QVector3D(2.f, 3.f, 4.f));
        camera->setViewCenter(QVector3D(1.f, 3.f, 4.f)); // looking in -x = 90 deg y-rotation
        camera->setUpVector(QVector3D(0.f, 1.f, 0.f));

        // Child coordinate system:
        // y = camera up vector = global y
        // z = negative camera look direction = global x
        // x = y cross z = global -z
        QScopedPointer<Qt3DCore::QEntity> child(new Qt3DCore::QEntity(camera.data()));
        Qt3DCore::QTransform *childTransform = new Qt3DCore::QTransform();
        childTransform->setTranslation(QVector3D(1.f, 2.f, 3.f));
        child->addComponent(childTransform);

        QScopedPointer<Qt3DRender::TestAspect> test(new Qt3DRender::TestAspect(root.data()));
        runRequiredJobs(test.data());

        Qt3DRender::Render::Entity *childEntity = test->nodeManagers()->lookupResource<Qt3DRender::Render::Entity, Qt3DRender::Render::EntityManager>(child->id());

        // THEN
        QMatrix4x4 m;
        m.translate(10.f, 9.f, 8.f); // parent's world translation
        m.translate(2.f, 3.f, 4.f); // camera's world translation
        m.translate(3.f, 2.f, -1.f); // child's world translation
        m.rotate(90.f, QVector3D(0.f, 1.f, 0.f)); // camera's rotation

        fuzzyCompareMatrix(convertToQMatrix4x4(*childEntity->worldTransform()), m);
    }
};


QTEST_MAIN(tst_QCamera)

#include "tst_qcamera.moc"
