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

#include <QtTest>

#include <QtQuick3DUtils/private/qssgutils_p.h>

#include <QtGui/qquaternion.h>

class tst_RotationDataClass : public QObject
{
    Q_OBJECT

public:
    tst_RotationDataClass() = default;
    ~tst_RotationDataClass() = default;

    using Dirty = RotationData::Dirty;
    static bool isDirty(const RotationData &rd) { return (rd.m_dirty != Dirty::None); };

private slots:
    void test_initialState();
    void test_construct();
    void test_eulerAssign();
    void test_quatAssign();
    void test_aba();
    void test_compare();
    void test_compare2();
    void test_fuzzyQuatCompare();
    void test_compare_precision();
};

void tst_RotationDataClass::test_initialState()
{

    // Initial state
    RotationData rotation;
    QCOMPARE(isDirty(rotation), false);
    QVERIFY(qFuzzyCompare(rotation.toEulerAngles(), QVector3D()));
    QVERIFY(qFuzzyCompare(rotation.toQuaternion(), QQuaternion()));
}

void tst_RotationDataClass::test_construct()
{
    QVector3D eulerRot = QVector3D(1.0f, 0.0f, 0.0f);
    QQuaternion quatRot = QQuaternion::fromEulerAngles(eulerRot);

    RotationData a(eulerRot);
    QCOMPARE(isDirty(a), true);

    RotationData b(quatRot);
    QCOMPARE(isDirty(b), true);

    QCOMPARE(a, b);
    QCOMPARE(a.m_quatRot, b.m_quatRot); // Compensate for the XFAIL. Verify that the two quaternions a componentwise equal.
    // NOTE: Comparison is done based on the stored quaternion
    QCOMPARE(isDirty(a), false); // Dirty is cleared as 'a' was set using euler angles
    QCOMPARE(isDirty(b), true); // Still dirty, as the Euler angles where never compared and therefore not calculated
    QCOMPARE(b.m_dirty, Dirty::Euler);
    QVERIFY(qFuzzyCompare(b.toEulerAngles(), eulerRot));
    QCOMPARE(isDirty(b), false); // Both internal values queried, so neither should be dirty


}

void tst_RotationDataClass::test_eulerAssign()
{
    RotationData rotation;
    QCOMPARE(isDirty(rotation), false);
    QVERIFY(qFuzzyCompare(rotation.toEulerAngles(), QVector3D()));
    QVERIFY(qFuzzyCompare(rotation.toQuaternion(), QQuaternion()));

    QVector3D eulerRot = QVector3D(1.0f, 0.0f, 0.0f);
    QQuaternion quatRot = QQuaternion::fromEulerAngles(eulerRot);

    rotation = eulerRot;
    QVERIFY(qFuzzyCompare(rotation.m_eulerRot, eulerRot));
    QCOMPARE(isDirty(rotation), true);

    {
        QCOMPARE(rotation.m_dirty, RotationData::Dirty::Quaternion);
        QQuaternion retQuatRot = rotation.toQuaternion();
        QCOMPARE(isDirty(rotation), false);
        QVERIFY(qFuzzyCompare(retQuatRot, quatRot));
        QVERIFY(qFuzzyCompare(rotation.m_quatRot, quatRot));
    }
}

void tst_RotationDataClass::test_quatAssign()
{
    RotationData rotation;
    QCOMPARE(isDirty(rotation), false);
    QVERIFY(qFuzzyCompare(rotation.toEulerAngles(), QVector3D()));
    QVERIFY(qFuzzyCompare(rotation.toQuaternion(), QQuaternion()));

    QVector3D eulerRot = QVector3D(1.0f, 0.0f, 0.0f);
    QQuaternion quatRot = QQuaternion::fromEulerAngles(eulerRot);

    rotation = quatRot;
    QVERIFY(qFuzzyCompare(rotation.m_quatRot, quatRot));
    QCOMPARE(isDirty(rotation), true);

    {
        QCOMPARE(rotation.m_dirty, RotationData::Dirty::Euler);
        QVector3D retEulerRot = rotation.toEulerAngles();
        QCOMPARE(isDirty(rotation), false);
        QVERIFY(qFuzzyCompare(retEulerRot, eulerRot));
        QVERIFY(qFuzzyCompare(rotation.m_eulerRot, eulerRot));
    }
}

void tst_RotationDataClass::test_aba()
{
    // Dirty state should be mutually exclusive and the value
    // updated appropriately each time
    // (i.e., not skipping a valid write by testing agains an old value).
    RotationData rotation; // Init { 0, 0, 0}
    rotation = QVector3D(1.0f, 0.0f, 0.0f);
    // Quat dirty
    rotation = QQuaternion::fromEulerAngles(QVector3D(0.0f, 0.0f, 0.0f));
    // Euler dirty
    QCOMPARE(rotation, RotationData{});
}

