/*
 * 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/.
 */

#pragma once

#include "Math.hpp"
#include "Range.hpp"

#ifdef NEAR
#undef NEAR
#endif
#ifdef FAR
#undef FAR
#endif

namespace OpenVulkano::Math
{
	class Frustum
	{
		enum Planes
		{
			LEFT = 0,
			RIGHT,
			BOTTOM,
			TOP,
			NEAR,
			FAR,
			COUNT,
			COMBINATIONS = COUNT * (COUNT - 1) / 2
		};

	public:
		Frustum()
		{}

		// m = ProjectionMatrix * ViewMatrix
		Frustum(Math::Matrix4f m)
		{
			m = Math::Utils::transpose(m);
			m_planes[LEFT] =   m[3] + m[0];
			m_planes[RIGHT] =  m[3] - m[0];
			m_planes[BOTTOM] = m[3] + m[1];
			m_planes[TOP] =    m[3] - m[1];
			m_planes[NEAR] =   m[3] + m[2];
			m_planes[FAR] =    m[3] - m[2];

			Math::Vector3f crosses[COMBINATIONS] = {
					Math::Utils::cross(Math::Vector3f(m_planes[LEFT]),   Math::Vector3f(m_planes[RIGHT])),
					Math::Utils::cross(Math::Vector3f(m_planes[LEFT]),   Math::Vector3f(m_planes[BOTTOM])),
					Math::Utils::cross(Math::Vector3f(m_planes[LEFT]),   Math::Vector3f(m_planes[TOP])),
					Math::Utils::cross(Math::Vector3f(m_planes[LEFT]),   Math::Vector3f(m_planes[NEAR])),
					Math::Utils::cross(Math::Vector3f(m_planes[LEFT]),   Math::Vector3f(m_planes[FAR])),
					Math::Utils::cross(Math::Vector3f(m_planes[RIGHT]),  Math::Vector3f(m_planes[BOTTOM])),
					Math::Utils::cross(Math::Vector3f(m_planes[RIGHT]),  Math::Vector3f(m_planes[TOP])),
					Math::Utils::cross(Math::Vector3f(m_planes[RIGHT]),  Math::Vector3f(m_planes[NEAR])),
					Math::Utils::cross(Math::Vector3f(m_planes[RIGHT]),  Math::Vector3f(m_planes[FAR])),
					Math::Utils::cross(Math::Vector3f(m_planes[BOTTOM]), Math::Vector3f(m_planes[TOP])),
					Math::Utils::cross(Math::Vector3f(m_planes[BOTTOM]), Math::Vector3f(m_planes[NEAR])),
					Math::Utils::cross(Math::Vector3f(m_planes[BOTTOM]), Math::Vector3f(m_planes[FAR])),
					Math::Utils::cross(Math::Vector3f(m_planes[TOP]),    Math::Vector3f(m_planes[NEAR])),
					Math::Utils::cross(Math::Vector3f(m_planes[TOP]),    Math::Vector3f(m_planes[FAR])),
					Math::Utils::cross(Math::Vector3f(m_planes[NEAR]),   Math::Vector3f(m_planes[FAR]))
			};

			m_points[0] = Intersection<LEFT, BOTTOM, NEAR>(crosses);
			m_points[1] = Intersection<LEFT, TOP, NEAR>(crosses);
			m_points[2] = Intersection<RIGHT, BOTTOM, NEAR>(crosses);
			m_points[3] = Intersection<RIGHT, TOP, NEAR>(crosses);
			m_points[4] = Intersection<LEFT, BOTTOM, FAR>(crosses);
			m_points[5] = Intersection<LEFT, TOP, FAR>(crosses);
			m_points[6] = Intersection<RIGHT, BOTTOM, FAR>(crosses);
			m_points[7] = Intersection<RIGHT, TOP, FAR>(crosses);

		}

