#include <psemek/gfx/program.hpp>

#include <psemek/util/to_string.hpp>
#include <psemek/util/at_scope_exit.hpp>
#include <psemek/util/exception.hpp>

#include <memory>
#include <vector>
#include <sstream>

namespace psemek::gfx
{

	void program::uniform_proxy::operator = (bool b)
	{
		gl::Uniform1i(location_, b ? 1 : 0);
	}

	void program::uniform_proxy::operator = (int i)
	{
		gl::Uniform1i(location_, i);
	}

	void program::uniform_proxy::operator = (unsigned int u)
	{
		gl::Uniform1ui(location_, u);
	}

	void program::uniform_proxy::operator = (float f)
	{
		gl::Uniform1f(location_, f);
	}

	void program::uniform_proxy::operator = (math::vector<int, 1> const & v)
	{
		gl::Uniform1i(location_, v[0]);
	}

	void program::uniform_proxy::operator = (math::vector<int, 2> const & v)
	{
		gl::Uniform2i(location_, v[0], v[1]);
	}

	void program::uniform_proxy::operator = (math::vector<int, 3> const & v)
	{
		gl::Uniform3i(location_, v[0], v[1], v[2]);
	}

	void program::uniform_proxy::operator = (math::vector<int, 4> const & v)
	{
		gl::Uniform4i(location_, v[0], v[1], v[2], v[3]);
	}

	void program::uniform_proxy::operator = (math::vector<unsigned int, 1> const & v)
	{
		gl::Uniform1ui(location_, v[0]);
	}

	void program::uniform_proxy::operator = (math::vector<unsigned int, 2> const & v)
	{
		gl::Uniform2ui(location_, v[0], v[1]);
	}

	void program::uniform_proxy::operator = (math::vector<unsigned int, 3> const & v)
	{
		gl::Uniform3ui(location_, v[0], v[1], v[2]);
	}

	void program::uniform_proxy::operator = (math::vector<unsigned int, 4> const & v)
	{
		gl::Uniform4ui(location_, v[0], v[1], v[2], v[3]);
	}

	void program::uniform_proxy::operator = (math::vector<float, 1> const & v)
	{
		gl::Uniform1f(location_, v[0]);
	}

	void program::uniform_proxy::operator = (math::vector<float, 2> const & v)
	{
		gl::Uniform2f(location_, v[0], v[1]);
	}

	void program::uniform_proxy::operator = (math::vector<float, 3> const & v)
	{
		gl::Uniform3f(location_, v[0], v[1], v[2]);
	}

	void program::uniform_proxy::operator = (math::vector<float, 4> const & v)
	{
		gl::Uniform4f(location_, v[0], v[1], v[2], v[3]);
	}

	void program::uniform_proxy::operator = (math::point<int, 1> const & v)
	{
		gl::Uniform1i(location_, v[0]);
	}

	void program::uniform_proxy::operator = (math::point<int, 2> const & v)
	{
		gl::Uniform2i(location_, v[0], v[1]);
	}

	void program::uniform_proxy::operator = (math::point<int, 3> const & v)
	{
		gl::Uniform3i(location_, v[0], v[1], v[2]);
	}

	void program::uniform_proxy::operator = (math::point<int, 4> const & v)
	{
		gl::Uniform4i(location_, v[0], v[1], v[2], v[3]);
	}

	void program::uniform_proxy::operator = (math::point<unsigned int, 1> const & v)
	{
		gl::Uniform1ui(location_, v[0]);
	}

	void program::uniform_proxy::operator = (math::point<unsigned int, 2> const & v)
	{
		gl::Uniform2ui(location_, v[0], v[1]);
	}

	void program::uniform_proxy::operator = (math::point<unsigned int, 3> const & v)
	{
		gl::Uniform3ui(location_, v[0], v[1], v[2]);
	}

	void program::uniform_proxy::operator = (math::point<unsigned int, 4> const & v)
	{
		gl::Uniform4ui(location_, v[0], v[1], v[2], v[3]);
	}

