#pragma once

#include <cmath>

namespace blitz {

static constexpr float PI = 3.141592653f;

template <typename T>
struct Vec2 {
	union {
		T x;
		T r;
	};

	union {
		T y;
		T g;
	};

	constexpr Vec2(T X = 0, T Y = 0) : x(X), y(Y) {}
};

template <typename T>
inline bool operator==(const Vec2<T>& vec2, const Vec2<T>& other) {
	return vec2.x == other.x && vec2.y == other.y;
}

template <typename T>
struct Vec3 {
	union {
		T x;
		T r;
	};

	union {
		T y;
		T g;
	};

	union {
		T z;
		T b;
	};

	constexpr Vec3(T X = 0, T Y = 0, T Z = 0) : x(X), y(Y), z(Z) {}
};

template <typename T>
inline bool operator==(const Vec3<T>& vec3, const Vec3<T>& other) {
	return vec3.x == other.x && vec3.y == other.y && vec3.z == other.z;
}

template <typename T>
struct Vec4 {
	union {
		T x;
		T r;
	};

	union {
		T y;
		T g;
	};

	union {
		T z;
		T b;
	};

	union {
		T w;
		T a;
	};

	constexpr Vec4(Vec3<T> vec, T W = 1) : x(vec.x), y(vec.y), z(vec.z), w(W) {}
	constexpr Vec4(T X = 0, T Y = 0, T Z = 0, T W = 0) : x(X), y(Y), z(Z), w(W) {}
};

template <typename T>
inline bool operator==(const Vec4<T>& vec4, const Vec4<T>& other) {
	return vec4.x == other.x && vec4.y == other.y && vec4.z == other.z && vec4.w = other.w;
}

using Vec2uc = Vec2<unsigned int>;
using Vec2i = Vec2<int>;
using Vec2u = Vec2<unsigned int>;
using Vec2f = Vec2<float>;
using Vec2d = Vec2<double>;

using Vec3uc = Vec3<unsigned int>;
using Vec3i = Vec3<int>;
using Vec3u = Vec3<unsigned int>;
using Vec3f = Vec3<float>;
using Vec3d = Vec3<double>;

using Vec4uc = Vec4<unsigned int>;
using Vec4i = Vec4<int>;
using Vec4u = Vec4<unsigned int>;
using Vec4f = Vec4<float>;
using Vec4d = Vec4<double>;

using Color = Vec3<unsigned char>;

template <typename T>
struct Mat4 {
	static const std::size_t MATRIX_SIZE = 4;

	T x0, x1, x2, x3;
	T y0, y1, y2, y3;
	T z0, z1, z2, z3;
	T w0, w1, w2, w3;

	T operator[](std::size_t offset) const {
		return reinterpret_cast<const T*>(this)[offset];
	}

	T& operator[](std::size_t offset) {
		return reinterpret_cast<T*>(this)[offset];
	}

	T* data() {
		return reinterpret_cast<T*>(this);
	}

	const T* data() const {
		return reinterpret_cast<const T*>(this);
	}

	T at(std::size_t row, std::size_t column) const {
		return operator[](row* MATRIX_SIZE + column);
	}

	T& at(std::size_t row, std::size_t column) {
		return operator[](row* MATRIX_SIZE + column);
	}
};

typedef Mat4<float> Mat4f;
typedef Mat4<int> Mat4i;
typedef Mat4<double> Mat4d;

template <typename T>
inline bool operator==(const Mat4<T>& mat, const Mat4<T>& other) {
	return mat.x0 == other.x0 && mat.y0 == other.y0 && mat.z0 == other.z0 && mat.w0 == other.w0 && mat.x1 == other.x1 &&
		   mat.y1 == other.y1 && mat.z1 == other.z1 && mat.w1 == other.w1 && mat.x2 == other.x2 && mat.y2 == other.y2 &&
		   mat.z2 == other.z2 && mat.w2 == other.w2 && mat.x3 == other.x3 && mat.y3 == other.y3 && mat.z3 == other.z3 &&
		   mat.w3 == other.w3;
}

//////////////////////////////////////////////////////////////////
// 						   Operators							//
//////////////////////////////////////////////////////////////////

template <typename T>
Vec2<T> operator+(const Vec2<T>& vect, const Vec2<T>& other) {
	return {vect.x + other.x, vect.y + other.y};
}

template <typename T>
Vec2<T> operator-(const Vec2<T>& vect) {
	return {-vect.x, -vect.y};
}

template <typename T>
Vec2<T> operator-(const Vec2<T>& vect, const Vec2<T>& other) {
	return vect + (-other);
}

template <typename T>
Vec3<T> operator-(const Vec3<T>& vect) {
	return {-vect.x, -vect.y, -vect.z};
}

template <typename T>
Vec3<T> operator+(const Vec3<T>& vect, const Vec3<T>& other) {
	return {vect.x + other.x, vect.y + other.y, vect.z + other.y};
}

template <typename T>
Vec3<T> operator-(const Vec3<T>& vect, const Vec3<T>& other) {
	return vect + (-other);
}

template <typename T>
Vec4<T> operator-(const Vec4<T>& vect) {
	return {-vect.x, -vect.y, -vect.z, -vect.w};
}

template <typename T>
Vec4<T> operator+(const Vec4<T>& vect, const Vec4<T>& other) {
	return {vect.x + other.x, vect.y + other.y, vect.z + other.y, vect.w + other.w};
}

template <typename T>
Vec4<T> operator-(const Vec4<T>& vect, const Vec4<T>& other) {
	return vect + (-other);
}

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

namespace maths {

template <typename T>
T Length(const Vec3<T>& vect) {
	return std::sqrt(vect.x * vect.x + vect.y * vect.y + vect.z * vect.z);
}

template <typename T>
Vec3<T> Normalize(const Vec3<T>& vect) {
	T length = Length(vect);

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

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};
}

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;
}

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,
	};
}

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;
}

template <typename T>
T Distance(const Vec3<T>& vect, const Vec3<T>& other) {
	return Length(vect - other);
}

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

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};
}

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;
}

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;
}

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;
}

Mat4f Perspective(float fovY, float aspectRatio, float zNear, float zFar);
Mat4f Look(const Vec3f& eye, const Vec3f& center, const Vec3f& up);

Mat4f Inverse(const Mat4f& mat);

}  // namespace maths
}  // namespace blitz
