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

#include <QTest>
#include <QSignalSpy>

#include <QtQuick3D/private/qquick3dnode_p.h>
#include <QtQuick3DUtils/private/qssgutils_p.h>

#include <QtQuick3DRuntimeRender/private/qssgrendernode_p.h>

class tst_QQuick3DNode : public QObject
{
    Q_OBJECT

    // Work-around to get access to updateSpatialNode
    class NodeItem : public QQuick3DNode
    {
    public:
        using QQuick3DNode::updateSpatialNode;
    };

private slots:
    void testProperties();
    void testSceneRotation();
    void testEnums();
    void testPositionMapping();
    void testDirectionMapping();
};

void tst_QQuick3DNode::testProperties()
{
    NodeItem nodeItem;
    auto node = static_cast<QSSGRenderNode *>(nodeItem.updateSpatialNode(nullptr));
    const auto originalNode = node; // for comparisons later...
    QVERIFY(node);

    // Positions
    const float x = 50.0f;
    nodeItem.setX(x);
    node = static_cast<QSSGRenderNode *>(nodeItem.updateSpatialNode(node));
    QCOMPARE(x, nodeItem.x());
    QVERIFY(qFuzzyCompare(x, mat44::getPosition(node->localTransform).x()));

    const float y = 60.0f;
    nodeItem.setY(y);
    node = static_cast<QSSGRenderNode *>(nodeItem.updateSpatialNode(node));
    QCOMPARE(y, nodeItem.y());
    QVERIFY(qFuzzyCompare(y, mat44::getPosition(node->localTransform).y()));

    const float z = 70.0f;
    nodeItem.setZ(z);
    node = static_cast<QSSGRenderNode *>(nodeItem.updateSpatialNode(node));
    QCOMPARE(z, nodeItem.z());
    QVERIFY(qFuzzyCompare(z, mat44::getPosition(node->localTransform).z()));

    QVector3D pos1(x, y, z);
    QCOMPARE(pos1, nodeItem.position());
    QVERIFY(qFuzzyCompare(pos1, mat44::getPosition(node->localTransform)));

    pos1 *= 2.0;
    nodeItem.setPosition(pos1);
    node = static_cast<QSSGRenderNode *>(nodeItem.updateSpatialNode(node));
    QCOMPARE(pos1, nodeItem.position());
    QVERIFY(qFuzzyCompare(pos1, mat44::getPosition(node->localTransform)));

    // Rotation, Scale, Pivot
    QCOMPARE(nodeItem.rotation(), QQuaternion());
    QCOMPARE(nodeItem.scale(), QVector3D(1, 1, 1));
    QCOMPARE(nodeItem.pivot(), QVector3D());

    QQuaternion rot = QQuaternion::fromEulerAngles(100, 200, 300);
    nodeItem.setRotation(rot);
    node = static_cast<QSSGRenderNode *>(nodeItem.updateSpatialNode(node));
    QCOMPARE(rot, nodeItem.rotation());
    QVERIFY(qFuzzyCompare(rot, QQuaternion::fromRotationMatrix(mat44::getUpper3x3(node->localTransform))));

    QVector3D scale(0.5, 1.0, 2.0);
    nodeItem.setScale(scale);
    node = static_cast<QSSGRenderNode *>(nodeItem.updateSpatialNode(node));
    QCOMPARE(scale, nodeItem.scale());
    QVERIFY(qFuzzyCompare(scale, mat44::getScale(node->localTransform)));

    QVector3D pivot(10, 20, 30);
    nodeItem.setPivot(pivot);
    node = static_cast<QSSGRenderNode *>(nodeItem.updateSpatialNode(node));
    QCOMPARE(pivot, nodeItem.pivot());
    QCOMPARE(pivot, node->pivot);

    // Opacity
    const float opacity = 0.2f;
    nodeItem.setLocalOpacity(opacity);
    node = static_cast<QSSGRenderNode *>(nodeItem.updateSpatialNode(node));
    QCOMPARE(opacity, nodeItem.localOpacity());
    QCOMPARE(opacity, node->localOpacity);

    // Visibility
    nodeItem.setVisible(false);
    node = static_cast<QSSGRenderNode *>(nodeItem.updateSpatialNode(node));
    QVERIFY(!node->getLocalState(QSSGRenderNode::LocalState::Active));
    QSignalSpy spy(&nodeItem, SIGNAL(visibleChanged()));
    nodeItem.setVisible(true);
    node = static_cast<QSSGRenderNode *>(nodeItem.updateSpatialNode(node));
    QVERIFY(node->getLocalState(QSSGRenderNode::LocalState::Active));
    QCOMPARE(spy.size(), 1);

    QCOMPARE(originalNode, node);
}

void tst_QQuick3DNode::testSceneRotation()
{
    QQuick3DNode parentNode;
    QQuick3DNode selfNode(&parentNode);

    const auto rotX45 = QQuaternion::fromAxisAndAngle(QVector3D(1.0f, 0.0f, 0.0f), 45.0f);
    parentNode.setRotation(rotX45);
    QVERIFY(qFuzzyCompare(selfNode.sceneRotation(), rotX45));

    const auto rotZ30 = QQuaternion::fromAxisAndAngle(QVector3D(0.0f, 0.0f, 1.0f), 30.0f);
    selfNode.setRotation(rotZ30);
    QVERIFY(qFuzzyCompare(selfNode.sceneRotation(), rotX45 * rotZ30));

    // !m_hasInheritedUniformScale path, but the XY plane is scaled uniformly.
    parentNode.setScale(QVector3D(5.0f, 5.0f, 1.0f));
    QVERIFY(qFuzzyCompare(selfNode.sceneRotation(), rotX45 * rotZ30));
}

