/****************************************************************************
**
** Copyright (C) 2020 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Quick 3D.
**
** $QT_BEGIN_LICENSE:GPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 or (at your option) any later version
** approved by the KDE Free Qt Foundation. The licenses are as published by
** the Free Software Foundation and appearing in the file LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/

#include <QtTest>

#include <QtQuick3DRuntimeRender/private/qssgrenderray_p.h>
#include <QtQuick3DRuntimeRender/private/qssgrendererimpllayerrenderhelper_p.h>

class intersection : public QObject
{
    Q_OBJECT

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

private slots:
    void test_aabbIntersection();
    void test_aabbIntersectionScaled();
    void test_aabbIntersectionTranslated();
    void test_aabbIntersectionRotated();
    void test_aabbIntersectionv2();
    void test_aabbIntersectionScaledv2();
    void test_aabbIntersectionTranslatedv2();
    void test_aabbIntersectionRotatedv2();

private:
    static QSSGRenderRay::IntersectionResult intersectWithAABB_proxy(const QMatrix4x4 &inGlobalTransform,
                                                                     const QSSGBounds3 &inBounds,
                                                                     const QSSGRenderRay &ray)
    {
        return QSSGRenderRay::intersectWithAABB(inGlobalTransform, inBounds, ray);
    }
    static QSSGRenderRay::IntersectionResult intersectWithAABBv2_proxy(const QMatrix4x4 &inGlobalTransform,
                                                                       const QSSGBounds3 &inBounds,
                                                                       const QSSGRenderRay &ray)
    {
        QSSGRenderRay::RayData data = QSSGRenderRay::createRayData(inGlobalTransform, ray);
        const auto hit = QSSGRenderRay::intersectWithAABBv2(data, inBounds);
        return hit.intersects() ? QSSGRenderRay::createIntersectionResult(data, hit)
                                : QSSGRenderRay::IntersectionResult();
    }
    using IntersectionFunc = std::function<QSSGRenderRay::IntersectionResult(const QMatrix4x4 &, const QSSGBounds3 &, const QSSGRenderRay &)>;

    void aabbIntersection(const IntersectionFunc &intersectFunc);
    void aabbIntersectionScaled(const IntersectionFunc &intersectFunc);
    void aabbIntersectionTranslated(const IntersectionFunc &intersectFunc);
    void aabbIntersectionRotated(const IntersectionFunc &intersectFunc);
};

void intersection::test_aabbIntersection()
{
    aabbIntersection(&intersection::intersectWithAABB_proxy);
}

void intersection::test_aabbIntersectionScaled()
{
    aabbIntersectionScaled(&intersection::intersectWithAABB_proxy);
}

void intersection::test_aabbIntersectionTranslated()
{
    aabbIntersectionTranslated(&intersection::intersectWithAABB_proxy);
}

void intersection::test_aabbIntersectionRotated()
{
    aabbIntersectionRotated(&intersection::intersectWithAABB_proxy);
}

void intersection::test_aabbIntersectionv2()
{
    aabbIntersection(&intersection::intersectWithAABBv2_proxy);
}

void intersection::test_aabbIntersectionScaledv2()
{
    aabbIntersectionScaled(&intersection::intersectWithAABBv2_proxy);
}

void intersection::test_aabbIntersectionTranslatedv2()
{
    aabbIntersectionTranslated(&intersection::intersectWithAABBv2_proxy);
}

void intersection::test_aabbIntersectionRotatedv2()
{
    aabbIntersectionRotated(&intersection::intersectWithAABBv2_proxy);
}

void intersection::aabbIntersection(const IntersectionFunc &intersectFunc)
{
    QSSGRenderRay::IntersectionResult res;
    QMatrix4x4 globalTransform; // Identity
    // Bounding box of size 1, placed at origin
    QSSGBounds3 bounds { /*min=*/ QVector3D{ -0.5f, -0.5f, -0.5f }, /*max=*/ QVector3D{ 0.5f, 0.5f, 0.5f } };

    // Pick ray goes directly down the z-axis and should hit directly in the middle of the box
    auto pickRay = QSSGRenderRay(/*Origin=*/{0.0f, 0.0f, 10.0f}, /*Direction=*/{0.0f, 0.0f, -1.0f});
    res = intersectFunc(globalTransform, bounds, pickRay);
    QCOMPARE(res.intersects, true);

    { // Test against a flat bounding volume
        QSSGBounds3 boundsFlat { /*min=*/ QVector3D{ -1.0f, 0.0f, -1.0f }, /*max=*/ QVector3D{ 1.0f, 0.0f, 1.0f } };
        res = intersectFunc(globalTransform, boundsFlat, QSSGRenderRay(/*Origin=*/{0.0f, 10.0f, 0.0f}, /*Direction=*/{0.0f, -1.0f, 0.0f}));
        QCOMPARE(res.intersects, true);
    }

    { // horizontal scan
        const int steps = 10;
        const float stepping = (bounds.maximum.x() - bounds.minimum.x()) / float(steps);
        float rayX = bounds.minimum.x() - stepping;
        const float rayY = (bounds.maximum.y() - bounds.minimum.y()) / 2.0f;

        // First pick should fail, as it's just outside the bound's x extents
        QVERIFY(rayX < bounds.maximum.x());
        pickRay = QSSGRenderRay(/*Origin=*/{rayX, rayY, 10.0f}, /*Direction=*/{0.0f, 0.0f, -1.0f});
        res = intersectFunc(globalTransform, bounds, pickRay);
        QCOMPARE(res.intersects, false);

        // scan through the bounding box along the x-axis
        rayX = bounds.minimum.x();
        for (int i = 0; i != steps; ++i, rayX += stepping) {
            pickRay = QSSGRenderRay(/*Origin=*/{rayX, rayY, 10.0f}, /*Direction=*/{0.0f, 0.0f, -1.0f});
            res = intersectFunc(globalTransform, bounds, pickRay);
            QCOMPARE(res.intersects, true);
        }

        // Do one more step to go outside the box and test that we don't hit.
        rayX += stepping;
        pickRay = QSSGRenderRay(/*Origin=*/{rayX, rayY, 10.0f}, /*Direction=*/{0.0f, 0.0f, -1.0f});
        res = intersectFunc(globalTransform, bounds, pickRay);
        QCOMPARE(res.intersects, false);
    }

    { // vertical scan
        const int steps = 10;
        const float stepping = (bounds.maximum.y() - bounds.minimum.y()) / float(steps);
        float rayY = bounds.minimum.y() - stepping;
        const float rayX = (bounds.maximum.x() - bounds.minimum.x()) / 2.0f;

        // First pick should fail, as it's just outside the bound's y extents
        QVERIFY(rayY < bounds.maximum.y());
        pickRay = QSSGRenderRay(/*Origin=*/{rayY, rayX, 10.0f}, /*Direction=*/{0.0f, 0.0f, -1.0f});
        res = intersectFunc(globalTransform, bounds, pickRay);
        QCOMPARE(res.intersects, false);

        // scan through the bounding box along the y-axis
        rayY = bounds.minimum.y();
        for (int i = 0; i != steps; ++i, rayY += stepping) {
            pickRay = QSSGRenderRay(/*Origin=*/{rayY, rayX, 10.0f}, /*Direction=*/{0.0f, 0.0f, -1.0f});
            res = intersectFunc(globalTransform, bounds, pickRay);
            QCOMPARE(res.intersects, true);
        }

        // Do one more step to go outside the box and test that we don't hit.
        rayY += stepping;
        pickRay = QSSGRenderRay(/*Origin=*/{rayY, rayX, 10.0f}, /*Direction=*/{0.0f, 0.0f, -1.0f});
        res = intersectFunc(globalTransform, bounds, pickRay);
        QCOMPARE(res.intersects, false);
    }
}