	void program::uniform_proxy::operator = (math::point<float, 1> const & v)
	{
		gl::Uniform1f(location_, v[0]);
	}

	void program::uniform_proxy::operator = (math::point<float, 2> const & v)
	{
		gl::Uniform2f(location_, v[0], v[1]);
	}

	void program::uniform_proxy::operator = (math::point<float, 3> const & v)
	{
		gl::Uniform3f(location_, v[0], v[1], v[2]);
	}

	void program::uniform_proxy::operator = (math::point<float, 4> const & v)
	{
		gl::Uniform4f(location_, v[0], v[1], v[2], v[3]);
	}

	void program::uniform_proxy::operator = (math::matrix<float, 2, 2> const & m)
	{
		gl::UniformMatrix2fv(location_, 1, gl::TRUE, &m[0][0]);
	}

	void program::uniform_proxy::operator = (math::matrix<float, 2, 3> const & m)
	{
		gl::UniformMatrix3x2fv(location_, 1, gl::TRUE, &m[0][0]);
	}

	void program::uniform_proxy::operator = (math::matrix<float, 2, 4> const & m)
	{
		gl::UniformMatrix4x2fv(location_, 1, gl::TRUE, &m[0][0]);
	}

	void program::uniform_proxy::operator = (math::matrix<float, 3, 2> const & m)
	{
		gl::UniformMatrix2x3fv(location_, 1, gl::TRUE, &m[0][0]);
	}

	void program::uniform_proxy::operator = (math::matrix<float, 3, 3> const & m)
	{
		gl::UniformMatrix3fv(location_, 1, gl::TRUE, &m[0][0]);
	}

	void program::uniform_proxy::operator = (math::matrix<float, 3, 4> const & m)
	{
		gl::UniformMatrix4x3fv(location_, 1, gl::TRUE, &m[0][0]);
	}

	void program::uniform_proxy::operator = (math::matrix<float, 4, 2> const & m)
	{
		gl::UniformMatrix2x4fv(location_, 1, gl::TRUE, &m[0][0]);
	}

	void program::uniform_proxy::operator = (math::matrix<float, 4, 3> const & m)
	{
		gl::UniformMatrix3x4fv(location_, 1, gl::TRUE, &m[0][0]);
	}

	void program::uniform_proxy::operator = (math::matrix<float, 4, 4> const & m)
	{
		gl::UniformMatrix4fv(location_, 1, gl::TRUE, &m[0][0]);
	}

	void program::uniform_proxy::operator = (math::interval<int> const & i)
	{
		gl::Uniform2i(location_, i.min, i.max);
	}

	void program::uniform_proxy::operator = (math::interval<unsigned int> const & i)
	{
		gl::Uniform2ui(location_, i.min, i.max);
	}

	void program::uniform_proxy::operator = (math::interval<float> const & i)
	{
		gl::Uniform2f(location_, i.min, i.max);
	}

	void program::uniform_proxy::operator = (math::quaternion<float> const & q)
	{
		(*this) = q.coords;
	}

	static std::string annotated_source(std::string_view source)
	{
		std::ostringstream os;

		bool first = true;

		int line = 1;
		for (char c : source)
		{
			if (first)
			{
				os << line << ": ";
				first = false;
			}

			os.put(c);
			if (c == '\n')
			{
				++line;
				first = true;
			}
		}

		return os.str();
	}

	static void load_shader(GLuint shader, std::string_view source)
	{
		char const * vert_sources[1] { source.data() };
		GLint vert_sources_len[1] { static_cast<GLint>(source.size()) };
		gl::ShaderSource(shader, 1, vert_sources, vert_sources_len);
		gl::CompileShader(shader);

		GLint status;
		gl::GetShaderiv(shader, gl::COMPILE_STATUS, &status);
		if (status != gl::TRUE)
		{
			GLint log_len;
			gl::GetShaderiv(shader, gl::INFO_LOG_LENGTH, &log_len);
			std::unique_ptr<char[]> log(new char [log_len]);
			gl::GetShaderInfoLog(shader, log_len, nullptr, log.get());
			throw util::exception(util::to_string("Shader compilation failed: ", log.get(), "\nShader source: \n", annotated_source(source)));
		}
	}

