#pragma once

#include "common.h"
#include "buffer_object.h"

#include "util/geometry.h"

namespace gl
{
	class vao;

	class buffer_pointer
	{
	public:
		enum class type
		{
			s8 = GL_BYTE,
			u8 = GL_UNSIGNED_BYTE,
			s16 = GL_SHORT,
			u16 = GL_UNSIGNED_SHORT,
			s32 = GL_INT,
			u32 = GL_UNSIGNED_INT,
			f16 = GL_HALF_FLOAT,
			f32 = GL_FLOAT,
			f64 = GL_DOUBLE,
			fixed = GL_FIXED,
			s32_2_10_10_10_rev = GL_INT_2_10_10_10_REV,
			u32_2_10_10_10_rev = GL_UNSIGNED_INT_2_10_10_10_REV,
			u32_10f_11f_11f_rev = GL_UNSIGNED_INT_10F_11F_11F_REV
		};

	private:
		vao* m_vao;
		u32 m_offset;
		u32 m_stride;
		u32 m_size = 4;
		type m_type = type::f32;
		bool m_normalize = false;

	public:
		buffer_pointer(vao* vao, u32 offset = 0, u32 stride = 0)
			: m_vao(vao), m_offset(offset), m_stride(stride)
		{
		}

		const class ::gl::vao& get_vao() const
		{
			return *m_vao;
		}

		class ::gl::vao& get_vao()
		{
			return *m_vao;
		}

		buffer_pointer& offset(u32 value)
		{
			m_offset = value;
			return *this;
		}

		u32 offset() const
		{
			return m_offset;
		}

		buffer_pointer& stride(u32 value)
		{
			m_stride = value;
			return *this;
		}

		u32 stride() const
		{
			return m_stride;
		}

		buffer_pointer& size(u32 value)
		{
			m_size = value;
			return *this;
		}

		u32 size() const
		{
			return m_size;
		}

		buffer_pointer& set_type(type value)
		{
			m_type = value;
			return *this;
		}

		type get_type() const
		{
			return m_type;
		}

		buffer_pointer& normalize(bool value)
		{
			m_normalize = value;
			return *this;
		}

		bool normalize() const
		{
			return m_normalize;
		}

		buffer_pointer& operator>>(u32 value)
		{
			return stride(value);
		}

		buffer_pointer& config(type type_ = type::f32, u32 size_ = 4, bool normalize_ = false)
		{
			return set_type(type_).size(size_).normalize(normalize_);
		}
	};

	class attrib_t;

	class vao
	{
		template <buffer::target BindId, uint GetStateId>
		class entry
		{
			vao& m_parent;

		public:
			using save_binding_state = save_binding_state_base<entry, (static_cast<GLuint>(BindId)), GetStateId>;

			entry(vao* parent) noexcept : m_parent(*parent)
			{
			}

			entry& operator=(const buffer& buf) noexcept
			{
				m_parent.bind();
				buf.bind(BindId);

				return *this;
			}
		};

		GLuint m_id = GL_NONE;

	public:
		entry<buffer::target::pixel_pack, GL_PIXEL_PACK_BUFFER_BINDING> pixel_pack_buffer{this};
		entry<buffer::target::pixel_unpack, GL_PIXEL_UNPACK_BUFFER_BINDING> pixel_unpack_buffer{this};
		entry<buffer::target::array, GL_ARRAY_BUFFER_BINDING> array_buffer{this};
		entry<buffer::target::element_array, GL_ELEMENT_ARRAY_BUFFER_BINDING> element_array_buffer{this};

		vao() = default;
		vao(const vao&) = delete;

		vao(vao&& vao_) noexcept
		{
			swap(vao_);
		}
		vao(GLuint id) noexcept
		{
			set_id(id);
		}

		~vao() noexcept
		{
			if (created())
				remove();
		}

		void swap(vao& vao_) noexcept
		{
			auto my_old_id = id();
			set_id(vao_.id());
			vao_.set_id(my_old_id);
		}