void tst_RotationDataClass::test_compare()
{
    {
        RotationData a;
        RotationData b;
        QVERIFY(a == b);
        QVERIFY(b == a);

        a = RotationData(QVector3D(1.0f, 1.0f, 1.0f));
        QVERIFY(a != b);
        QVERIFY(b != a);

        a = RotationData(QQuaternion());
        QVERIFY(a == b);
        QVERIFY(b == a);
    }

    {
        RotationData a;
        QVector3D b;
        QVERIFY(a == b);
        QVERIFY(b == a);

        a = QVector3D(1.0f, 1.0f, 1.0f);
        QVERIFY(a != b);
        QVERIFY(b != a);

        a = QQuaternion();
        QVERIFY(a == b);
        QVERIFY(b == a);
    }

    {
        RotationData a;
        QQuaternion b;
        QVERIFY(a == b);
        QVERIFY(b == a);

        a = QVector3D(1.0f, 1.0f, 1.0f);
        QVERIFY(a != b);
        QVERIFY(b != a);

        a = QQuaternion();
        QVERIFY(a == b);
        QVERIFY(b == a);
    }
}

void tst_RotationDataClass::test_compare2()
{
    RotationData a;
    RotationData b;
    QVERIFY(a == b);
    QVERIFY(b == a);

    {
        const auto v = QVector3D();
        const auto qa = QQuaternion::fromEulerAngles(v);

        // Quat stored as normalized value ?
        QQuaternion qaNormalized = qa.normalized();
        a = qa;
        QVERIFY(a == qaNormalized);
        QVERIFY(a == v);

        b = -a.toQuaternion();
        QVERIFY(a == b);
        QVERIFY(b == a);
    }

    {
        const auto v = QVector3D(0.0f, 0.0f, 45.0f);
        const auto qa = QQuaternion::fromEulerAngles(v);

        // Quat stored as normalized value ?
        QQuaternion qaNormalized = qa.normalized();
        {
            // Assumption 1: QQuaternion can be non-normalized but the RotationData class always
            // contains the normalized quaternion.
            {
                const auto qaNonNormalized = qa * 30.0f;
                QVERIFY(!qFuzzyCompare(qaNonNormalized, qaNormalized));
                {
                    RotationData rd(qaNonNormalized);
                    QVERIFY(rd.m_quatRot == qaNormalized);
                }

                {
                    RotationData rd;
                    rd = qaNonNormalized;
                    QVERIFY(rd.m_quatRot == qaNormalized);
                }
            }

            // Assumption 2: compare or fuzzy compare of QQuaternion is strictly a component by component compare
            auto qb = -qa;
            QVERIFY(!qFuzzyCompare(qb, qa));
        }
        a = qa;
        QVERIFY(a == qaNormalized);
        QVERIFY(a.m_quatRot == qaNormalized); // Compensate for the XFAIL
        QVERIFY(a == v);

        b = -a.toQuaternion();
        QVERIFY(a == b);
        QVERIFY(-a.m_quatRot == b.m_quatRot);
        QVERIFY(b == a);
        QVERIFY(b.m_quatRot == -a.m_quatRot);
    }
}

void tst_RotationDataClass::test_fuzzyQuatCompare()
{
    RotationData a;
    RotationData b;
    QVERIFY(a == b);
    QVERIFY(b == a);

    { // QTBUG-105918 (Verify that the divergence from 1.0f does not result in a negative number).
      // (i.e., -0.00001 <= e)
        const QQuaternion qa = QQuaternion(0.5f, 0.5f, 0.5f, 0.5f);
        const QQuaternion qb = QQuaternion(0.5f + 0.0007f, 0.5f, 0.5f, 0.5f);
        QVERIFY(qa != qb);
        QVERIFY(!qFuzzyCompare(qa, qb));
        a = qa;
        b = qb;
        QVERIFY(a != b);
        b = -qb;
        QVERIFY(a != b);
    }
}

