#pragma once
#include <vector>
#include <exception>
#include <glm/glm.hpp>

namespace Nebulix
{
	struct VertexAttribute
	{
		GLenum AttributeType = GL_FLOAT;
		GLboolean Normalised = GL_FALSE;
		/// <summary>
		/// The number of elements for this attribute. 
		/// For example: 1 float value for each the x,y,z coordinate of the vertex which means this variable should be set to 3
		/// </summary>
		int NrOfElements = 3;
		int Stride;

		VertexAttribute(int nrOfElements = 3, GLenum attributeType = GL_FLOAT, GLboolean normalised = GL_FALSE)
			: AttributeType{ attributeType }, Normalised{ normalised }, NrOfElements{ nrOfElements }
		{
			if (attributeType == GL_INT)
				Stride = nrOfElements * sizeof(int);
			else if (attributeType == GL_FLOAT)
				Stride = nrOfElements * sizeof(float);
			else if (attributeType == GL_DOUBLE)
				Stride = nrOfElements * sizeof(double);
			else if (attributeType == GL_BOOL)
				Stride = nrOfElements * sizeof(bool);
			else if (attributeType == GL_BYTE)
				Stride = nrOfElements * sizeof(char);
			else
				throw std::exception("ERROR::CONSTRUCT::VERTEX_ATTRIBUTE::NOT_IMPLEMENTED");
		}
	};

	// TODO: move the triangle stuff to a seperate mesh class and use the mesh class here 
	// (in the final engine, this mesh class will be a component and a game object will just hold a list of different components)
	class GameObject
	{
	public:
		glm::vec3 Position;
		glm::vec3 Scale;

		GameObject(std::vector<float>& vertices, std::vector<VertexAttribute> vertexAttributes, 
			glm::vec3 position = glm::vec3(0.0f), glm::vec3 scale = glm::vec3(1.0f))
			: vertices{ vertices }, vertexAttributes{ vertexAttributes }, Position{position}, Scale{scale}
		{
			Init();
		}
		~GameObject()
		{
			glDeleteVertexArrays(1, &vertexArray);
			glDeleteBuffers(1, &vertexBuffer);
		}

		// NOTE: This method will not call "Shader.Use()" but will not set any properties.
		// This is because I do not know the names and values of the properties. Most likely this will be possible after learning about materials
		void Draw()
		{
			glBindVertexArray(vertexArray);

			// TODO: Use shader; Set shader matrices. Wait for materials

			//glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); // for EBOs
			glDrawArrays(GL_TRIANGLES, 0, nrOfVertices); // for VAOs

		}

		glm::mat4 GetModelMatrix()
		{
			glm::mat4 model = glm::mat4(1.0f);
			// TODO: add properties for rotation/scale and add these to the model matrix
			model = glm::translate(model, Position);
			model = glm::scale(model, Scale);
			return model;
		}

	private:
		std::vector<float> vertices;

		// VBO == all vertices (Vertex Buffer Object)
		// EBO == connection of vertices (faces) and is optional, although it is a bit more efficient than using VBOs, since it reuses vertices; (Element Buffer Object)
		// VAO == collection of VBOs/EBOs used for rendering (Vertex Array Object)
		GLuint vertexBuffer, vertexArray;
		std::vector<VertexAttribute> vertexAttributes;

		int nrOfVertices;

		void Init()
		{
			glGenVertexArrays(1, &vertexArray);
			glGenBuffers(1, &vertexBuffer);
			glBindVertexArray(vertexArray);

			glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer);
			glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(vertices[0]), vertices.data(), GL_STATIC_DRAW);

			int stride = 0;
			for (size_t i = 0; i < vertexAttributes.size(); i++)
			{
				stride += vertexAttributes[i].Stride;
			}

			int offset = 0;
			int nrNonVertices = 0;
			for (size_t i = 0; i < vertexAttributes.size(); i++)
			{
				VertexAttribute attr = vertexAttributes[i];
				glVertexAttribPointer(i, attr.NrOfElements, attr.AttributeType, attr.Normalised, stride, (void*)offset);
				glEnableVertexAttribArray(i);
				offset += attr.Stride;

				if (i != 0)
					nrNonVertices += attr.NrOfElements;
			}

			for (size_t i = 0; i < vertices.size(); i += 3)
			{
				nrOfVertices++;
				i += nrNonVertices;
			}
		}
	};
}
