/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/

#include <catch2/catch_all.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>

#include "Math/Math.hpp"
#include "Scene/Ray.hpp"
#include "Scene/GeometryFactory.hpp"

using namespace OpenVulkano::Math;
using namespace OpenVulkano::Math::Utils;
using namespace OpenVulkano::Scene;

namespace
{
	void CompareVec3Approx(const Vector3f& f, const Vector3f& s)
	{
		REQUIRE_THAT(f.x, Catch::Matchers::WithinRel(s.x));
		REQUIRE_THAT(f.x, Catch::Matchers::WithinRel(s.x));
		REQUIRE_THAT(f.x, Catch::Matchers::WithinRel(s.x));
	}
};

TEST_CASE("RaySphereIntersection")
{
	auto sphere = GeometryFactory::MakeSphere(1, 32, 16);
	RayHit h1, h2;
	// 2 intersections
	{
		Ray ray(Vector3f(0, 0, -5), Vector3f(0, 0, 1));
		REQUIRE(ray.IntersectSphere(Vector3f(0), 1, h1, h2) == 2);
		REQUIRE((h1.point == Vector3f(0, 0, -1) && h2.point == Vector3f(0, 0, 1)));
		REQUIRE((h1.normal == Vector3f(0, 0, -1) && h2.normal == Vector3f(0, 0, 1)));
		REQUIRE(h1.distance < h2.distance);
		REQUIRE((h1.distance == distance(ray.GetOrigin(), h1.point) && h2.distance == distance(ray.GetOrigin(), h2.point)));
		// this returns just closest point
		if (auto opt = ray.IntersectSphere(Vector3f(0), 1))
		{
			REQUIRE(opt.value() == h1);
		}
	}
	// 1 intersection
	{
		Ray ray(Vector3f(1, 0, -1), Vector3f(0, 0, 1));
		REQUIRE(ray.IntersectSphere(Vector3f(0), 1, h1, h2) == 1);
		REQUIRE(h1 == h2);
		REQUIRE(h1.point == Vector3f(1, 0, 0));
		REQUIRE(ray.IntersectSphere(Vector3f(0), 1).value() == h1);
	}
	// 0 intersections
	{
		Ray ray(Vector3f(2, 0, -1), Vector3f(0, 0, 1));
		REQUIRE(ray.IntersectSphere(Vector3f(0), 1, h1, h2) == 0);
		REQUIRE(!ray.IntersectSphere(Vector3f(0), 1).has_value());
	}
	// ray is inside sphere
	{
		Ray ray(Vector3f(0, 0, 0), Vector3f(0.5, 0.5, 1));
		REQUIRE(ray.IntersectSphere(Vector3f(0), 1, h1, h2) == 1);
		REQUIRE(h1 == h2);
		::CompareVec3Approx(h1.normal, h1.point);
		auto value = ray.IntersectSphere(Vector3f(0), 1);
		REQUIRE(value->distance == h1.distance);
		::CompareVec3Approx(value->normal, h1.normal);
		::CompareVec3Approx(value->point, h1.point);
	}
	// ray intersects sphere behind the origin
	{
		Ray ray(Vector3f(0, 0, 3), Vector3f(0, 0, 1));
		REQUIRE(ray.IntersectSphere(Vector3f(0), 1, h1, h2) == 0);
		REQUIRE(!ray.IntersectSphere(Vector3f(0), 1).has_value());
	}
}

TEST_CASE("RayTriangleIntersection")
{
	auto tri = GeometryFactory::MakeTriangle(Vector3f(0), Vector3f(3, 0, 0), Vector3f(1.5, 2, 0));
	std::optional<RayHit> hit;
	// intersects
	{
		Ray ray(Vector3f(1.5, 2, -5), Vector3f(0, 0, 1));
		hit = ray.IntersectTriangle(tri.vertices[0].position, tri.vertices[1].position, tri.vertices[2].position);
		REQUIRE(hit.has_value());
		REQUIRE(hit->distance == distance(ray.GetOrigin(), hit->point));
		REQUIRE(hit->point == Vector3f(1.5, 2, 0));
	}
	{
		Ray ray(Vector3f(1.5, 1, -1), Vector3f(0, 0, 1));
		hit = ray.IntersectTriangle(tri.vertices[0].position, tri.vertices[1].position, tri.vertices[2].position);
		REQUIRE(hit.has_value());
		REQUIRE(hit->distance == distance(ray.GetOrigin(), hit->point));
		REQUIRE(hit->point == Vector3f(1.5, 1, 0));
	}
	// no intersections
	{
		Ray ray(Vector3f(5, 0, 0), Vector3f(0, 0, 1));
		hit = ray.IntersectTriangle(tri.vertices[0].position, tri.vertices[1].position, tri.vertices[2].position);
		REQUIRE(!hit.has_value());
	}
	{
		Ray ray(Vector3f(1.5, 1, 0.5), Vector3f(0, 0, 1));
		hit = ray.IntersectTriangle(tri.vertices[0].position, tri.vertices[1].position, tri.vertices[2].position);
		REQUIRE(!hit.has_value());
	}
}

