#include "tutorials.h"

#include <glm/gtx/euler_angles.hpp>

/* create a window and initialize OpenGL context */
int tutorial_1( const int width, const int height )
{
	glfwSetErrorCallback( glfw_callback );

	if ( !glfwInit() )
	{
		return( EXIT_FAILURE );
	}

	glfwWindowHint( GLFW_CONTEXT_VERSION_MAJOR, 4 );
	glfwWindowHint( GLFW_CONTEXT_VERSION_MINOR, 6 );
	glfwWindowHint( GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE );
	glfwWindowHint( GLFW_SAMPLES, 8 );
	glfwWindowHint( GLFW_RESIZABLE, GL_TRUE );
	glfwWindowHint( GLFW_DOUBLEBUFFER, GL_TRUE );

	GLFWwindow * window = glfwCreateWindow( width, height, "PG2 OpenGL", nullptr, nullptr );
	if ( !window )
	{
		glfwTerminate();
		return EXIT_FAILURE;
	}

	glfwSetFramebufferSizeCallback( window, framebuffer_resize_callback );
	glfwMakeContextCurrent( window );

	if ( !gladLoadGLLoader( ( GLADloadproc )glfwGetProcAddress ) )
	{
		if ( !gladLoadGL() )
		{
			return EXIT_FAILURE;
		}
	}

	glEnable( GL_DEBUG_OUTPUT );
	glDebugMessageCallback( gl_callback, nullptr );

	printf( "OpenGL %s, ", glGetString( GL_VERSION ) );
	printf( "%s", glGetString( GL_RENDERER ) );
	printf( " (%s)\n", glGetString( GL_VENDOR ) );
	printf( "GLSL %s\n", glGetString( GL_SHADING_LANGUAGE_VERSION ) );

	glEnable( GL_MULTISAMPLE );

	// map from the range of NDC coordinates <-1.0, 1.0>^2 to <0, width> x <0, height>
	glViewport( 0, 0, width, height );
	// GL_LOWER_LEFT (OpenGL) or GL_UPPER_LEFT (DirectX, Windows) and GL_NEGATIVE_ONE_TO_ONE or GL_ZERO_TO_ONE
	glClipControl( GL_UPPER_LEFT, GL_NEGATIVE_ONE_TO_ONE );

	// setup vertex buffer as AoS (array of structures)
	GLfloat vertices[] =
	{
		-0.9f, 0.9f, 0.0f,  0.0f, 1.0f, // vertex 0 : p0.x, p0.y, p0.z, t0.u, t0.v
		0.9f, 0.9f, 0.0f,   1.0f, 1.0f, // vertex 1 : p1.x, p1.y, p1.z, t1.u, t1.v
		0.0f, -0.9f, 0.0f,  0.5f, 0.0f  // vertex 2 : p2.x, p2.y, p2.z, t2.u, t2.v
	};
	const int no_vertices = 3;
	const int vertex_stride = sizeof( vertices ) / no_vertices;
	// optional index array
	unsigned int indices[] =
	{
		0, 1, 2
	};

	GLuint vao = 0;
	glGenVertexArrays( 1, &vao );
	glBindVertexArray( vao );
	GLuint vbo = 0;
	glGenBuffers( 1, &vbo ); // generate vertex buffer object (one of OpenGL objects) and get the unique ID corresponding to that buffer
	glBindBuffer( GL_ARRAY_BUFFER, vbo ); // bind the newly created buffer to the GL_ARRAY_BUFFER target
	glBufferData( GL_ARRAY_BUFFER, sizeof( vertices ), vertices, GL_STATIC_DRAW ); // copies the previously defined vertex data into the buffer's memory
	// vertex position
	glVertexAttribPointer( 0, 3, GL_FLOAT, GL_FALSE, vertex_stride, 0 );
	glEnableVertexAttribArray( 0 );
	// vertex texture coordinates
	glVertexAttribPointer( 1, 2, GL_FLOAT, GL_FALSE, vertex_stride, ( void * )( sizeof( float ) * 3 ) );
	glEnableVertexAttribArray( 1 );
	GLuint ebo = 0; // optional buffer of indices
	glGenBuffers( 1, &ebo );
	glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, ebo );
	glBufferData( GL_ELEMENT_ARRAY_BUFFER, sizeof( indices ), indices, GL_STATIC_DRAW );

	GLuint vertex_shader = glCreateShader( GL_VERTEX_SHADER );
	std::vector<char> shader_source;
	if ( LoadShader( "basic_shader.vert", shader_source ) == S_OK )
	{
		const char * tmp = static_cast< const char * >( &shader_source[0] );
		glShaderSource( vertex_shader, 1, &tmp, nullptr );
		glCompileShader( vertex_shader );
	}
	CheckShader( vertex_shader );

	GLuint fragment_shader = glCreateShader( GL_FRAGMENT_SHADER );
	if ( LoadShader( "basic_shader.frag", shader_source ) == S_OK )
	{
		const char * tmp = static_cast< const char * >( &shader_source[0] );
		glShaderSource( fragment_shader, 1, &tmp, nullptr );
		glCompileShader( fragment_shader );
	}
	CheckShader( fragment_shader );

	GLuint shader_program = glCreateProgram();
	glAttachShader( shader_program, vertex_shader );
	glAttachShader( shader_program, fragment_shader );
	glLinkProgram( shader_program );
	// TODO check linking
	glUseProgram( shader_program );

	glPointSize( 10.0f );
	glLineWidth( 1.0f );
	glPolygonMode( GL_FRONT_AND_BACK, GL_FILL );

	// main loop
	while ( !glfwWindowShouldClose( window ) )
	{
		glClearColor( 0.2f, 0.3f, 0.3f, 1.0f ); // state setting function
		glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT ); // state using function

		GLint viewport[4];
		glGetIntegerv( GL_VIEWPORT, viewport );		
		glm::mat4 P = glm::mat4( 1.0f );
		//P.set( 0, 0, float( std::min( viewport[2], viewport[3] ) ) / viewport[2] );
		//P.set( 1, 1, float( std::min( viewport[2], viewport[3] ) ) / viewport[3] );		
		P[0][0] = 100 * 2.0f / viewport[2];		
		P[1][1] = 100 * 2.0f / viewport[3];
		SetMatrix4x4( shader_program, glm::value_ptr( P ), "P" );

		glBindVertexArray( vao );

		glDrawArrays( GL_POINTS, 0, 3 );
		glDrawArrays( GL_LINE_LOOP, 0, 3 );
		glDrawArrays( GL_TRIANGLES, 0, 3 );
		//glDrawElements( GL_TRIANGLES, 3, GL_UNSIGNED_INT, 0 ); // optional - render from an index buffer

		glfwSwapBuffers( window );
		glfwPollEvents();
	}

	glDeleteShader( vertex_shader );
	glDeleteShader( fragment_shader );
	glDeleteProgram( shader_program );

	glDeleteBuffers( 1, &vbo );
	glDeleteVertexArrays( 1, &vao );

	glfwTerminate();

	return EXIT_SUCCESS;
}

