#pragma once

#include <psemek/gfx/gl.hpp>

#include <psemek/math/vector.hpp>
#include <psemek/math/point.hpp>
#include <psemek/math/matrix.hpp>
#include <psemek/math/quaternion.hpp>

#include <vector>
#include <optional>
#include <array>
#include <cstdint>

namespace psemek::gfx
{

	struct attrib_description
	{
		GLuint index;
		GLint size;
		GLenum type;
		bool integer = false;
		GLboolean normalized;
		void const * pointer;
		GLuint divisor;
	};

	struct attribs_description
	{
		std::vector<attrib_description> attribs;
		GLuint index_count = 0;
		GLuint vertex_size = 0;
		GLuint instance_size = 0;

		attribs_description & operator += (attribs_description const & d);
	};

	inline attribs_description operator + (attribs_description d1, attribs_description const & d2)
	{
		for (auto a : d2.attribs)
		{
			a.index += d1.index_count;
			a.pointer = reinterpret_cast<char const *>(a.pointer) + (a.divisor == 0 ? d1.vertex_size : d1.instance_size);
			d1.attribs.push_back(a);
		}
		d1.index_count += d2.index_count;
		d1.vertex_size += d2.vertex_size;
		d1.instance_size += d2.instance_size;
		return d1;
	}

	inline attribs_description & attribs_description::operator += (attribs_description const & d)
	{
		(*this) = (*this) + d;
		return *this;
	}

	template <typename T>
	struct normalized
	{
		using type = T;
	};

	template <typename T>
	struct integer
	{
		using type = T;
	};

	// skips a single attribute index
	struct skip
	{};

	// skips bytes in the vertex
	template <std::size_t N>
	struct padding
	{
		static constexpr std::size_t size = N;
	};

	// instance attribute, for instanced meshes only
	template <typename T>
	struct instanced
	{
		using type = T;
	};

	template <std::size_t N>
	struct instanced<padding<N>>
	{
		using type = padding<N>;
		static constexpr std::size_t size = N;
	};

	template <typename ... Attribs>
	attribs_description make_attribs_description();

	namespace detail
	{

		template <typename Attrib>
		struct attrib_traits;

		template <>
		struct attrib_traits<std::uint8_t>
		{
			using attrib_type = std::uint8_t;

			static constexpr GLint size = 1;
			static constexpr GLenum type = gl::UNSIGNED_BYTE;
			static constexpr bool integer = false;
			static constexpr GLboolean normalized = gl::FALSE;
		};

		template <>
		struct attrib_traits<std::int8_t>
		{
			using attrib_type = std::int8_t;

			static constexpr GLint size = 1;
			static constexpr GLenum type = gl::BYTE;
			static constexpr bool integer = false;
			static constexpr GLboolean normalized = gl::FALSE;
		};

		template <>
		struct attrib_traits<std::uint16_t>
		{
			using attrib_type = std::uint16_t;

			static constexpr GLint size = 1;
			static constexpr GLenum type = gl::UNSIGNED_SHORT;
			static constexpr bool integer = false;
			static constexpr GLboolean normalized = gl::FALSE;
		};

		template <>
		struct attrib_traits<std::int16_t>
		{
			using attrib_type = std::int16_t;

			static constexpr GLint size = 1;
			static constexpr GLenum type = gl::SHORT;
			static constexpr bool integer = false;
			static constexpr GLboolean normalized = gl::FALSE;
		};

		template <>
		struct attrib_traits<std::uint32_t>
		{
			using attrib_type = std::uint32_t;

			static constexpr GLint size = 1;
			static constexpr GLenum type = gl::UNSIGNED_INT;
			static constexpr bool integer = false;
			static constexpr GLboolean normalized = gl::FALSE;
		};

		template <>
		struct attrib_traits<std::int32_t>
		{
			using attrib_type = std::int32_t;

			static constexpr GLint size = 1;
			static constexpr GLenum type = gl::INT;
			static constexpr bool integer = false;
			static constexpr GLboolean normalized = gl::FALSE;
		};

		template <>
		struct attrib_traits<float>
		{
			using attrib_type = float;

			static constexpr GLint size = 1;
			static constexpr GLenum type = gl::FLOAT;
			static constexpr bool integer = false;
			static constexpr GLboolean normalized = gl::FALSE;
		};

#ifndef PSEMEK_GLES
		template <>
		struct attrib_traits<double>
		{
			using attrib_type = double;

			static constexpr GLint size = 1;
			static constexpr GLenum type = gl::DOUBLE;
			static constexpr bool integer = false;
			static constexpr GLboolean normalized = gl::FALSE;
		};
#endif

		template <typename T, std::size_t N>
		struct attrib_traits<std::array<T, N>>
		{
			using attrib_type = std::array<T, N>;

