#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/interval.hpp>
#include <psemek/math/quaternion.hpp>
#include <psemek/util/hash_table.hpp>

#include <string_view>

namespace psemek::gfx
{

	struct program
	{
		program(std::string_view compute_source);
		program(std::string_view vertex_source, std::string_view fragment_source);
		program(std::string_view vertex_source, std::string_view geometry_source, std::string_view fragment_source);

		program(program && other);

		program & operator = (program && other);

		~program();

		static program null()
		{
			return program(nullptr);
		}

		GLuint id() const;

		void bind() const;

		void reset();

		GLint location(std::string_view name) const;

		struct uniform_proxy
		{
			uniform_proxy(GLint location)
				: location_(location)
			{}

			GLint location() const { return location_; }

			void operator = (bool b);
			void operator = (int i);
			void operator = (unsigned int i);
			void operator = (float f);

			void operator = (math::vector<int, 1> const & v);
			void operator = (math::vector<int, 2> const & v);
			void operator = (math::vector<int, 3> const & v);
			void operator = (math::vector<int, 4> const & v);

			void operator = (math::vector<unsigned int, 1> const & v);
			void operator = (math::vector<unsigned int, 2> const & v);
			void operator = (math::vector<unsigned int, 3> const & v);
			void operator = (math::vector<unsigned int, 4> const & v);

			void operator = (math::vector<float, 1> const & v);
			void operator = (math::vector<float, 2> const & v);
			void operator = (math::vector<float, 3> const & v);
			void operator = (math::vector<float, 4> const & v);

			template <typename T, std::size_t D>
			void operator = (math::vector<T, D> const & v);

			void operator = (math::point<int, 1> const & v);
			void operator = (math::point<int, 2> const & v);
			void operator = (math::point<int, 3> const & v);
			void operator = (math::point<int, 4> const & v);

			void operator = (math::point<unsigned int, 1> const & v);
			void operator = (math::point<unsigned int, 2> const & v);
			void operator = (math::point<unsigned int, 3> const & v);
			void operator = (math::point<unsigned int, 4> const & v);

			void operator = (math::point<float, 1> const & v);
			void operator = (math::point<float, 2> const & v);
			void operator = (math::point<float, 3> const & v);
			void operator = (math::point<float, 4> const & v);

			template <typename T, std::size_t D>
			void operator = (math::point<T, D> const & v);

			void operator = (math::matrix<float, 2, 2> const & m);
			void operator = (math::matrix<float, 2, 3> const & m);
			void operator = (math::matrix<float, 2, 4> const & m);
			void operator = (math::matrix<float, 3, 2> const & m);
			void operator = (math::matrix<float, 3, 3> const & m);
			void operator = (math::matrix<float, 3, 4> const & m);
			void operator = (math::matrix<float, 4, 2> const & m);
			void operator = (math::matrix<float, 4, 3> const & m);
			void operator = (math::matrix<float, 4, 4> const & m);

			void operator = (math::interval<int> const & i);
			void operator = (math::interval<unsigned int> const & i);
			void operator = (math::interval<float> const & i);

			template <typename T>
			void operator = (math::interval<T> const & i);

			void operator = (math::quaternion<float> const & q);

		private:
			GLint location_;
		};

		uniform_proxy operator[] (std::string_view name) const;
		uniform_proxy operator[] (GLint location) const;

		GLuint uniform_block_index(std::string_view name) const;
		void uniform_block_binding(std::string_view name, GLuint binding) const;
		void uniform_block_binding(GLuint index, GLuint binding) const;

	private:
		GLuint program_ = 0;
		mutable util::hash_map<std::string, GLint, util::any_hash, std::equal_to<>> uniforms_;
		mutable util::hash_map<std::string, GLuint, util::any_hash, std::equal_to<>> uniform_blocks_;

		program(std::nullptr_t)
		{}
	};

	template <typename T, std::size_t D>
	void program::uniform_proxy::operator = (math::vector<T, D> const & v)
	{
		if constexpr (std::is_floating_point_v<T>)
		{
			(*this) = math::cast<float>(v);
		}
		else if (std::is_integral_v<T>)
		{
			if constexpr (std::is_unsigned_v<T>)
			{
				(*this) = math::cast<unsigned int>(v);
			}
			else
			{
				(*this) = math::cast<int>(v);
			}
		}
		else
		{
			static_assert("Unknown uniform data type");
		}
	}

	template <typename T, std::size_t D>
	void program::uniform_proxy::operator = (math::point<T, D> const & p)
	{
		if constexpr (std::is_floating_point_v<T>)
		{
			(*this) = math::cast<float>(p);
		}
		else if (std::is_integral_v<T>)
		{
			if constexpr (std::is_unsigned_v<T>)
			{
				(*this) = math::cast<unsigned int>(p);
			}
			else
			{
				(*this) = math::cast<int>(p);
			}
		}
		else
		{
			static_assert("Unknown uniform data type");
		}
	}

	template <typename T>
	void program::uniform_proxy::operator = (math::interval<T> const & i)
	{
		if constexpr (std::is_floating_point_v<T>)
		{
			(*this) = math::cast<float>(i);
		}
		else if (std::is_integral_v<T>)
		{
			if constexpr (std::is_unsigned_v<T>)
			{
				(*this) = math::cast<unsigned int>(i);
			}
			else
			{
				(*this) = math::cast<int>(i);
			}
		}
		else
		{
			static_assert("Unknown uniform data type");
		}
	}

}