void tst_RotationDataClass::test_compare_precision()
{
    // Compare precision between the component-by-component compare and the fuzzyQuaternion compare
    // These are just for monitoring, it is possible that these fail if e.g., the fudge factor is changed.
    RotationData a;
    RotationData b;
    QVERIFY(a == b);
    QVERIFY(b == a);

    {
        const QQuaternion qa = QQuaternion(0.5f, 0.5f, 0.5f, 0.5f);
        const QQuaternion qb = QQuaternion(0.5f + 0.0007f, 0.5f, 0.5f, 0.5f);
        QVERIFY(qa != qb);
        QVERIFY(!qFuzzyCompare(qa, qb));
        a = qa;
        b = qb;
        QVERIFY(a != b);
        b = -qb;
        QVERIFY(a != b);
    }
    {
        const QQuaternion qa = QQuaternion(0.5f, 0.5f, 0.5f, 0.5f);
        const QQuaternion qb = QQuaternion(0.5f, 0.5f + 0.001f, 0.5f, 0.5f);
        QVERIFY(qa != qb);
        QVERIFY(!qFuzzyCompare(qa, qb));
        a = qa;
        b = qb;
        QVERIFY(a != b);
        b = -qb;
        QVERIFY(a != b);
    }
    {
        const QQuaternion qa = QQuaternion(0.5f, 0.5f, 0.5f, 0.5f);
        const QQuaternion qb = QQuaternion(0.5f, 0.5f + 0.0007f, 0.5f, 0.5f);
        QVERIFY(qa != qb);
        QVERIFY(!qFuzzyCompare(qa, qb));
        a = qa;
        b = qb;
        QVERIFY(a != b);
        b = -qb;
        QVERIFY(a != b);
    }
    {
        const QQuaternion qa = QQuaternion(0.5f, 0.5f, 0.5f, 0.5f);
        const QQuaternion qb = QQuaternion(0.5f, 0.5f + 0.0007f, 0.5f, 0.5f);
        QVERIFY(qa != qb);
        QVERIFY(!qFuzzyCompare(qa, qb));
        a = qa;
        b = qb;
        QVERIFY(a != b);
        b = -qb;
        QVERIFY(a != b);
    }
    {
        const QQuaternion qa = QQuaternion(1000.f, 0.5f, 0.5f, 0.5f);
        const QQuaternion qb = QQuaternion(1000.f + 0.000001f, 0.5f + 0.000001f, 0.5f, 0.5f);
        QVERIFY(qa != qb);
        QVERIFY(qFuzzyCompare(qa, qb));
        a = qa;
        b = qb;
        QEXPECT_FAIL("", "Difference is too big...", Continue);
        QVERIFY(a == b);
        QVERIFY(qFuzzyCompare(a.m_quatRot, b.m_quatRot)); // Compensate for XFAIL
        b = -qb;
        QEXPECT_FAIL("", "Negation introduces change", Continue);
        QVERIFY(a == b);
        QVERIFY(qFuzzyCompare(-a.m_quatRot, b.m_quatRot)); // Compensate for XFAIL
    }
    // RotationData is preciser than qFuzzyCompare(QQuaternion)
    {
        const float e = std::numeric_limits<float>::epsilon();
        float fa = 1.0f;
        float fb = fa + e;
        QVERIFY(fa != fb);
        const QQuaternion qa = QQuaternion(1.0f, fa, 0.f, 0.f);
        const QQuaternion qb = QQuaternion(1.0f, fb, 0.f, 0.f);

        QVERIFY(qa != qb); // Component wise compare
        QVERIFY(qFuzzyCompare(qa, qb));
        a = qa;
        b = qb;
        QVERIFY(a != b);
        b = -qb;
        QVERIFY(a != b);
        { // Push QQuaternion past it's limits and verify that RotationData still picks up the difference
            const auto qad = qa / 2.0f;
            const auto qbd = qa / 2.0f;

            QVERIFY(qad == qbd); // Component wise compare (now below the threshold).
            QVERIFY(qFuzzyCompare(qad, qbd));
            a = qad;
            b = qbd;
            QVERIFY(a.m_quatRot == b.m_quatRot);
            // When float components are identical after normalization, RotationData correctly reports equal
            QVERIFY(a == b);
            b = -qbd;
            QVERIFY(a == b);
        }
    }
    {
        const QQuaternion qa = QQuaternion(0.5f, 0.5f, 0.5f, 0.5f);
        const QQuaternion qb = QQuaternion(0.5f, 0.5f + 0.000001f, 0.5f, 0.5f);
        QVERIFY(qa != qb);
        QVERIFY(qFuzzyCompare(qa, qb));
        a = qa;
        b = qb;
        QVERIFY(a != b);
        b = -qb;
        QVERIFY(a != b);
    }
    {
        const QQuaternion qa = QQuaternion(0.5f, 0.5f, 0.5f, 0.5f);
        const QQuaternion qb = QQuaternion(0.5f + 0.000001f, 0.5f + 0.000001f, 0.5f, 0.5f);
        QVERIFY(qa != qb);
        QVERIFY(qFuzzyCompare(qa, qb));
        a = qa;
        b = qb;
        QVERIFY(a != b);
        b = -qb;
        QVERIFY(a != b);
    }

    // As above but with a non-normalized quaternion, to verify that the precision
    // is not lost when normalizing the quaternion in the constructor or assignment operator.
    {
        const QQuaternion qa = QQuaternion::fromEulerAngles(QVector3D(45.0f, 30.0f, 15.0f));
        const QQuaternion qb = qa * (1.0f + 1e-6f);

        RotationData rd;
        rd = qb;
        QVERIFY(rd == qb);
        QVERIFY(qb == rd);
        QVERIFY(rd == -qb);
        QVERIFY(-qb == rd);
    }
}

QTEST_APPLESS_MAIN(tst_RotationDataClass)

#include "tst_rotation.moc"
