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

#include <QtTest/QTest>
#include <qbackendnodetester.h>
#include <testrenderer.h>

#include <Qt3DCore/private/qscene_p.h>
#include <Qt3DRender/private/renderstatenode_p.h>
#include <Qt3DRender/private/managers_p.h>

#include <Qt3DRender/QRenderState>
#include <Qt3DRender/QAlphaCoverage>
#include <Qt3DRender/QAlphaTest>
#include <Qt3DRender/QBlendEquation>
#include <Qt3DRender/QBlendEquationArguments>
#include <Qt3DRender/QColorMask>
#include <Qt3DRender/QCullFace>
#include <Qt3DRender/QDepthRange>
#include <Qt3DRender/QDepthTest>
#include <Qt3DRender/QDithering>
#include <Qt3DRender/QFrontFace>
#include <Qt3DRender/QPointSize>
#include <Qt3DRender/QPolygonOffset>
#include <Qt3DRender/QScissorTest>
#include <Qt3DRender/QStencilTest>
#include <Qt3DRender/QStencilTestArguments>
#include <Qt3DRender/QStencilMask>
#include <Qt3DRender/QStencilOperation>
#include <Qt3DRender/QStencilOperationArguments>
#include <Qt3DRender/QClipPlane>

#include "testarbiter.h"

using namespace Qt3DCore;
using namespace Qt3DRender;
using namespace Qt3DRender::Render;

class tst_QRenderState : public QBackendNodeTester
{
    Q_OBJECT
public:
    tst_QRenderState() {}
    ~tst_QRenderState() {}

private:
    RenderStateManager m_renderStateManager;
    TestRenderer m_renderer;

    RenderStateNode* createBackendNode(QRenderState *frontend)
    {
        RenderStateNode *backend = m_renderStateManager.getOrCreateResource(frontend->id());
        backend->setRenderer(&m_renderer);
        simulateInitializationSync(frontend, backend);
        return backend;
    }

    // Create two frontend objects of class T and set given property to v1 / v2 respectively
    template <class T, class V, class Setter>
    void addTestCase(Qt3DRender::Render::StateMask mask, const char *property, Setter setter, V v1, V v2)
    {
        T *obj1 = new T();
        T *obj2 = new T();

        (obj1->*(setter))(v1);
        (obj2->*(setter))(v2);

        QTest::addRow("%s::%s", obj1->metaObject()->className(), property)
                << (QRenderState*) obj1
                << (QRenderState*) obj2
                << (quint64) mask
                << QString(property)
                << QVariant(v2);
    }

    // Create two frontend objects of class T and set given property to v1 / v2 respectively, for the specified arguments sub-object
    template <class T, class Args, class V, class Setter>
    void addStencilTestCase(Qt3DRender::Render::StateMask mask, const char *argsName, const char *property, Setter setter, V v1, V v2)
    {
        T *obj1 = new T();
        T *obj2 = new T();

        const QMetaObject *metaObj = obj1->metaObject();
        const int pIndex = metaObj->indexOfProperty(argsName);
        const QMetaProperty prop = metaObj->property(pIndex);
        Args *args1 = qvariant_cast<Args*>(prop.read(obj1));
        Args *args2 = qvariant_cast<Args*>(prop.read(obj2));

        (args1->*(setter))(v1);
        (args2->*(setter))(v2);

        QTest::addRow("%s::%s::%s", metaObj->className(), argsName, property)
                << (QRenderState*) obj1
                << (QObject*) args1
                << (QRenderState*) obj2
                << (quint64) mask
                << QString(property)
                << QVariant(v2);
    }

private Q_SLOTS:

    void checkPropertyUpdates_data()
    {
        QTest::addColumn<QRenderState *>("frontend1");
        QTest::addColumn<QRenderState *>("frontend2");
        QTest::addColumn<quint64>("mask");
        QTest::addColumn<QString>("propertyName");
        QTest::addColumn<QVariant>("value");

        addTestCase<QAlphaTest>(AlphaTestMask, "alphaFunction", &QAlphaTest::setAlphaFunction, QAlphaTest::Always, QAlphaTest::LessOrEqual);
        addTestCase<QAlphaTest>(AlphaTestMask, "referenceValue", &QAlphaTest::setReferenceValue, 0.f, 1.f);

        addTestCase<QBlendEquation>(BlendStateMask, "blendFunction", &QBlendEquation::setBlendFunction, QBlendEquation::Add, QBlendEquation::Subtract);

        addTestCase<QBlendEquationArguments>(BlendEquationArgumentsMask, "sourceRgb", &QBlendEquationArguments::setSourceRgb, QBlendEquationArguments::Zero, QBlendEquationArguments::One);
        addTestCase<QBlendEquationArguments>(BlendEquationArgumentsMask, "sourceAlpha", &QBlendEquationArguments::setSourceAlpha, QBlendEquationArguments::SourceAlpha, QBlendEquationArguments::OneMinusSource1Color);
        addTestCase<QBlendEquationArguments>(BlendEquationArgumentsMask, "destinationRgb", &QBlendEquationArguments::setDestinationRgb, QBlendEquationArguments::DestinationColor, QBlendEquationArguments::OneMinusSourceAlpha);
        addTestCase<QBlendEquationArguments>(BlendEquationArgumentsMask, "destinationAlpha", &QBlendEquationArguments::setDestinationAlpha, QBlendEquationArguments::DestinationAlpha, QBlendEquationArguments::DestinationColor);
        addTestCase<QBlendEquationArguments>(BlendEquationArgumentsMask, "bufferIndex", &QBlendEquationArguments::setBufferIndex, 0, 1);

        addTestCase<QClipPlane>(ClipPlaneMask, "planeIndex", &QClipPlane::setPlaneIndex, 0, 1);
        addTestCase<QClipPlane>(ClipPlaneMask, "normal", &QClipPlane::setNormal, QVector3D(0, 1, 0), QVector3D(0, 0, 1));
        addTestCase<QClipPlane>(ClipPlaneMask, "distance", &QClipPlane::setDistance, 1.f, 2.f);

        addTestCase<QColorMask>(ColorStateMask, "redMasked", &QColorMask::setRedMasked, false, true);
        addTestCase<QColorMask>(ColorStateMask, "blueMasked", &QColorMask::setBlueMasked, false, true);
        addTestCase<QColorMask>(ColorStateMask, "greenMasked", &QColorMask::setGreenMasked, false, true);
        addTestCase<QColorMask>(ColorStateMask, "alphaMasked", &QColorMask::setAlphaMasked, false, true);

        addTestCase<QCullFace>(CullFaceStateMask, "mode", &QCullFace::setMode, QCullFace::Back, QCullFace::FrontAndBack);

        addTestCase<QFrontFace>(FrontFaceStateMask, "direction", &QFrontFace::setDirection, QFrontFace::ClockWise, QFrontFace::CounterClockWise);

        addTestCase<QPointSize>(PointSizeMask, "sizeMode", &QPointSize::setSizeMode, QPointSize::Programmable, QPointSize::Fixed);
        addTestCase<QPointSize>(PointSizeMask, "value", &QPointSize::setValue, 2.f, 4.f);

        addTestCase<QPolygonOffset>(PolygonOffsetStateMask, "scaleFactor", &QPolygonOffset::setScaleFactor, 1.f, 2.f);
        addTestCase<QPolygonOffset>(PolygonOffsetStateMask, "depthSteps", &QPolygonOffset::setDepthSteps, 1.f, 2.f);

        addTestCase<QScissorTest>(ScissorStateMask, "left", &QScissorTest::setLeft, 10, 20);
        addTestCase<QScissorTest>(ScissorStateMask, "bottom", &QScissorTest::setBottom, 10, 20);
        addTestCase<QScissorTest>(ScissorStateMask, "width", &QScissorTest::setWidth, 10, 20);
        addTestCase<QScissorTest>(ScissorStateMask, "height", &QScissorTest::setHeight, 10, 20);

        addTestCase<QStencilMask>(StencilWriteStateMask, "frontOutputMask", &QStencilMask::setFrontOutputMask, 0x12, 0x34);
        addTestCase<QStencilMask>(StencilWriteStateMask, "backOutputMask", &QStencilMask::setBackOutputMask, 0x12, 0x34);

        addTestCase<QDepthRange>(DepthRangeMask, "nearValue", &QDepthRange::setNearValue, 0.1, 0.2);
        addTestCase<QDepthRange>(DepthRangeMask, "farValue", &QDepthRange::setFarValue, 0.5, 0.6);
    }