/* colors */
int tutorial_2()
{
	Color3f color = Color3f::black;

	return 0;
}

/* LDR textures */
int tutorial_3( const std::string & file_name )
{
	Texture texture = Texture3u( file_name ); // gamma compressed sRGB LDR image
	Color3u pixel1 = texture.pixel( texture.width() / 2, texture.height() / 2 );
	Color3u pixel2 = texture.texel( 0.5f, 0.5f );

	return 0;
}

/* load geometry and materials from OBJ and MTL files */
int tutorial_4( const std::string & file_name )
{
	MeshLoader mesh_loader;
	std::vector<std::shared_ptr<TriangularMesh>> meshes;

	mesh_loader.LoadTriangularMesh( file_name, meshes, glm::mat4( 1.0f ), glm::vec3( 0.0f ) ); // transformation matrix, pivot, material index offset
	mesh_loader.LoadTriangularMesh( "../../data/adjacent_triangles.obj", meshes, glm::mat4( 1.0f ), glm::vec3( 0.0f ) );

	for ( auto & mesh : meshes )
	{
		mesh->apply_transformation(); // apply the transformation to the actual vertices and reset the transformation to identity

		auto material = mesh->material();		

		const Color3f diff_coef = material->diffuse( glm::vec2( 0.5f, 0.5f ) ); // diffuse_coefficient = diffuse_color * diffuse_texture(u, v)
		printf( "%u, %s: diff_value = (%0.3f, %0.3f, %0.3f)\n",
			mesh->vertex_buffer()->mat_idx.x, // material index stored at each vertex
			mesh->material()->name().c_str(), // material name
			diff_coef.data[0], diff_coef.data[1], diff_coef.data[2] );

		const Vertex * vertices = mesh->vertex_buffer();
		const Triangle * indices = mesh->index_buffer();
	}	

	return 0;
}

namespace component
{
	struct Transform
	{
		glm::vec3 translation{ 0.0f };
		glm::vec3 rotation{ 0.0f };
		glm::vec3 scale{ 1.0f };

		glm::mat4 model_matrix{ 1.0f };