void tst_QQuick3DNode::testEnums()
{
    NodeItem nodeItem;
    auto node = static_cast<QSSGRenderNode *>(nodeItem.updateSpatialNode(nullptr));
    QVERIFY(node);
}

void tst_QQuick3DNode::testPositionMapping()
{
    QQuick3DNode nodeA, nodeB;
    nodeB.setPosition(QVector3D{2.0f, 2.0f, 2.0f});

    { // map pos TO scene A
        const QVector3D expected { 1.0f, 1.0f, 1.0f };
        const QVector3D input { 1.0f, 1.0f, 1.0f };
        const auto res = nodeA.mapPositionToScene(input);
        QVERIFY(qFuzzyCompare(res, expected));
    }

    { // map pos TO scene B
        const QVector3D expected { 3.0f, 3.0f, 3.0f };
        const QVector3D input { 1.0f, 1.0f, 1.0f };
        const auto res = nodeB.mapPositionToScene(input);
        QVERIFY(qFuzzyCompare(res, expected));
    }

    { // map pos FROM scene A
        const QVector3D expected { 1.0f, 1.0f, 1.0f };
        const QVector3D input { 1.0f, 1.0f, 1.0f };
        const auto res = nodeA.mapPositionFromScene(input);
        QVERIFY(qFuzzyCompare(res, expected));
    }

    { // map pos FROM scene B
        const QVector3D expected { 1.0f, 1.0f, 1.0f };
        const QVector3D input { 3.0f, 3.0f, 3.0f };
        const auto res = nodeB.mapPositionFromScene(input);
        QVERIFY(qFuzzyCompare(res, expected));
    }

    { // map node A's postion TO node B
        const QVector3D expected { -1.0f, -1.0f, -1.0f };
        const QVector3D input { 1.0f, 1.0f, 1.0f };
        const auto res = nodeA.mapPositionToNode(&nodeB, input);
        QVERIFY(qFuzzyCompare(res, expected));
    }

    { // map node A's postion to node 'null'
        const QVector3D expected { 1.0f, 1.0f, 1.0f };
        const QVector3D input { 1.0f, 1.0f, 1.0f };
        const auto res = nodeA.mapPositionToNode(nullptr, input);
        QVERIFY(qFuzzyCompare(res, expected));
    }

    { // map pos FROM node B's pos
        const QVector3D expected { 3.0f, 3.0f, 3.0f };
        const QVector3D input { 1.0f, 1.0f, 1.0f };
        const auto res = nodeA.mapPositionFromNode(&nodeB, input);
        QVERIFY(qFuzzyCompare(res, expected));
    }

    { // map pos FROM null node
        const QVector3D expected { 1.0f, 1.0f, 1.0f };
        const QVector3D input { 1.0f, 1.0f, 1.0f };
        const auto res = nodeA.mapPositionFromNode(nullptr, input);
        QVERIFY(qFuzzyCompare(res, expected));
    }
}

void tst_QQuick3DNode::testDirectionMapping()
{
    QQuick3DNode nodeA, nodeB;
    nodeB.setEulerRotation(QVector3D{0.0f, 0.0f, 90.0f});

    { // map dir TO scene A
        const QVector3D expected { 1.0f, 1.0f, 1.0f };
        const QVector3D input { 1.0f, 1.0f, 1.0f };
        const auto res = nodeA.mapDirectionToScene(input);
        QVERIFY(qFuzzyCompare(res, expected));
    }

    { // map dir TO scene B
        const QVector3D expected { -1.0f, 1.0f, 0.0f };
        const QVector3D input { 1.0f, 1.0f, 0.0f };
        const auto res = nodeB.mapDirectionToScene(input);
        QVERIFY(qFuzzyCompare(res, expected));
    }

    { // map dir FROM scene A
        const QVector3D expected { 1.0f, 1.0f, 1.0f };
        const QVector3D input { 1.0f, 1.0f, 1.0f };
        const auto res = nodeA.mapDirectionFromScene(input);
        QVERIFY(qFuzzyCompare(res, expected));
    }

    { // map dir FROM scene B
        const QVector3D expected { 1.0f, -1.0f, 0.0f };
        const QVector3D input { 1.0f, 1.0f, 0.0f };
        const auto res = nodeB.mapDirectionFromScene(input);
        QVERIFY(qFuzzyCompare(res, expected));
    }

    { // map direction FROM node A's TO node B's space
        const QVector3D expected { 1.0f, -1.0f, 0.0f };
        const QVector3D input { 1.0f, 1.0f, 0.0f };
        const auto res = nodeA.mapDirectionToNode(&nodeB, input);
        QVERIFY(qFuzzyCompare(res, expected));
    }

    { // map direction FROM node A's TO null node
        const QVector3D expected { 1.0f, 1.0f, 1.0f };
        const QVector3D input { 1.0f, 1.0f, 1.0f };
        const auto res = nodeA.mapDirectionToNode(nullptr, input);
        QVERIFY(qFuzzyCompare(res, expected));
    }

    { // map direction FROM node B's pos
        const QVector3D expected { -1.0f, 1.0f, 0.0f };
        const QVector3D input { 1.0f, 1.0f, 0.0f };
        const auto res = nodeA.mapDirectionFromNode(&nodeB, input);
        QVERIFY(qFuzzyCompare(res, expected));
    }

    { // map pos FROM null node
        const QVector3D expected { 1.0f, 1.0f, 1.0f };
        const QVector3D input { 1.0f, 1.0f, 1.0f };
        const auto res = nodeA.mapDirectionFromNode(nullptr, input);
        QVERIFY(qFuzzyCompare(res, expected));
    }
}

QTEST_APPLESS_MAIN(tst_QQuick3DNode)
#include "tst_qquick3dnode.moc"