			static constexpr GLint size = N;
			static constexpr GLenum type = attrib_traits<T>::type;
			static constexpr bool integer = attrib_traits<T>::integer;
			static constexpr GLboolean normalized = gl::FALSE;
		};

		template <typename T, std::size_t N>
		struct attrib_traits<math::vector<T, N>>
		{
			using attrib_type = math::vector<T, N>;

			static constexpr GLint size = N;
			static constexpr GLenum type = attrib_traits<T>::type;
			static constexpr bool integer = attrib_traits<T>::integer;
			static constexpr GLboolean normalized = gl::FALSE;
		};

		template <typename T, std::size_t N>
		struct attrib_traits<math::point<T, N>>
		{
			using attrib_type = math::point<T, N>;

			static constexpr GLint size = N;
			static constexpr GLenum type = attrib_traits<T>::type;
			static constexpr bool integer = attrib_traits<T>::integer;
			static constexpr GLboolean normalized = gl::FALSE;
		};

		template <typename T, std::size_t R, std::size_t C>
		struct attrib_traits<math::matrix<T, R, C>>
		{
			using attrib_type = math::matrix<T, R, C>;
		};

		template <typename T>
		struct attrib_traits<math::quaternion<T>>
		{
			using attrib_type = math::quaternion<T>;

			static constexpr GLint size = 4;
			static constexpr GLenum type = attrib_traits<T>::type;
			static constexpr bool integer = attrib_traits<T>::integer;
			static constexpr GLboolean normalized = gl::FALSE;
		};

		template <typename T>
		struct attrib_traits<normalized<T>>
		{
			using attrib_type = T;

			static constexpr GLint size = attrib_traits<T>::size;
			static constexpr GLenum type = attrib_traits<T>::type;
			static constexpr bool integer = false;
			static constexpr GLboolean normalized = gl::TRUE;
		};

		template <typename T>
		struct attrib_traits<integer<T>>
		{
			using attrib_type = T;

			static constexpr GLint size = attrib_traits<T>::size;
			static constexpr GLenum type = attrib_traits<T>::type;
			static constexpr bool integer = true;
			static constexpr GLboolean normalized = gl::FALSE;
		};

		template <typename T>
		struct attrib_traits<instanced<T>>
		{
			using attrib_type = typename attrib_traits<T>::attrib_type;

			static constexpr GLint size = attrib_traits<T>::size;
			static constexpr GLenum type = attrib_traits<T>::type;
			static constexpr bool integer = attrib_traits<T>::integer;
			static constexpr GLboolean normalized = attrib_traits<T>::normalized;
		};

		template <typename T>
		struct is_padding : std::false_type
		{};

		template <std::size_t N>
		struct is_padding<padding<N>> : std::true_type
		{};

		template <std::size_t N>
		struct is_padding<instanced<padding<N>>> : std::true_type
		{};

		template <typename T>
		struct is_instanced : std::false_type
		{};

		template <typename T>
		struct is_instanced<instanced<T>> : std::true_type
		{};

		template <typename T>
		struct remove_instanced
		{
			using type = T;
		};

		template <typename T>
		struct remove_instanced<instanced<T>>
		{
			using type = T;
		};

		template <typename T>
		struct is_matrix : std::false_type
		{};

		template <typename T, std::size_t R, std::size_t C>
		struct is_matrix<math::matrix<T, R, C>> : std::true_type
		{};

		template <typename Attr>
		std::size_t attr_size()
		{
			if constexpr (std::is_same_v<Attr, skip>)
			{
				return 0;
			}
			else if constexpr (is_padding<Attr>::value)
			{
				return Attr::size;
			}
			else
			{
				return sizeof(typename attrib_traits<Attr>::attrib_type);
			}
		}

		template <bool Instance, typename ... Attribs>
		struct make_attribs_description_impl;

		template <bool Instance>
		struct make_attribs_description_impl<Instance>
		{
			static void make(attribs_description &)
			{}

			static void make_impl(attribs_description & result, std::size_t index, std::size_t offset)
			{
				result.index_count = index;
				if constexpr (Instance)
				{
					result.instance_size = offset;
				}
				else
				{
					result.vertex_size = offset;
				}
			}
		};

		template <bool Instance, typename Attr1, typename ... Attribs>
		struct make_attribs_description_impl<Instance, Attr1, Attribs...>
		{
			static void make(attribs_description & result)
			{
				make_impl(result, 0, 0);
			}