		void update_model_matrix()
		{
			model_matrix = glm::translate( glm::mat4( 1.0f ), translation )
				* glm::eulerAngleZYX( rotation.z, rotation.y, rotation.x )
				* glm::scale( glm::mat4( 1.0f ), scale );
		}
	};


	struct Mesh
	{
		GLuint vao{ 0 };
		GLuint vbo{ 0 };
		GLuint ebo{ 0 };
		int id{ -1 };
		static int counter;

		Mesh() // constructor with no parameters, e.g. Mesh a; calls this constructor
		{
			Init();
			counter++;
			id = counter;
			printf( "New mesh %d\n", id );
		}

		Mesh( const Mesh & mesh ) // explicit copy constructor, e.g. Mesh b = a; calls this constructor
		{
			vao = mesh.vao;
			vbo = mesh.vbo;
			ebo = mesh.ebo;
			counter++;
			id = counter;
			printf( "New mesh %d\n", id );
		}

		Mesh( Mesh && ) noexcept = default; // forces implicit move constructor (because a copy constructor, a copy assignment operator, a destructor (even if default but user - provided), or a move constructor / assignment operator prevent the implicit move constructor), e.g. Mesh a = make_mesh(); or Mesh b = std::move( a ); calls this constructor

		Mesh & operator=( const Mesh & ) = delete; // remove implicit copy assignment operator, e.g. b = a; calls this operator
		/*Mesh & operator=(const Mesh & a) // re-enable implicit copy assignment operator
		{
			printf( "Mesh buffers have been created.\n" );
			return *this;
		}*/

		Mesh & operator=( Mesh && ) = delete;
		/*Mesh & operator=(Mesh && a) // re-enable implicit move assignment operator, i.e. b = std::move( a ) calls this operator
		{
			printf( "Mesh buffers have been created.\n" );
			return *this;
		}*/

		void Init()
		{
			vao = 1;
			vbo = 2;
			ebo = 3;

			printf( "Mesh buffers have been created.\n" );
		}

		~Mesh()
		{
			vao = 0;
			vbo = 0;
			ebo = 0;

			printf( "Mesh buffers have been deallocated.\n" );
			printf( "Delete mesh %d\n", id );
		}
	};

	int Mesh::counter = 0;

	class Complex
	{
	public:
		void Init( const int a )
		{
			a_ = a;
		}

		int a() const
		{
			return a_;
		}

	private:
		int a_{ 0 };
	};
}

int tutorial_5()
{
	// see more examples at https://github.com/skypjack/entt?tab=readme-ov-file#code-example

	entt::registry registry;

	const auto entity1 = registry.create();
	registry.emplace<component::Transform>( entity1 );
	registry.emplace<component::Mesh>( entity1 );

	const auto entity2 = registry.create();
	registry.emplace<component::Transform>( entity2 );

	const auto entity3 = registry.create();
	registry.emplace<component::Mesh>( entity3 );
	registry.emplace<component::Complex>( entity3 );

	{
		auto view = registry.view<component::Transform, component::Mesh>();
		for ( auto [entity, transform, mesh] : view.each() )
		{
			transform.scale.x = 2.0f;
			transform.update_model_matrix();
			mesh.vao = 123;
			printf( "- %d, %0.3f\n", entity, transform.scale.x );
		}
	}

	{
		auto view = registry.view<component::Transform>();
		for ( auto [entity, transform] : view.each() )
		{
			transform.scale.x *= 2.0f;
			transform.update_model_matrix();
			printf( "- %d, %0.3f\n", entity, transform.scale.x );
		}
	}

	if ( registry.valid( entity1 ) )
	{
		auto & transform = registry.get<component::Transform>( entity1 );
		printf( "- %d, %0.3f\n", entity1, transform.scale.x );
	}

	if ( registry.valid( entity3 ) )
	{
		auto [complex, mesh] = registry.get<component::Complex, component::Mesh>( entity3 );
		printf( "- %d, %d\n", entity3, complex.a() );
	}

	component::Mesh a;
	component::Mesh b;
	component::Mesh c = a;
	//b = a;

	printf( "---\n" );
	auto entity4 = registry.create();
	registry.emplace<component::Transform>( entity4 );
	registry.emplace<component::Mesh>( entity4 );
	if ( registry.valid( entity4 ) )
	{
		printf( "---\n" );
		registry.erase<component::Mesh>( entity4 ); // removes only the mesh component from the entity
		registry.destroy( entity4 ); // removes the entity completely

		if ( !registry.valid( entity4 ) )
		{
			entity4 = entt::null;
		}
	}

	/*
	erase<T> = fast but unsafe (assumes component exists)
	remove<T> = safe but slightly slower (no crash if missing)
	destroy() = entity is gone
	*/

	registry.clear();

	return 0;
}