void intersection::aabbIntersectionScaled(const IntersectionFunc &intersectFunc)
{
    QSSGRenderRay::IntersectionResult res;
    QMatrix4x4 globalTransform; // Identity
    // Bounding box of size 1, placed at origin
    QSSGBounds3 bounds { /*min=*/ QVector3D{ -0.5f, -0.5f, -0.5f }, /*max=*/ QVector3D{ 0.5f, 0.5f, 0.5f } };

    // Pick ray goes directly down the z-axis and should hit directly in the middle of the box
    auto pickRay = QSSGRenderRay(/*Origin=*/{0.0f, 0.0f, 10.0f}, /*Direction=*/{0.0f, 0.0f, -1.0f});
    res = intersectFunc(globalTransform, bounds, pickRay);
    QCOMPARE(res.intersects, true);

    { // Scale +
        globalTransform.setToIdentity();
        const float scale = 2.0f;
        globalTransform.scale(scale);
        const float rayY = bounds.minimum.y() * scale;
        const float rayX = bounds.minimum.x() * scale;
        pickRay = QSSGRenderRay(/*Origin=*/{rayY, rayX, 10.0f}, /*Direction=*/{0.0f, 0.0f, -1.0f});
        res = intersectFunc(globalTransform, bounds, pickRay);
        QCOMPARE(res.intersects, true);
    }

    { // Scale -
        globalTransform.setToIdentity();
        const float scale = -2.0f;
        globalTransform.scale(scale);
        const float rayY = bounds.maximum.y() * scale;
        const float rayX = bounds.maximum.x() * scale;
        pickRay = QSSGRenderRay(/*Origin=*/{rayY, rayX, 10.0f}, /*Direction=*/{0.0f, 0.0f, -1.0f});
        res = intersectFunc(globalTransform, bounds, pickRay);
        QCOMPARE(res.intersects, true);
    }
}