    void checkPropertyUpdates()
    {
        // GIVEN
        QFETCH(QRenderState*, frontend1);
        QFETCH(QRenderState*, frontend2);
        QFETCH(quint64, mask);
        QFETCH(QString, propertyName);
        QFETCH(QVariant, value);

        // THEN
        RenderStateNode *backend1 = createBackendNode(frontend1);
        RenderStateNode *backend2 = createBackendNode(frontend2);

        QVERIFY(backend1->type() == mask);
        QVERIFY(backend2->type() == mask);
        QVERIFY(backend1->impl() != backend2->impl());

        // WHEN
        TestArbiter arbiter;
        arbiter.setArbiterOnNode(frontend1);
        const QMetaObject *metaObj = frontend1->metaObject();
        const int pIndex = metaObj->indexOfProperty(propertyName.toStdString().c_str());
        metaObj->property(pIndex).write(frontend1, value);
        QCoreApplication::processEvents();

        // THEN
        QCOMPARE(arbiter.dirtyNodes().size(), 1);
        QCOMPARE(arbiter.dirtyNodes().front(), frontend1);

        // WHEN
        backend1->syncFromFrontEnd(frontend1, false);

        // THEN
        QVERIFY(backend1->impl() == backend2->impl());

        arbiter.clear();
    }

    void checkStencilUpdates_data()
    {
        QTest::addColumn<QRenderState *>("frontend1");
        QTest::addColumn<QObject *>("args1");
        QTest::addColumn<QRenderState *>("frontend2");
        QTest::addColumn<quint64>("mask");
        QTest::addColumn<QString>("propertyName");
        QTest::addColumn<QVariant>("value");

        qRegisterMetaType<QStencilOperationArguments*>("QStencilOperationArguments*");
        qRegisterMetaType<QStencilTestArguments*>("QStencilTestArguments*");

        for (bool front : QList<bool> { false, true }) {
            const char *argsProperty = front ? "front" : "back";

            addStencilTestCase<QStencilOperation, QStencilOperationArguments>(
                        StencilOpMask, argsProperty, "allTestsPassOperation",
                        &QStencilOperationArguments::setAllTestsPassOperation,
                        QStencilOperationArguments::Zero, QStencilOperationArguments::Keep);

            addStencilTestCase<QStencilOperation, QStencilOperationArguments>(
                        StencilOpMask, argsProperty, "depthTestFailureOperation",
                        &QStencilOperationArguments::setDepthTestFailureOperation,
                        QStencilOperationArguments::Replace, QStencilOperationArguments::Zero);

            addStencilTestCase<QStencilOperation, QStencilOperationArguments>(
                        StencilOpMask, argsProperty, "stencilTestFailureOperation",
                        &QStencilOperationArguments::setStencilTestFailureOperation,
                        QStencilOperationArguments::Increment, QStencilOperationArguments::Decrement);

            addStencilTestCase<QStencilTest, QStencilTestArguments>(
                        StencilTestStateMask, argsProperty, "comparisonMask",
                        &QStencilTestArguments::setComparisonMask, 0x12, 0x34);

            addStencilTestCase<QStencilTest, QStencilTestArguments>(
                        StencilTestStateMask, argsProperty, "referenceValue",
                        &QStencilTestArguments::setReferenceValue, 1, 2);

            addStencilTestCase<QStencilTest, QStencilTestArguments>(
                        StencilTestStateMask, argsProperty, "stencilFunction",
                        &QStencilTestArguments::setStencilFunction,
                        QStencilTestArguments::Always, QStencilTestArguments::Equal);
        }
    }

    void checkStencilUpdates()
    {
        // GIVEN
        QFETCH(QRenderState*, frontend1);
        QFETCH(QObject*, args1);
        QFETCH(QRenderState*, frontend2);
        QFETCH(quint64, mask);
        QFETCH(QString, propertyName);
        QFETCH(QVariant, value);

        // THEN
        RenderStateNode *backend1 = createBackendNode(frontend1);
        RenderStateNode *backend2 = createBackendNode(frontend2);
        QVERIFY(backend1->type() == mask);
        QVERIFY(backend2->type() == mask);
        QVERIFY(backend1->impl() != backend2->impl());

        // WHEN
        TestArbiter arbiter;
        arbiter.setArbiterOnNode(frontend1);
        const QMetaObject *metaObj = args1->metaObject();
        const int pIndex = metaObj->indexOfProperty(propertyName.toStdString().c_str());
        metaObj->property(pIndex).write(args1, value);
        QCoreApplication::processEvents();

        // THEN
        QCOMPARE(arbiter.dirtyNodes().size(), 1);
        QCOMPARE(arbiter.dirtyNodes().front(), frontend1);

        // WHEN
        backend1->syncFromFrontEnd(frontend1, false);

        // THEN
        QVERIFY(backend1->impl() == backend2->impl());
    }
};

QTEST_MAIN(tst_QRenderState)

#include "tst_qrenderstate.moc"