			static void make_impl(attribs_description & result, std::size_t index, std::size_t offset)
			{
				if constexpr (std::is_same_v<Attr1, skip>)
				{
					make_attribs_description_impl<Instance, Attribs...>::make_impl(result, index + 1, offset);
				}
				else if constexpr (is_padding<Attr1>::value)
				{
					if constexpr (is_instanced<Attr1>::value == Instance)
						make_attribs_description_impl<Instance, Attribs...>::make_impl(result, index, offset + Attr1::size);
					else
						make_attribs_description_impl<Instance, Attribs...>::make_impl(result, index, offset);
				}
				else if constexpr (is_instanced<Attr1>::value == Instance)
				{
					using attr = typename remove_instanced<Attr1>::type;

					if constexpr (is_matrix<attr>::value)
					{
						using T = typename attr::scalar_type;
						using traits = attrib_traits<math::vector<T, attr::static_columns>>;

						for (std::size_t row = 0; row < attr::static_rows; ++row)
						{
							attrib_description attr;
							attr.index = index + row;
							attr.size = traits::size;
							attr.type = traits::type;
							attr.integer = traits::integer;
							attr.normalized = traits::normalized;
							attr.pointer = reinterpret_cast<char const *>(offset + row * attr::static_columns * sizeof(T));
							attr.divisor = Instance ? 1 : 0;

							result.attribs.push_back(attr);
						}

						make_attribs_description_impl<Instance, Attribs...>::make_impl(result, index + attr::static_rows, offset + attr_size<Attr1>());
					}
					else
					{

						using traits = attrib_traits<Attr1>;

						attrib_description attr;
						attr.index = index;
						attr.size = traits::size;
						attr.type = traits::type;
						attr.integer = traits::integer;
						attr.normalized = traits::normalized;
						attr.pointer = reinterpret_cast<char const *>(offset);
						attr.divisor = Instance ? 1 : 0;

						result.attribs.push_back(attr);

						make_attribs_description_impl<Instance, Attribs...>::make_impl(result, index + 1, offset + attr_size<Attr1>());
					}
				}
				else
				{
					using attr = typename remove_instanced<Attr1>::type;

					if constexpr (is_matrix<attr>::value)
					{
						make_attribs_description_impl<Instance, Attribs...>::make_impl(result, index + attr::static_rows, offset);
					}
					else
					{
						make_attribs_description_impl<Instance, Attribs...>::make_impl(result, index + 1, offset);
					}
				}
			}
		};

	}

	template <typename ... Attribs>
	attribs_description make_attribs_description()
	{
		attribs_description result;
		detail::make_attribs_description_impl<false, Attribs...>::make(result);
		detail::make_attribs_description_impl<true, Attribs...>::make(result);
		return result;
	}

	inline void setup(attribs_description const & attribs, bool instance)
	{
		for (auto const & a : attribs.attribs)
		{
			if (instance != (a.divisor != 0)) continue;
			gl::EnableVertexAttribArray(a.index);
			gl::VertexAttribDivisor(a.index, a.divisor);
			GLuint stride = (a.divisor == 0) ? attribs.vertex_size : attribs.instance_size;
			if (a.integer)
				gl::VertexAttribIPointer(a.index, a.size, a.type, stride, a.pointer);
			else
				gl::VertexAttribPointer(a.index, a.size, a.type, a.normalized, stride, a.pointer);
		}
	}

	inline std::int8_t to_signed_8bit(float x)
	{
		return static_cast<std::int8_t>(math::clamp((0.5f * x + 0.5f) * 255.f - 128.f, {-128.f, 127.f}));
	}

	inline std::uint8_t to_unsigned_8bit(float x)
	{
		return static_cast<std::uint8_t>(math::clamp(x * 255.f, {0.f, 255.f}));
	}

	inline std::int16_t to_signed_16bit(float x)
	{
		return static_cast<std::int16_t>(math::clamp((0.5f * x + 0.5f) * 65535.f - 32768.f, {-32768.f, 32767.f}));
	}

	inline std::uint16_t to_unsigned_16bit(float x)
	{
		return static_cast<std::uint16_t>(math::clamp(x * 65535.f, {0.f, 65535.f}));
	}

	template <std::size_t D>
	math::vector<std::int8_t, D> to_signed_8bit(math::vector<float, D> const & v)
	{
		math::vector<std::int8_t, D> result;
		for (std::size_t i = 0; i < D; ++i)
			result[i] = to_signed_8bit(v[i]);
		return result;
	}

	template <std::size_t D>
	math::vector<std::uint8_t, D> to_unsigned_8bit(math::vector<float, D> const & v)
	{
		math::vector<std::uint8_t, D> result;
		for (std::size_t i = 0; i < D; ++i)
			result[i] = to_unsigned_8bit(v[i]);
		return result;
	}

	template <std::size_t D>
	math::vector<std::int16_t, D> to_signed_16bit(math::vector<float, D> const & v)
	{
		math::vector<std::int16_t, D> result;
		for (std::size_t i = 0; i < D; ++i)
			result[i] = to_signed_16bit(v[i]);
		return result;
	}

	template <std::size_t D>
	math::vector<std::uint16_t, D> to_unsigned_16bit(math::vector<float, D> const & v)
	{
		math::vector<std::uint16_t, D> result;
		for (std::size_t i = 0; i < D; ++i)
			result[i] = to_unsigned_16bit(v[i]);
		return result;
	}

}
