#include "pw/visual/shader.hpp"
#include "pw/core/debug.hpp"
#include "pw/core/serialize.hpp"

#include "glad/glad.h"

namespace pw {

struct shader::impl
{
	shader& _shader;

	GLuint _shader_program;
	std::vector<GLuint> _shader_stages;

	impl(shader& s)
		: _shader(s)
	{
	}

	~impl()
	{
		clear();
	}

	bool is_valid()
	{
		return glIsProgram(_shader_program);
	}

	bool build()
	{

		for (auto s : _shader._source)
		{
			GLuint shader_type = 0;
			switch (s.first) {
			case shader::code_type::vertex:
				shader_type = GL_VERTEX_SHADER;
				break;
			case shader::code_type::compute:
				shader_type = GL_COMPUTE_SHADER;
				break;
			case shader::code_type::geometry:
				shader_type = GL_GEOMETRY_SHADER;
				break;
			case shader::code_type::fragment:
				shader_type = GL_FRAGMENT_SHADER;
				break;
			}

			GLuint shaderId = glCreateShader(shader_type);

			char* src = const_cast<char*>(s.second.c_str());
			GLint size = static_cast<GLint>(s.second.length());

			glShaderSource(shaderId , 1, &src, &size);

			glCompileShader(shaderId);

			GLint is_compiled = GL_FALSE;
			glGetShaderiv(shaderId, GL_COMPILE_STATUS, &is_compiled);
			if(is_compiled == GL_FALSE)
			{

				GLint log_length;

				glGetShaderiv(shaderId, GL_INFO_LOG_LENGTH, &log_length);

				std::vector<char> log_buffer(static_cast<size_t>(log_length));

				glGetShaderInfoLog(shaderId, log_length, &log_length, log_buffer.data());

				// TODO - handle errors!
				debug::e() << log_buffer.data();

				return false;
			}

			_shader_stages.push_back(shaderId);

		}

		_shader_program = glCreateProgram();


		for (auto s : _shader_stages)
			glAttachShader(_shader_program,s);


        // TODO attribute binding ...

		/* Bind attribute index 0 (coordinates) to in_Position and attribute index 1 (color) to in_Color */
		/* Attribute locations must be setup before calling glLinkProgram. */
		//		 glBindAttribLocation(shaderprogram, 0, "in_Position");
		//		 glBindAttribLocation(shaderprogram, 1, "in_Color");

		glLinkProgram(_shader_program);

		GLint is_linked = 0;
		glGetProgramiv(_shader_program, GL_LINK_STATUS, &is_linked);
		if(is_linked == GL_FALSE)
		{

			GLint log_length;

			/* Noticed that glGetProgramiv is used to get the length for a shader program, not glGetShaderiv. */
			glGetProgramiv(_shader_program, GL_INFO_LOG_LENGTH, &log_length);

			/* The maxLength includes the NULL character */
			std::vector<char> info_log(static_cast<size_t>(log_length));

			/* Notice that glGetProgramInfoLog, not glGetShaderInfoLog. */
			glGetProgramInfoLog(_shader_program, log_length, &log_length, info_log.data());

			debug::e() << info_log.data();

			/* Handle the error in an appropriate way such as displaying a message or writing to a log file. */
			/* In this simple program, we'll just leave */

			return false;
		}

		return true;
	}


	void use()
	{
		glUseProgram(_shader_program);
	}

	void clear()
	{
		if (is_valid()) {
			glDeleteProgram(_shader_program);

			for (auto s : _shader_stages)
			{
				glDeleteShader(s);
			}
		}
	}

	int uniform_location(std::string const& name) const
	{
		return glGetUniformLocation(_shader_program,name.c_str());
	}

	void bind(int location,const matrix3x3f& m)
	{
		glUniformMatrix3fv(location,1,GL_FALSE,m.data);
	}

	void bind(int location,const matrix4x4f& m)
	{
		glUniformMatrix4fv(location,1,GL_FALSE,m.data);
	}

	void bind(int location,const vector4f& v)
	{
		glUniform4fv(location,1,v.data);
	}

	void bind(int location,const float& v)
	{
		glUniform1f(location,v);
	}
};



shader::shader()
{
    _impl = make_unique<impl>(*this);
}

shader::~shader()
{
}

bool shader::ready() const
{
	return _impl->is_valid();
}

shader &shader::set(int location, float v)
{
	_impl->bind(location,v); return *this;
}

shader &shader::set(int location, const vector4f &v)
{
	_impl->bind(location,v); return *this;
}

shader &shader::set(int location, const matrix4x4f &v)
{
    _impl->bind(location,v); return *this;
}

bool shader::build()
{
	return _impl->build();
}

void shader::use()
{
	_impl->use();
}

void shader::set_uniforms(uniform_cache_t c)
{
	for (auto& u : c) {
		// get name
		std::string name = std::get<0>(u);
		// get location
		GLint loc = std::get<2>(u);

		// if lower 0 check for location
		if (loc < 0) {
			loc = _impl->uniform_location(name);
			std::get<2>(u) = loc; // cache location
		}

		auto var = std::get<1>(u);

		std::visit([this,loc](auto&& arg) {

			using T = std::decay_t<decltype(arg)>;

			if constexpr ((std::is_same_v<T, vector4f>) ||
				(std::is_same_v<T, matrix4x4f>) ||
				(std::is_same_v<T, float>) ) {
					set(loc, std::forward<T>(arg));
			} else {
				std::cout << "can't" << std::endl;
			}
		}, var);
	}
}

unsigned int shader::native_handle() const
{
	return _impl->_shader_program;
}

int shader::uniform_location(const std::string &name) const
{
	return _impl->uniform_location(name);
}


}