	static void load_program(GLuint program, std::vector<GLuint> const & shaders)
	{
		for (auto s : shaders)
			gl::AttachShader(program, s);
		gl::LinkProgram(program);

		GLint status;
		gl::GetProgramiv(program, gl::LINK_STATUS, &status);
		if (status != gl::TRUE)
		{
			GLint log_len;
			gl::GetProgramiv(program, gl::INFO_LOG_LENGTH, &log_len);
			std::unique_ptr<char[]> log(new char [log_len]);
			gl::GetProgramInfoLog(program, log_len, nullptr, log.get());
			throw util::exception(util::to_string("Program link failed: ", log.get()));
		}

		for (auto s : shaders)
			gl::DetachShader(program, s);
	}

	static GLuint create_program(std::vector<std::pair<GLenum, std::string_view>> const & sources)
	{
		std::vector<GLuint> shaders;

		[[maybe_unused]] util::at_scope_exit delete_shaders([&shaders]{
			for (auto s : shaders)
				gl::DeleteShader(s);
		});

		for (auto const & p : sources)
		{
			GLuint sh = gl::CreateShader(p.first);
			shaders.push_back(sh);
			load_shader(sh, p.second);
		}

		GLuint program = gl::CreateProgram();

		try
		{
			load_program(program, shaders);
		}
		catch (...)
		{
			gl::DeleteProgram(program);
			throw;
		}

		return program;
	}

	program::program(std::string_view compute_source)
	{
		program_ = create_program({{gl::COMPUTE_SHADER, compute_source}});
	}

	program::program(std::string_view vertex_source, std::string_view fragment_source)
	{
		program_ = create_program({{gl::VERTEX_SHADER, vertex_source}, {gl::FRAGMENT_SHADER, fragment_source}});
	}

	program::program(std::string_view vertex_source, std::string_view geometry_source, std::string_view fragment_source)
	{
		program_ = create_program({{gl::VERTEX_SHADER, vertex_source}, {gl::GEOMETRY_SHADER, geometry_source}, {gl::FRAGMENT_SHADER, fragment_source}});
	}

	program::program(program && other)
		: program_(other.program_)
		, uniforms_(std::move(other.uniforms_))
	{
		other.program_ = 0;
	}

	program & program::operator = (program && other)
	{
		if (this == &other)
			return *this;

		reset();

		program_ = other.program_;
		uniforms_ = std::move(other.uniforms_);
		other.program_ = 0;

		return *this;
	}

	program::~program()
	{
		reset();
	}

	GLuint program::id() const
	{
		return program_;
	}

	void program::bind() const
	{
		gl::UseProgram(program_);
	}

	void program::reset()
	{
		gl::DeleteProgram(program_);
		program_ = 0;
	}

	GLint program::location(std::string_view name) const
	{
		auto it = uniforms_.find(name);
		if (it == uniforms_.end())
		{
			std::string name_str(name);
			auto location = gl::GetUniformLocation(program_, name_str.data());
			uniforms_[std::move(name_str)] = location;
			return location;
		}
		return it->second;
	}

	program::uniform_proxy program::operator[] (std::string_view name) const
	{
		return {location(name)};
	}

	program::uniform_proxy program::operator[] (GLint location) const
	{
		return {location};
	}

	GLuint program::uniform_block_index(std::string_view name) const
	{
		auto it = uniform_blocks_.find(name);
		if (it == uniform_blocks_.end())
		{
			std::string name_str(name);
			auto index = gl::GetUniformBlockIndex(program_, name.data());
			uniform_blocks_[std::move(name_str)] = index;
			return index;
		}
		return it->second;
	}

	void program::uniform_block_binding(std::string_view name, GLuint binding) const
	{
		uniform_block_binding(uniform_block_index(name), binding);
	}

	void program::uniform_block_binding(GLuint index, GLuint binding) const
	{
		if (index != gl::INVALID_INDEX)
			gl::UniformBlockBinding(program_, index, binding);
	}

}