void intersection::aabbIntersectionTranslated(const IntersectionFunc &intersectFunc)
{
    QSSGRenderRay::IntersectionResult res;
    QMatrix4x4 globalTransform; // Identity
    // Bounding box of size 1, placed at origin
    QSSGBounds3 bounds { /*min=*/ QVector3D{ -0.5f, -0.5f, -0.5f }, /*max=*/ QVector3D{ 0.5f, 0.5f, 0.5f } };

    // Pick ray goes directly down the z-axis and should hit directly in the middle of the box
    auto pickRay = QSSGRenderRay(/*Origin=*/{0.0f, 0.0f, 10.0f}, /*Direction=*/{0.0f, 0.0f, -1.0f});
    res = intersectFunc(globalTransform, bounds, pickRay);
    QCOMPARE(res.intersects, true);

    { // Translate 1
        globalTransform.setToIdentity();
        const float uniformTranslation = 1.0f;
        globalTransform.translate(uniformTranslation, uniformTranslation);

        // Verify that we can get a hit in the center of the bounding box
        pickRay = QSSGRenderRay(/*Origin=*/{0.0f, 0.0f, 10.0f}, /*Direction=*/{0.0f, 0.0f, -1.0f});
        res = intersectFunc(globalTransform, bounds, pickRay);
        QCOMPARE(res.intersects, false);

        const float rayY = bounds.minimum.y() + uniformTranslation;
        const float rayX = bounds.minimum.x() + uniformTranslation;
        pickRay = QSSGRenderRay(/*Origin=*/{rayX, rayY, 10.0f}, /*Direction=*/{0.0f, 0.0f, -1.0f});
        res = intersectFunc(globalTransform, bounds, pickRay);
        QCOMPARE(res.intersects, true);
    }

    { // Translate 2
        globalTransform.setToIdentity();
        const float translationX = -1.0f;
        const float translationY = 1.0f;
        globalTransform.translate(translationX, translationY);

        // Verify that we can get a hit in the center of the bounding box
        pickRay = QSSGRenderRay(/*Origin=*/{0.0f, 0.0f, 10.0f}, /*Direction=*/{0.0f, 0.0f, -1.0f});
        res = intersectFunc(globalTransform, bounds, pickRay);
        QCOMPARE(res.intersects, false);

        const float rayY = bounds.minimum.y() + translationY;
        const float rayX = bounds.minimum.x() + translationX;
        pickRay = QSSGRenderRay(/*Origin=*/{rayX, rayY, 10.0f}, /*Direction=*/{0.0f, 0.0f, -1.0f});
        res = intersectFunc(globalTransform, bounds, pickRay);
        QCOMPARE(res.intersects, true);
    }

    { // Translate 3
        globalTransform.setToIdentity();
        const float uniformTranslation = -1.0f;
        globalTransform.translate(uniformTranslation, uniformTranslation);

        // Verify that we can get a hit in the center of the bounding box
        pickRay = QSSGRenderRay(/*Origin=*/{0.0f, 0.0f, 10.0f}, /*Direction=*/{0.0f, 0.0f, -1.0f});
        res = intersectFunc(globalTransform, bounds, pickRay);
        QCOMPARE(res.intersects, false);

        const float rayY = bounds.maximum.y() + uniformTranslation;
        const float rayX = bounds.maximum.x() + uniformTranslation;
        pickRay = QSSGRenderRay(/*Origin=*/{rayX, rayY, 10.0f}, /*Direction=*/{0.0f, 0.0f, -1.0f});
        res = intersectFunc(globalTransform, bounds, pickRay);
        QCOMPARE(res.intersects, true);
    }

    { // Translate 4
        globalTransform.setToIdentity();
        const float translationX = 1.0f;
        const float translationY = -1.0f;
        globalTransform.translate(translationX, translationY);

        // Verify that we can get a hit in the center of the bounding box
        pickRay = QSSGRenderRay(/*Origin=*/{0.0f, 0.0f, 10.0f}, /*Direction=*/{0.0f, 0.0f, -1.0f});
        res = intersectFunc(globalTransform, bounds, pickRay);
        QCOMPARE(res.intersects, false);

        const float rayY = bounds.minimum.y() + translationY;
        const float rayX = bounds.minimum.x() + translationX;
        pickRay = QSSGRenderRay(/*Origin=*/{rayX, rayY, 10.0f}, /*Direction=*/{0.0f, 0.0f, -1.0f});
        res = intersectFunc(globalTransform, bounds, pickRay);
        QCOMPARE(res.intersects, true);
    }
}