		// http://iquilezles.org/www/articles/frustumcorrect/frustumcorrect.htm
		[[nodiscard]] bool IsBoxVisible(const Math::Vector3f& minp, const Math::Vector3f& maxp) const
		{ // http://iquilezles.org/www/articles/frustumcorrect/frustumcorrect.htm
			// check box outside/inside of frustum
			for (int i = 0; i < COUNT; i++)
			{
				if ((Math::Utils::dot(m_planes[i], Math::Vector4f(minp.x, minp.y, minp.z, 1.0f)) < 0.0) &&
					(Math::Utils::dot(m_planes[i], Math::Vector4f(maxp.x, minp.y, minp.z, 1.0f)) < 0.0) &&
					(Math::Utils::dot(m_planes[i], Math::Vector4f(minp.x, maxp.y, minp.z, 1.0f)) < 0.0) &&
					(Math::Utils::dot(m_planes[i], Math::Vector4f(maxp.x, maxp.y, minp.z, 1.0f)) < 0.0) &&
					(Math::Utils::dot(m_planes[i], Math::Vector4f(minp.x, minp.y, maxp.z, 1.0f)) < 0.0) &&
					(Math::Utils::dot(m_planes[i], Math::Vector4f(maxp.x, minp.y, maxp.z, 1.0f)) < 0.0) &&
					(Math::Utils::dot(m_planes[i], Math::Vector4f(minp.x, maxp.y, maxp.z, 1.0f)) < 0.0) &&
					(Math::Utils::dot(m_planes[i], Math::Vector4f(maxp.x, maxp.y, maxp.z, 1.0f)) < 0.0))
				{
					return false;
				}
			}

			// check frustum outside/inside box
			int out;
			out = 0;
			for (int i = 0; i < 8; i++) out += ((m_points[i].x > maxp.x) ? 1 : 0);
			if (out == 8) return false;
			out = 0;
			for (int i = 0; i < 8; i++) out += ((m_points[i].x < minp.x) ? 1 : 0);
			if (out == 8) return false;
			out = 0;
			for (int i = 0; i < 8; i++) out += ((m_points[i].y > maxp.y) ? 1 : 0);
			if (out == 8) return false;
			out = 0;
			for (int i = 0; i < 8; i++) out += ((m_points[i].y < minp.y) ? 1 : 0);
			if (out == 8) return false;
			out = 0;
			for (int i = 0; i < 8; i++) out += ((m_points[i].z > maxp.z) ? 1 : 0);
			if (out == 8) return false;
			out = 0;
			for (int i = 0; i < 8; i++) out += ((m_points[i].z < minp.z) ? 1 : 0);
			if (out == 8) return false;

			return true;
		}

		[[nodiscard]] bool IsBoxVisible(const Math::Vector4f& minp, const Math::Vector4f& maxp) const
		{
			return IsBoxVisible(Math::Vector3f(minp), Math::Vector3f(maxp));
		}

		[[nodiscard]] bool IsBoxVisible(const Range<Math::Vector3f>& range) const
		{
			return IsBoxVisible(range.min, range.max);
		}

	private:
		template<Planes i, Planes j>
		struct ij2k
		{
			enum
			{
				k = i * (9 - i) / 2 + j - 1
			};
		};

		template<Planes a, Planes b, Planes c>
		Math::Vector3f Intersection(const Math::Vector3f* crosses) const
		{
			float D = Math::Utils::dot(Math::Vector3f(m_planes[a]), crosses[ij2k<b, c>::k]);
			Math::Vector3f res = Math::Matrix3f(crosses[ij2k<b, c>::k], -crosses[ij2k<a, c>::k], crosses[ij2k<a, b>::k]) *
					Math::Vector3f(m_planes[a].w, m_planes[b].w, m_planes[c].w);
			return res * (-1.0f / D);
		}

		Math::Vector4f m_planes[COUNT];
		Math::Vector3f m_points[8];
	};
}
