#include "pw/core/size.hpp"
#include "pw/core/matrix.hpp"
#include "pw/core/mesh.hpp"
#include "pw/core/time.hpp"
#include "pw/core/axisangle.hpp"
#include "pw/core/serialize.hpp"
#include "pw/core/matrix_transform.hpp"

#include "pw/core/debug.hpp"
#include "pw/visual/pipeline.hpp"
#include "pw/visual/shader.hpp"
#include "pw/visual/vertex_array.hpp"

#include "glad/glad.h"

namespace pw {

class command {

};


class mesh_command : command {

	//	shader
	// vertexarray

};

class queue {

	//	vector<commands> ...
};

struct triangle_renderer
{
	//    GLuint vbo = 0;
	//    GLuint vao = 0;
	//    GLuint shader_programme = 0;

	shader shader_p;
	geometry amesh;
	vertex_array amesh_renderer;
	time::tick_t tick;

	triangle_renderer()
	{
	}

	void setup()
	{

		const float z_val = -5.f;
		const float s = 1.0f;


#if 0
		mesh::vertex3array_t vertices = {
			{ 0.0f,  0.5f,  z_val} // 0
			,{ 0.5f,  0.0f,  z_val} // 1
			,{-0.5f,  0.0f,  z_val} // 2
		};

		// actual indices
		mesh::indexarray_t indices = { 0, 1, 2};

#else


		//
		//		0 -- 1
		//		|  / |
		//		2 -- 3

		// geometry
		geometry::vertex3array_t vertices = {
			{-s,  s,  z_val} // 0
			,{ s,  s,  z_val} // 1
			,{-s, -s,  z_val} // 2
			,{ s, -s,  z_val} // 3
		};

		// topology / indices
		geometry::indexarray_t indices = { 2, 1, 0,
									   2, 3, 1};


#endif

		amesh.set_indices(indices);
		amesh.set_vertices(vertices);

		amesh.compute_normals();

		amesh_renderer.create(amesh);

		const char* vertex_shader_2 = R"(
									  #version 400
									  uniform mat4 model;
									  uniform mat4 view;
									  uniform mat4 projection;

									  in vec3 vertex_p;

									  void main() {
									  gl_Position = projection * view * model * vec4(vertex_p, 1.0);
									  //										gl_Position = model * vec4(vertex_p, 1.0);
									  }
									  )";

		const char *fragment_shader_2 = R"(
										#version 400
										uniform vec4 input_color = vec4(1.0, 0.0, 0.0, 1.0);
										out vec4 frag_colour;
										void main() {
										frag_colour = input_color;
										})";

		shader_p.set_source(vertex_shader_2,shader::code_type::vertex);
		shader_p.set_source(fragment_shader_2,shader::code_type::fragment);

		if (!shader_p.build())
			exit(-1);
	}

	void draw()
	{

		// input needed here -
		// model view projection

		shader_p.use();

		auto v_col = ping_pong(static_cast<float>(time::now()),1.0f);

		vector4f col({0.5f,1-v_col,v_col,1.0f});

		matrix4x4f model_mat; model_mat.set_identity();

		auto v_angle = ping_pong(static_cast<float>(time::now()),2 * ::pw::pi<float>());

		axisangle rot(vector3::forward(),v_angle);
		model_mat = rot.to_matrix();

		matrix4x4f view_mat = matrix_transform<float>::look_at(vector3({0,0,0}),
															   vector3::forward(),
															   vector3::up());

		// materials should carry this
#if 1
		glEnable(GL_CULL_FACE);
		glCullFace(GL_BACK);
		glFrontFace(GL_CCW);
#endif


		// now bind textures

#if 1
		auto proj_mat = matrix_transform<float>::orthographic_projection(1.3,1.0,
																		 0.2f,100.f);


#else
		auto proj_mat = matrix_transform<float>::perspective_projection(deg_to_rad(60.f),
																		1.3f,
																		0.2f,1000.f);
#endif


#if 0
		// highly inefficient - should be cached -
		shader_p.set("input_color",col);
		shader_p.set("model",model_mat);
		shader_p.set("view",view_mat);
		shader_p.set("projection",proj_mat);

#else
		// new version with ...
		shader::uniform_cache_t us;
		us.push_back(std::make_tuple("input_color",col,-1));
		us.push_back(std::make_tuple("model",model_mat,-1));
		us.push_back(std::make_tuple("view",view_mat,-1));
		us.push_back(std::make_tuple("projection",proj_mat,-1));

		shader_p.set_uniforms(us);

#endif

		amesh_renderer.draw();

        auto error = glGetError();
        if (error != GL_NO_ERROR){
            debug::e() << "GL error " << error;
        }

		//		debug::d() << 100 * (timer::now() - t0) << "ms";
	}
};


struct pipeline::impl {

	sizei _size;

	GLuint _fbo_draw;
	GLuint _fbo_msaa;

	GLuint _rbo_color;
	GLuint _rbo_depth;

	//testing
	triangle_renderer tr;

	bool create(sizei size);

	void draw();

	impl() = default;
	~impl() = default;

};

GLuint generate_multisample_texture(const sizei& s,int samples)
{
	GLuint texture;
	glGenTextures(1, &texture);

	glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, texture);
	glTexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, samples, GL_RGB8, s.width, s.height, GL_TRUE);
	glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, 0);

	return texture;
}

