#pragma once

#include "common.h"
#include "sync.hpp"

#include "Emu/RSX/Program/GLSLTypes.h"
#include "util/geometry.h"
#include "util/mutex.h"

namespace gl
{
	namespace glsl
	{
		class shader
		{
			std::string source;
			::glsl::program_domain type;
			GLuint m_id = GL_NONE;

			fence m_compiled_fence;
			fence m_init_fence;

			shared_mutex m_compile_lock;
			atomic_t<bool> m_is_compiled{};

			void precompile();

		public:
			shader() = default;

			shader(::glsl::program_domain type_, const std::string& src)
			{
				create(type_, src);
			}

			~shader()
			{
				remove();
			}

			void remove()
			{
				if (m_id)
				{
					glDeleteShader(m_id);
					m_id = GL_NONE;
				}
			}

			void create(::glsl::program_domain type_, const std::string& src);

			shader& compile();

			uint id() const
			{
				return m_id;
			}

			const std::string& get_source() const
			{
				return source;
			}

			fence get_compile_fence_sync() const
			{
				return m_compiled_fence;
			}

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

			bool compiled() const
			{
				return m_is_compiled;
			}

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

		class program
		{
			GLuint m_id = GL_NONE;
			fence m_fence;

		public:
			class uniform_t
			{
				program& m_program;
				GLint m_location;

			public:
				uniform_t(program& program, GLint location)
					: m_program(program), m_location(location)
				{
				}

				GLint location() const
				{
					return m_location;
				}

				void operator=(int rhs) const
				{
					glProgramUniform1i(m_program.id(), location(), rhs);
				}
				void operator=(unsigned rhs) const
				{
					glProgramUniform1ui(m_program.id(), location(), rhs);
				}
				void operator=(float rhs) const
				{
					glProgramUniform1f(m_program.id(), location(), rhs);
				}
				void operator=(bool rhs) const
				{
					glProgramUniform1ui(m_program.id(), location(), rhs ? 1 : 0);
				}
				void operator=(const color1i& rhs) const
				{
					glProgramUniform1i(m_program.id(), location(), rhs.r);
				}
				void operator=(const color1f& rhs) const
				{
					glProgramUniform1f(m_program.id(), location(), rhs.r);
				}
				void operator=(const color2i& rhs) const
				{
					glProgramUniform2i(m_program.id(), location(), rhs.r, rhs.g);
				}
				void operator=(const color2f& rhs) const
				{
					glProgramUniform2f(m_program.id(), location(), rhs.r, rhs.g);
				}
				void operator=(const color3i& rhs) const
				{
					glProgramUniform3i(m_program.id(), location(), rhs.r, rhs.g, rhs.b);
				}
				void operator=(const color3f& rhs) const
				{
					glProgramUniform3f(m_program.id(), location(), rhs.r, rhs.g, rhs.b);
				}
				void operator=(const color4i& rhs) const
				{
					glProgramUniform4i(m_program.id(), location(), rhs.r, rhs.g, rhs.b, rhs.a);
				}
				void operator=(const color4f& rhs) const
				{
					glProgramUniform4f(m_program.id(), location(), rhs.r, rhs.g, rhs.b, rhs.a);
				}
				void operator=(const areaf& rhs) const
				{
					glProgramUniform4f(m_program.id(), location(), rhs.x1, rhs.y1, rhs.x2, rhs.y2);
				}
				void operator=(const areai& rhs) const
				{
					glProgramUniform4i(m_program.id(), location(), rhs.x1, rhs.y1, rhs.x2, rhs.y2);
				}
				void operator=(const std::vector<int>& rhs) const
				{
					glProgramUniform1iv(m_program.id(), location(), ::size32(rhs), rhs.data());
				}
			};

			class uniforms_t
			{
				program& m_program;
				std::unordered_map<std::string, GLint> locations;

			public:
				uniforms_t(program* program)
					: m_program(*program)
				{
				}

				void clear()
				{
					locations.clear();
				}

				bool has_location(const std::string& name, int* location = nullptr);

				GLint location(const std::string& name);

				uniform_t operator[](GLint location)
				{
					return {m_program, location};
				}

				uniform_t operator[](const std::string& name)
				{
					return {m_program, location(name)};
				}

			} uniforms{this};

		public:
			program() = default;
			program(const program&) = delete;

			~program()
			{
				if (created())
				{
					remove();
				}
			}

			program& recreate()
			{
				remove();
				return create();
			}

			program& create()
			{
				m_id = glCreateProgram();
				return *this;
			}

			void remove()
			{
				glDeleteProgram(m_id);
				m_id = GL_NONE;
				uniforms.clear();
			}

			void link(std::function<void(program*)> init_func = {});

			void validate();

			void sync()
			{
				if (!m_fence.check_signaled())
				{
					m_fence.server_wait_sync();
				}
			}

			program& attach(const shader& shader_)
			{
				glAttachShader(m_id, shader_.id());
				return *this;
			}

			program& bind_attribute_location(const std::string& name, int index)
			{
				glBindAttribLocation(m_id, index, name.c_str());
				return *this;
			}

			program& bind_fragment_data_location(const std::string& name, int color_number)
			{
				glBindFragDataLocation(m_id, color_number, name.c_str());
				return *this;
			}

			GLuint id() const
			{
				return m_id;
			}

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

			explicit operator bool() const
			{
				return created();
			}
		};
	} // namespace glsl
} // namespace gl