		vao& operator=(const vao& rhs) = delete;
		vao& operator=(vao&& rhs) noexcept
		{
			swap(rhs);
			return *this;
		}

		void bind() const noexcept
		{
			glBindVertexArray(m_id);
		}

		void create() noexcept
		{
			glGenVertexArrays(1, &m_id);
		}

		void remove() noexcept
		{
			if (m_id != GL_NONE)
			{
				glDeleteVertexArrays(1, &m_id);
				m_id = GL_NONE;
			}
		}

		uint id() const noexcept
		{
			return m_id;
		}

		void set_id(uint id) noexcept
		{
			m_id = id;
		}

		bool created() const noexcept
		{
			return m_id != GL_NONE;
		}

		explicit operator bool() const noexcept
		{
			return created();
		}

		void enable_for_attributes(std::initializer_list<GLuint> indexes) noexcept
		{
			for (auto& index : indexes)
			{
				glEnableVertexAttribArray(index);
			}
		}

		void disable_for_attributes(std::initializer_list<GLuint> indexes) noexcept
		{
			for (auto& index : indexes)
			{
				glDisableVertexAttribArray(index);
			}
		}

		void enable_for_attribute(GLuint index) noexcept
		{
			enable_for_attributes({index});
		}

		void disable_for_attribute(GLuint index) noexcept
		{
			disable_for_attributes({index});
		}

		buffer_pointer operator+(u32 offset) noexcept
		{
			return {this, offset};
		}

		buffer_pointer operator>>(u32 stride) noexcept
		{
			return {this, {}, stride};
		}

		operator buffer_pointer() noexcept
		{
			return {this};
		}

		attrib_t operator[](u32 index) const noexcept;
	};

	class attrib_t
	{
		GLint m_location;

	public:
		attrib_t(GLint location)
			: m_location(location)
		{
		}

		GLint location() const
		{
			return m_location;
		}

		void operator=(float rhs) const
		{
			glDisableVertexAttribArray(location());
			glVertexAttrib1f(location(), rhs);
		}
		void operator=(double rhs) const
		{
			glDisableVertexAttribArray(location());
			glVertexAttrib1d(location(), rhs);
		}

		void operator=(const color1f& rhs) const
		{
			glDisableVertexAttribArray(location());
			glVertexAttrib1f(location(), rhs.r);
		}
		void operator=(const color1d& rhs) const
		{
			glDisableVertexAttribArray(location());
			glVertexAttrib1d(location(), rhs.r);
		}
		void operator=(const color2f& rhs) const
		{
			glDisableVertexAttribArray(location());
			glVertexAttrib2f(location(), rhs.r, rhs.g);
		}
		void operator=(const color2d& rhs) const
		{
			glDisableVertexAttribArray(location());
			glVertexAttrib2d(location(), rhs.r, rhs.g);
		}
		void operator=(const color3f& rhs) const
		{
			glDisableVertexAttribArray(location());
			glVertexAttrib3f(location(), rhs.r, rhs.g, rhs.b);
		}
		void operator=(const color3d& rhs) const
		{
			glDisableVertexAttribArray(location());
			glVertexAttrib3d(location(), rhs.r, rhs.g, rhs.b);
		}
		void operator=(const color4f& rhs) const
		{
			glDisableVertexAttribArray(location());
			glVertexAttrib4f(location(), rhs.r, rhs.g, rhs.b, rhs.a);
		}
		void operator=(const color4d& rhs) const
		{
			glDisableVertexAttribArray(location());
			glVertexAttrib4d(location(), rhs.r, rhs.g, rhs.b, rhs.a);
		}

		void operator=(buffer_pointer& pointer) const
		{
			pointer.get_vao().enable_for_attribute(m_location);
			glVertexAttribPointer(location(), pointer.size(), static_cast<GLenum>(pointer.get_type()), pointer.normalize(),
				pointer.stride(), reinterpret_cast<const void*>(u64{pointer.offset()}));
		}
	};
} // namespace gl
