/****************************************************************************
**
** Copyright (C) 2015 Klaralvdalens Datakonsult AB (KDAB).
** Contact: http://www.qt-project.org/legal
**
** This file is part of the Qt3D module of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL3$
** 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 http://www.qt.io/terms-conditions. For further
** information use the contact form at http://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPLv3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or later as published by the Free
** Software Foundation and appearing in the file LICENSE.GPL included in
** the packaging of this file. Please review the following information to
** ensure the GNU General Public License version 2.0 requirements will be
** met: http://www.gnu.org/licenses/gpl-2.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/

#include <QtTest/QtTest>
#include <Qt3DRenderer/private/renderentity_p.h>
#include <Qt3DRenderer/qraycastingservice.h>
#include <Qt3DRenderer/sphere.h>
#include <Qt3DCore/qboundingvolumeprovider.h>
#include <Qt3DCore/qray3d.h>

using namespace Qt3D;
using namespace Qt3D::Render;

class tst_RayCasting : public QObject
{
    Q_OBJECT
public:
    tst_RayCasting() {}
    ~tst_RayCasting() {}

private Q_SLOTS:
    void shouldReturnValidHandle();
    void shouldReturnResultForEachHandle();
    void shouldReturnAllResults();
    void shouldReturnHits();
    void shouldReturnHits_data();
    void shouldIntersect_data();
    void shouldIntersect();
    void shouldUseProvidedBoudingVolumes();

    void cleanupTestCase();

private:
    Sphere *volumeAt(int index);
    QVector<Sphere> boundingVolumes;
};

void tst_RayCasting::shouldIntersect_data()
{
    QTest::addColumn<QRay3D>("ray");
    QTest::addColumn<Sphere>("sphere");
    QTest::addColumn<int>("shouldIntersect");

    QRay3D ray(QVector3D(1, 1, 1), QVector3D(0, 0, 1));

    Sphere sphere1(QVector3D(1, 1, 1), 2);
    Sphere sphere2(QVector3D(0, 0, 0), 3);
    Sphere sphere3(QVector3D(0, 1, 3), 1);
    Sphere sphere4(QVector3D(4, 4, 5), 1);
    Sphere sphere5(QVector3D(2, 2, 11), 5);
    Sphere sphere6(QVector3D(2, 2, 13), 1);
    Sphere sphere7(QVector3D(2, 2, 15), 5);

    QTest::newRow("Ray starts inside sphere") << ray << sphere1 << 1;
    QTest::newRow("Ray starts inside sphere") << ray << sphere2 << 1;
    QTest::newRow("Ray intersects sphere tangentially") << ray << sphere3 << 1;
    QTest::newRow("No intersection") << ray << sphere4 << 0;
    QTest::newRow("Ray intersect sphere") << ray << sphere5 << 1;
    QTest::newRow("No intersection") << ray << sphere6 << 0;
    QTest::newRow("Ray intersect sphere") << ray << sphere7 << 1;
}

void tst_RayCasting::shouldIntersect()
{
    QFETCH(QRay3D, ray);
    QFETCH(Sphere, sphere);
    QFETCH(int, shouldIntersect);

    QVector3D intersectionPoint;

    QCOMPARE(sphere.intersects(ray, &intersectionPoint), shouldIntersect);
}

class MyBoudingVolumesProvider : public QBoundingVolumeProvider
{
public:
    MyBoudingVolumesProvider(QVector<QBoundingVolume *> volumes)
        : m_volumes(volumes)
    {}

    QVector<QBoundingVolume *> boundingVolumes() const
    {
        return m_volumes;
    }

private:
    QVector<QBoundingVolume *> m_volumes;
};

void tst_RayCasting::shouldReturnValidHandle()
{
    // GIVEN
    QRay3D ray;
    Sphere v1;
    MyBoudingVolumesProvider provider = QVector<QBoundingVolume *>() << &v1;

    QRayCastingService service(&provider);

    // WHEN
    QQueryHandle handle = service.query(ray, QAbstractCollisionQueryService::AllHits);

    // THEN
    QVERIFY(handle >= 0);

    // Wait the query to finish
    service.fetchResult(handle);
}

void tst_RayCasting::shouldReturnResultForEachHandle()
{
    // GIVEN
    QRay3D ray;
    QVector<QBoundingVolume *> volumes;
    MyBoudingVolumesProvider provider(volumes);

    QRayCastingService service(&provider);

    QQueryHandle handle1 = service.query(ray, QAbstractCollisionQueryService::AllHits);
    QQueryHandle handle2 = service.query(ray, QAbstractCollisionQueryService::FirstHit);

    // WHEN
    QCollisionQueryResult result2 = service.fetchResult(handle2);
    QCollisionQueryResult result1 = service.fetchResult(handle1);

    // THEN
    QCOMPARE(result1.handle(), handle1);
    QCOMPARE(result2.handle(), handle2);
}

void tst_RayCasting::shouldReturnAllResults()
{
    // GIVEN
    QRay3D ray;
    QVector<QBoundingVolume *> volumes;
    MyBoudingVolumesProvider provider(volumes);

    QRayCastingService service(&provider);

    QVector<QQueryHandle> handles;
    handles.append(service.query(ray, QAbstractCollisionQueryService::AllHits));
    handles.append(service.query(ray, QAbstractCollisionQueryService::FirstHit));

    // WHEN
    QVector<QCollisionQueryResult> results = service.fetchAllResults();

    // THEN
    bool expectedHandlesFound = true;
    Q_FOREACH (QQueryHandle expected, handles) {
        bool found = false;
        Q_FOREACH (QCollisionQueryResult result, results) {
            if (result.handle() == expected)
                found = true;
        }

        expectedHandlesFound &= found;
    }

    QVERIFY(expectedHandlesFound);
}

void tst_RayCasting::shouldReturnHits_data()
{
    QTest::addColumn<QRay3D>("ray");
    QTest::addColumn<QVector<QBoundingVolume *> >("volumes");
    QTest::addColumn<QVector<QNodeId> >("hits");
    QTest::addColumn<QAbstractCollisionQueryService::QueryMode >("queryMode");

    QRay3D ray(QVector3D(1, 1, 1), QVector3D(0, 0, 1));

    this->boundingVolumes.clear();
    this->boundingVolumes.append(QVector<Sphere>() << Sphere(QVector3D(1, 1, 1), 3, QNodeId::createId())
                                                   << Sphere(QVector3D(0, 0, 0), 3, QNodeId::createId())
                                                   << Sphere(QVector3D(0, 1, 3), 1, QNodeId::createId())
                                                   << Sphere(QVector3D(4, 4, 5), 1, QNodeId::createId())
                                                   << Sphere(QVector3D(2, 2, 11), 5, QNodeId::createId())
                                                   << Sphere(QVector3D(2, 2, 13), 1, QNodeId::createId())
                                                   << Sphere(QVector3D(2, 2, 15), 5, QNodeId::createId()));

    QTest::newRow("All hits, One sphere intersect") << ray
                                          << (QVector<QBoundingVolume *> () << volumeAt(0) << volumeAt(3))
                                          << (QVector<QNodeId>() << volumeAt(0)->id())
                                          << QAbstractCollisionQueryService::AllHits;

    QTest::newRow("All hits, Three sphere intersect") << ray
                                          << (QVector<QBoundingVolume *> () << volumeAt(0) << volumeAt(3) << volumeAt(6) << volumeAt(2))
                                          << (QVector<QNodeId>() << volumeAt(0)->id() << volumeAt(6)->id() << volumeAt(2)->id())
                                          << QAbstractCollisionQueryService::AllHits;

    QTest::newRow("All hits, No sphere intersect") << ray
                                          << (QVector<QBoundingVolume *> () << volumeAt(3)  << volumeAt(5))
                                          << (QVector<QNodeId>())
                                          << QAbstractCollisionQueryService::AllHits;

    QTest::newRow("Sphere 1 intersect, returns First Hit") << ray
                                                    << (QVector<QBoundingVolume *> () << volumeAt(0) << volumeAt(3) << volumeAt(1))
                                                    << (QVector<QNodeId>() << volumeAt(0)->id())
                                                    << QAbstractCollisionQueryService::FirstHit;

    QTest::newRow("Sphere 3 and 5 intersects, returns First Hit") << ray
                                                    << (QVector<QBoundingVolume *> () << volumeAt(3) << volumeAt(6) << volumeAt(4))
                                                    << (QVector<QNodeId>() << volumeAt(4)->id())
                                                    << QAbstractCollisionQueryService::FirstHit;

    QTest::newRow("Sphere 5 and 3 intersects, unordered list, returns First Hit") << ray
                                                    << (QVector<QBoundingVolume *> () << volumeAt(4) << volumeAt(3) << volumeAt(6))
                                                    << (QVector<QNodeId>() << volumeAt(4)->id())
                                                    << QAbstractCollisionQueryService::FirstHit;

    QTest::newRow("No sphere intersect, returns First Hit") << ray
                                                    << (QVector<QBoundingVolume *> () << volumeAt(3)  << volumeAt(5))
                                                    << (QVector<QNodeId>())
                                                    << QAbstractCollisionQueryService::FirstHit;
}

void tst_RayCasting::shouldReturnHits()
{
    // GIVEN
    QFETCH(QRay3D, ray);
    QFETCH(QVector<QBoundingVolume *>, volumes);
    QFETCH(QVector<QNodeId>, hits);
    QFETCH(QAbstractCollisionQueryService::QueryMode, queryMode);

    MyBoudingVolumesProvider provider(volumes);
    QRayCastingService service(&provider);

    // WHEN
    QQueryHandle handle = service.query(ray, queryMode);
    QCollisionQueryResult result = service.fetchResult(handle);

    // THEN
    QCOMPARE(result.entitiesHit().size(), hits.size());
    QCOMPARE(result.entitiesHit(), hits);
}

void tst_RayCasting::shouldUseProvidedBoudingVolumes()
{
    // GIVEN
    QRay3D ray(QVector3D(1, 1, 1), QVector3D(0, 0, 1));

    Sphere sphere1(QVector3D(1, 1, 1), 3);
    Sphere sphere3(QVector3D(0, 1, 3), 1);
    Sphere sphere4(QVector3D(4, 4, 5), 1);

    MyBoudingVolumesProvider provider(QVector<QBoundingVolume *>() << &sphere1 << &sphere4 << &sphere3);
    QVector<QNodeId> hits(QVector<QNodeId>() << sphere1.id() << sphere3.id());

    QRayCastingService service(&provider);

    // WHEN
    QQueryHandle handle = service.query(ray, QAbstractCollisionQueryService::AllHits);
    QCollisionQueryResult result = service.fetchResult(handle);

    // THEN
    QCOMPARE(result.entitiesHit().size(), hits.size());
    QCOMPARE(result.entitiesHit(), hits);
}

void tst_RayCasting::cleanupTestCase()
{
    this->boundingVolumes.clear();
}

Sphere *tst_RayCasting::volumeAt(int index)
{
    return &*(boundingVolumes.begin() + index);
}

QTEST_APPLESS_MAIN(tst_RayCasting)

#include "tst_raycasting.moc"