TEST_CASE("RayQuadIntersection")
{
	auto cube = GeometryFactory::MakeCube();
	std::optional<RayHit> hit;
	// intersects
	{
		Ray ray(Vector3f(0), Vector3f(0, 0, 1));
		// front face
		hit = ray.IntersectQuad(cube.vertices[0].position, cube.vertices[1].position, cube.vertices[2].position,
		                  cube.vertices[3].position);
		REQUIRE(hit.has_value());
		REQUIRE(hit->point == Vector3f(0, 0, 0.5));
	}
	// no intersections
	{
		Ray ray(Vector3f(0), Vector3f(0, 0, -1));
		hit = ray.IntersectQuad(cube.vertices[0].position, cube.vertices[1].position, cube.vertices[2].position,
		                        cube.vertices[3].position);
		REQUIRE(!hit.has_value());
	}
	{
		Ray ray(Vector3f(1, 1, 0), Vector3f(0, 0, 1));
		hit = ray.IntersectQuad(cube.vertices[0].position, cube.vertices[1].position, cube.vertices[2].position,
		                        cube.vertices[3].position);
		REQUIRE(!hit.has_value());
	}
}

TEST_CASE("RayAABBIntersection")
{
	auto sphere = GeometryFactory::MakeSphere(1, 32, 16);
	sphere.CalculateAABB();
	std::optional<RayHit> hit;
	RayHit h1, h2;
	// intersects
	{
		Ray ray(Vector3f(0, 0, -2), Vector3f(0, 0, 1));
		REQUIRE(ray.IntersectAABB(sphere.aabb, h1, h2) == 2);
		REQUIRE(h1.distance < h2.distance);
		REQUIRE(h1.point == Vector3f(0, 0, -1));
		REQUIRE(h2.point == Vector3f(0, 0, 1));
		auto p = ray.IntersectAABB(sphere.aabb);
		REQUIRE(p->point == h1.point);
		REQUIRE(p->distance == h1.distance);
	}
	{
		Ray ray(Vector3f(0, 0, 1), Vector3f(0, 0, 1));
		REQUIRE(ray.IntersectAABB(sphere.aabb, h1, h2) == 1);
		REQUIRE(h1.distance == h2.distance);
		CompareVec3Approx(h1.point, h2.point);
		REQUIRE(h1.point == Vector3f(0, 0, 1));
		REQUIRE(ray.IntersectAABB(sphere.aabb)->distance == h1.distance);
	}
	{
		// inside sphere
		Ray ray(Vector3f(0), Vector3f(0.3));
		REQUIRE(ray.IntersectAABB(sphere.aabb, h1, h2) == 1);
		REQUIRE(h1 == h2);
		auto val = ray.IntersectAABB(sphere.aabb);
		REQUIRE(val.has_value());
		REQUIRE_THAT(val->distance, Catch::Matchers::WithinRel(h1.distance));
		CompareVec3Approx(val->point, h1.point);
	}
	{
		Ray ray(Vector3f(2, -0.5, 1.5), Vector3f(-2, 0.5, -1.5));
		REQUIRE(ray.IntersectAABB(sphere.aabb, h1, h2) == 2);
		REQUIRE(h1.distance < h2.distance);
		auto val = ray.IntersectAABB(sphere.aabb);
		REQUIRE(val.has_value());
		REQUIRE(val->distance == h1.distance);
		CompareVec3Approx(val->point, h1.point);
	}
	// no intersections
	{
		Ray ray(Vector3f(3, 0, 1), Vector3f(0, 0, 1));
		REQUIRE(ray.IntersectAABB(sphere.aabb, h1, h2) == 0);
		REQUIRE(!ray.IntersectAABB(sphere.aabb).has_value());
	}
	{
		Ray ray(Vector3f(0, 0, 1.1), Vector3f(0, 0, 1));
		REQUIRE(ray.IntersectAABB(sphere.aabb, h1, h2) == 0);
		REQUIRE(!ray.IntersectAABB(sphere.aabb).has_value());
	}
}

TEST_CASE("RayPlaneIntersection") 
{ 
	Vector3f pOrigin = Vector3f(0);
	Vector3f pNorm = Vector3f(0, 1, 0);
	{
		Ray ray(Vector3f(2, -2, 2), Vector3f(0, 1, 0));
		auto hit = ray.IntersectPlane(pOrigin, pNorm);
		REQUIRE(hit.has_value());
		REQUIRE(hit->normal == pNorm);
		REQUIRE(hit->distance == 2.f);
		REQUIRE(hit->point == Vector3f(2, 0, 2));
	}
	{
		Ray ray(Vector3f(2, -2, 2), Vector3f(1, 0, 0));
		REQUIRE(!ray.IntersectPlane(pOrigin, pNorm));
	}
	{
		Ray ray(Vector3f(0), Vector3f(1, 0, 0));
		REQUIRE(!ray.IntersectPlane(pOrigin, pNorm));
	}
}
