#pragma once

/**
 * \file Maths.h
 * \brief File containing mathematical functions and constants
 */

#include "blitz/maths/Vector.h"
#include <cmath>

namespace blitz {
namespace maths {

static constexpr float PI = 3.141592653f;

/**
 * \return the amount of overlap between the ranges [a1 ; b1] and [a2 ; b2]
 */
template <typename T>
T RangesOverlap(T& a1, T& b1, T& a2, T& b2) {
	return std::min(std::max(a1, b1), std::max(a2, b2)) - std::max(std::min(a1, b1), std::min(a2, b2));
}


/**
 * \return whether the ranges [a1 ; b1] and [a2 ; b2] overlap
 */
template <typename T>
bool RangesOverlapping(T& a1, T& b1, T& a2, T& b2) {
	return RangesOverlap(a1, a2, b1, b2) >= 0;
}

//////////////////////////////////////////////////////////////////
// 							Vectors								//
//////////////////////////////////////////////////////////////////

/**
 * \brief Returns the length of the vector
 * \return the length of the vector
 */
template <typename T>
T Length(const Vec3<T>& vect) {
	return std::sqrt(vect.x * vect.x + vect.y * vect.y + vect.z * vect.z);
}

/**
 * \brief Normalizes the vector
 * \return the normalized vector
 */
template <typename T>
Vec3<T> Normalize(const Vec3<T>& vect) {
	T length = Length(vect);

	return {vect.x / length, vect.y / length, vect.z / length};
}

/**
 * \brief Normalizes the vector
 * \return the normalized vector
 */
template <typename T>
Vec4<T> Normalize(const Vec4<T>& vect) {
	T length = std::sqrt(vect.x * vect.x + vect.y * vect.y + vect.z * vect.z + vect.w * vect.w);

	return {vect.x / length, vect.y / length, vect.z / length, vect.w / length};
}

/**
 * \brief Returns the dot product of the two vectors
 * \return the dot product of the two vectors
 */
template <typename T>
T Dot(const Vec3<T>& vect, const Vec3<T>& other) {
	return vect.x * other.x + vect.y * other.y + vect.z * other.z;
}

/**
 * \brief Returns the cross product of the two vectors
 * \return the cross product of the two vectors
 */
template <typename T>
Vec3<T> Cross(const Vec3<T>& vect, const Vec3<T>& other) {
	return {
		vect.y * other.z - vect.z * other.y,
		vect.z * other.x - vect.x * other.z,
		vect.x * other.y - vect.y * other.x,
	};
}

/**
 * \brief Returns the dot product of the two vectors
 * \return the dot product of the two vectors
 */
template <typename T>
T Dot(const Vec4<T>& vect, const Vec4<T>& other) {
	return vect.x * other.x + vect.y * other.y + vect.z * other.z + vect.w * other.w;
}

/**
 * \brief Returns the distance between the two vectors
 * \return the distance between the two vectors
 */
template <typename T>
T Distance(const Vec3<T>& vect, const Vec3<T>& other) {
	return Length(vect - other);
}

/**
 * \brief Returns the minimum of the three coordinates of the vector
 * \return the minimum between the three coordinates of the vector
 */
template <typename T>
T ReduceMin(const Vec3<T>& vect) {
	return std::min(std::min(vect.x, vect.y), vect.z);
}

/**
 * \brief Returns the maximum of the three coordinates of the vector
 * \return the maximum between the three coordinates of the vector
 */
template <typename T>
T ReduceMax(const Vec3<T>& vect) {
	return std::max(std::max(vect.x, vect.y), vect.z);
}

/**
 * \brief Returns the (signed) minimal coordinate of the vector
 * \param v a vector
 * \return the (signed) minimal coordinate of the vector
 */
template <>
inline float ReduceMin<float>(const Vec3f& v) {
	return std::fminf(std::fminf(v.x, v.y), v.z);
}

/**
 * \brief Returns the (signed) maximal coordinate of the vector
 * \param v a vector
 * \return the (signed) maximal coordinate of the vector
 */
template <>
inline float ReduceMax<float>(const Vec3f& v) {
	return std::fmaxf(std::fmaxf(v.x, v.y), v.z);
}

/**
 * \brief returns the (signed) minimal coordinate of the vector
 * \param v a vector
 * \return the (signed) minimal coordinate of the vector
 */
template <>
inline double ReduceMin<double>(const Vec3d& v) {
	return std::fmin(std::fmin(v.x, v.y), v.z);
}

/**
 * \brief returns the (signed) maximal coordinate of the vector
 * \param v a vector
 * \return the (signed) maximal coordinate of the vector
 */
template <>
inline double ReduceMax<double>(const Vec3d& v) {
	return std::fmax(std::fmax(v.x, v.y), v.z);
}

/**
 * \brief returns the coordinate-wise minimum of the given vectors, where a coordinate in the returned vector is NAN iff any of the two
 * compared ones are NAN
 * \tparam T
 * \param self a vector
 * \param other an other vector
 * \return the coordinate-wise minimum of the given vectors, where a coordinate in the returned vector is NAN iff any of the two
 * compared ones are NAN
 */
template <typename T>
constexpr Vec3<T> Min(const Vec3<T>& self, const Vec3<T>& other) {
	return {
		std::min(self.x, other.x),
		std::min(self.y, other.y),
		std::min(self.z, other.z),
	};
}

/**
 * \brief returns the coordinate-wise maximum of the given vectors,
 * where a coordinate in the returned vector is NAN iff any of the two compared ones are NAN
 * \tparam T
 * \param self a vector
 * \param other an other vector
 * \return returns the coordinate-wise maximum of the given vectors,
 * where a coordinate in the returned vector is NAN iff any of the two compared ones are NAN
 */
template <typename T>
constexpr Vec3<T> Max(const Vec3<T>& self, const Vec3<T>& other) {
	return {
		std::max(self.x, other.x),
		std::max(self.y, other.y),
		std::max(self.z, other.z),
	};
}

//////////////////////////////////////////////////////////////////
// 						    Matricies							//
//////////////////////////////////////////////////////////////////

/**
 * \brief Returns the dot product of the matrix and the vector
 * \return the dot product of the matrix and the vector
 */
template <typename T>
Vec4<T> Dot(const Mat4<T>& mat, const Vec4<T>& vect) {
	return {mat.x0 * vect.x + mat.x1 * vect.y + mat.x2 * vect.z + mat.x3 * vect.w,
		mat.y0 * vect.x + mat.y1 * vect.y + mat.y2 * vect.z + mat.y3 * vect.w,
		mat.z0 * vect.x + mat.z1 * vect.y + mat.z2 * vect.z + mat.z3 * vect.w,
		mat.w0 * vect.x + mat.w1 * vect.y + mat.w2 * vect.z + mat.w3 * vect.w};
}

/**
 * \brief Returns the dot product of the matrix and an other matrix
 * \return the dot product of the matrix and the other matrix
 */
template <typename T>
Mat4<T> Dot(const Mat4<T>& mat, const Mat4<T>& other) {
	Mat4<T> result{};

	for (std::size_t i = 0; i < Mat4<T>::MATRIX_SIZE; i++) {
		for (std::size_t j = 0; j < Mat4<T>::MATRIX_SIZE; j++) {
			for (std::size_t k = 0; k < Mat4<T>::MATRIX_SIZE; k++) {
				result.at(i, j) += mat.at(i, k) * other.at(k, j);
			}
		}
	}

	return result;
}

/**
 * \brief Returns the identity matrix
 * \return the identity matrix
 */
template <typename T>
Mat4<T> Identity() {
	Mat4<T> result{};

	result.x0 = static_cast<T>(1);
	result.y1 = static_cast<T>(1);
	result.z2 = static_cast<T>(1);
	result.w3 = static_cast<T>(1);

	return result;
}

/**
 * \brief Returns the transposed matrix
 * \return the transposed matrix
 */
template <typename T>
Mat4<T> Transpose(const Mat4<T>& mat) {
	Mat4<T> result;

	result.x1 = mat.y0;
	result.x2 = mat.z0;
	result.x3 = mat.w0;
	result.y0 = mat.x1;
	result.y2 = mat.z1;
	result.y3 = mat.w1;
	result.z0 = mat.x2;
	result.z1 = mat.y2;
	result.z3 = mat.w2;
	result.w0 = mat.x3;
	result.w1 = mat.y3;
	result.w2 = mat.z3;
	result.x0 = mat.x0;
	result.y1 = mat.y1;
	result.z2 = mat.z2;
	result.w3 = mat.w3;

	return result;
}

/**
 * \brief Returns the perspective matrix
 * \param fovY The field of view in the y direction
 * \param aspectRatio The aspect ratio of the screen
 * \param zNear The near clipping plane
 * \param zFar The far clipping plane
 * \return the perspective matrix
 */
Mat4f Perspective(float fovY, float aspectRatio, float zNear, float zFar);
/**
 * \brief Returns the look matrix
 * \param eyePos The position of the camera
 * \param front The front vector of the camera
 * \param up The up vector of the camera
 * \return the look matrix
 */
Mat4f Look(const Vec3f& eyePos, const Vec3f& front, const Vec3f& up);

/**
 * \brief Returns the inverse of the matrix
 * \return the inverse of the matrix
 */
Mat4f Inverse(const Mat4f& mat);

/**
 * \brief Returns the translation matrix
 * \param translation The translation vector
 * \return the translation matrix
 */
Mat4f Translate(const Vec3f& translation);

/**
 * \brief Returns the scale matrix
 * \param axisFactor The scaling factor for each axis
 * \return the scale matrix
 */
Mat4f Scale(const Vec3f& axisFactor);

/**
 * \brief Returns the rotation matrix around the x axis
 * \param angle The angle of rotation
 * \return the rotation matrix
 */
Mat4f RotateX(float angle);
/**
 * \brief Returns the rotation matrix around the y axis
 * \param angle The angle of rotation
 * \return the rotation matrix
 */
Mat4f RotateY(float angle);
/**
 * \brief Returns the rotation matrix around the z axis
 * \param angle The angle of rotation
 * \return the rotation matrix
 */
Mat4f RotateZ(float angle);

/**
 * \brief Returns the rotation matrix
 * \param angles The angles of rotation
 * \return the rotation matrix
 */
Mat4f Rotate(const Vec3f& angles);

/**
 * \brief Returns the rotation matrix
 * \param angle The angle of rotation
 * \param axis The axis of rotation
 * \return the rotation matrix
 */
Mat4f Rotate(float angle, Vec3f axis);

}  // namespace maths
}  // namespace blitz