bool pipeline::impl::create(sizei size)
{

	_size = size;

	int max_msaa;

	// query actual maximum MSAA
	glGetIntegerv(GL_MAX_SAMPLES,&max_msaa);


	// create a 4x MSAA renderbuffer object for colorbuffer
	int msaa = std::min(max_msaa,4);


	debug::d() << "OpenGL multisampling: " << max_msaa << " choosen:" << msaa;

	glGenRenderbuffers(1, &_rbo_color);
	glBindRenderbuffer(GL_RENDERBUFFER, _rbo_color);
	glRenderbufferStorageMultisample(GL_RENDERBUFFER, msaa, GL_RGBA8, _size.width, _size.height);

	// create a 4x MSAA renderbuffer object for depthbuffer

	glGenRenderbuffers(1, &_rbo_depth);
	glBindRenderbuffer(GL_RENDERBUFFER, _rbo_depth);
	glRenderbufferStorageMultisample(GL_RENDERBUFFER, msaa, GL_DEPTH_COMPONENT, _size.width, _size.height);

	// create a 4x MSAA framebuffer object
	glGenFramebuffers(1, &_fbo_msaa);
	glBindFramebuffer(GL_FRAMEBUFFER, _fbo_msaa);

	// attach colorbuffer image to FBO
	glFramebufferRenderbuffer(GL_FRAMEBUFFER,       // 1. fbo target: GL_FRAMEBUFFER
							  GL_COLOR_ATTACHMENT0, // 2. color attachment point
							  GL_RENDERBUFFER,      // 3. rbo target: GL_RENDERBUFFER
							  _rbo_color);          // 4. rbo ID

	// attach depthbuffer image to FBO
	glFramebufferRenderbuffer(GL_FRAMEBUFFER,       // 1. fbo target: GL_FRAMEBUFFER
							  GL_DEPTH_ATTACHMENT,  // 2. depth attachment point
							  GL_RENDERBUFFER,      // 3. rbo target: GL_RENDERBUFFER
							  _rbo_depth);          // 4. rbo ID

	// check FBO status
	if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
		return false;

	// reset
	glBindFramebuffer(GL_FRAMEBUFFER, 0);


	tr.setup();


	return true;
}

void pipeline::impl::draw()
{

	glBindFramebuffer(GL_DRAW_FRAMEBUFFER, _fbo_msaa);

//        glClearColor(1.0,0,0,1);
//        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

	//	glViewport(0,0,800,600);

	//
	// draw pass
	//

	tr.draw();



	// reset



	// actuall blitting

#if 0

	/* We are going to blit into the window (default framebuffer)                     */
	glBindFramebuffer (GL_DRAW_FRAMEBUFFER, 0);
	glDrawBuffer      (GL_BACK);              /* Use backbuffer as color dst.         */

	/* Read from your FBO */
	glBindFramebuffer (GL_READ_FRAMEBUFFER, _fbo_draw );
	glReadBuffer      (GL_COLOR_ATTACHMENT0); /* Use Color Attachment 0 as color src. */

	/* Copy the color and depth buffer from your FBO to the default framebuffer       */
	glBlitFramebuffer (0,0, _size.width, _size.height,
					   0,0, _size.width, _size.height,
					   GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT,
					   GL_NEAREST);

#else


	glBindFramebuffer(GL_READ_FRAMEBUFFER, _fbo_msaa); // src FBO (multi-sample)
	glBindFramebuffer(GL_DRAW_FRAMEBUFFER, _fbo_draw);     // dst FBO (single-sample)

	glBlitFramebuffer(0, 0, _size.width, _size.height,             // src rect
					  0, 0, _size.width, _size.height,             // dst rect
					  GL_COLOR_BUFFER_BIT,             // buffer mask
					  GL_LINEAR);                      // scale filter

	glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
	glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);

	// debug::d() << _size.width << "x" << _size.height;

#endif


	GLenum err;
	while((err = glGetError()) != GL_NO_ERROR)
	{
		std::string error;

		switch(err) {
		case GL_INVALID_OPERATION:      error="INVALID_OPERATION";      break;
		case GL_INVALID_ENUM:           error="INVALID_ENUM";           break;
		case GL_INVALID_VALUE:          error="INVALID_VALUE";          break;
		case GL_OUT_OF_MEMORY:          error="OUT_OF_MEMORY";          break;
		case GL_INVALID_FRAMEBUFFER_OPERATION:  error="INVALID_FRAMEBUFFER_OPERATION";  break;
		}

		debug::e() << "OpenGL error:" << err << " " << error;
	}

}


//
//
//

pipeline::pipeline()
	: _impl(std::make_unique<pipeline::impl>())
{
}

pipeline::~pipeline()
{
	//
}

void pipeline::draw()
{
	_impl->draw();
}

bool pipeline::create(size s)
{
	return _impl->create(s.cast<int>());
}

}




//class pipeline;

//class pass
//{
//public:
//	virtual void apply(pipeline& p);
//};

//class pipeline
//{

//public:
//	void apply()
//	{
//		for (auto p : _passes) p.apply(*this);
//	}

//protected:
//	std::vector<pass> _passes;
//};







//pipeline >
//		n*pass
//		shadow_pass
//      lighting_pass
//		mesh_pass
//		compositor_pass

//compositor
// render to fbo > add a
// glBlitFramebuffer .. https://stackoverflow.com/questions/29254574/using-glblitframebuffer-to-display-a-texture