void intersection::aabbIntersectionRotated(const IntersectionFunc &intersectFunc)
{
    QSSGRenderRay::IntersectionResult res;
    QMatrix4x4 globalTransform; // Identity
    // Bounding box of size 1, placed at origin
    QSSGBounds3 bounds { /*min=*/ QVector3D{ -0.5f, -0.5f, -0.5f }, /*max=*/ QVector3D{ 0.5f, 0.5f, 0.5f } };

    // Pick ray goes directly down the z-axis and should hit directly in the middle of the box
    auto pickRay = QSSGRenderRay(/*Origin=*/{0.0f, 0.0f, 10.0f}, /*Direction=*/{0.0f, 0.0f, -1.0f});
    res = intersectFunc(globalTransform, bounds, pickRay);
    QCOMPARE(res.intersects, true);

    { // Rotate
        globalTransform.setToIdentity();
        const float rotation = 45.0f;
        globalTransform.rotate(rotation, {0.0f, 0.0f, 1.0f});

        // Verify that we can get a hit in the center of the bounding box
        pickRay = QSSGRenderRay(/*Origin=*/{0.0f, 0.0f, 10.0f}, /*Direction=*/{0.0f, 0.0f, -1.0f});
        res = intersectFunc(globalTransform, bounds, pickRay);
        QCOMPARE(res.intersects, true);

        float rayX = bounds.minimum.x();
        float rayY = bounds.minimum.y();
        pickRay = QSSGRenderRay(/*Origin=*/{rayX, rayY, 10.0f}, /*Direction=*/{0.0f, 0.0f, -1.0f});
        res = intersectFunc(globalTransform, bounds, pickRay);
        QCOMPARE(res.intersects, false);

        rayX = bounds.maximum.x();
        rayY = bounds.maximum.y();
        pickRay = QSSGRenderRay(/*Origin=*/{rayX, rayY, 10.0f}, /*Direction=*/{0.0f, 0.0f, -1.0f});
        res = intersectFunc(globalTransform, bounds, pickRay);
        QCOMPARE(res.intersects, false);

        const auto center = bounds.center();
        rayX = center.x();
        rayY = center.y() + std::sqrt(bounds.dimensions().x() + bounds.dimensions().y()) * 0.5f;
        pickRay = QSSGRenderRay(/*Origin=*/{rayX, rayY, 10.0f}, /*Direction=*/{0.0f, 0.0f, -1.0f});
        res = intersectFunc(globalTransform, bounds, pickRay);
        QCOMPARE(res.intersects, true);

        rayX = center.x() + std::sqrt(bounds.dimensions().x() + bounds.dimensions().y()) * 0.5f;
        rayY = center.y();
        pickRay = QSSGRenderRay(/*Origin=*/{rayX, rayY, 10.0f}, /*Direction=*/{0.0f, 0.0f, -1.0f});
        res = intersectFunc(globalTransform, bounds, pickRay);
        QCOMPARE(res.intersects, true);
    }
}

QTEST_APPLESS_MAIN(intersection)

#include "tst_intersection.moc"
