#pragma once
#ifndef RTRMODELLOADING_H
#define RTRMODELLOADING_H

//////////////
// INCLUDES //
//////////////

// OpenGL
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <glm/glm.hpp> // GLM is a header only library for math in OpenGL
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtx/transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <glm/gtc/random.hpp>

//Asset Import library
#include <assimp/Importer.hpp>
#include <assimp/scene.h>
#include <assimp/postprocess.h>

// C++ Std Library
#include <iostream>
#include <cmath>
#include <string>
#include <vector>
#include <fstream>
#include <sstream>
#include <map>
#include <filesystem>


// own headers
#include "rtrShader.h"
#include "rtrTextureLoading.h"
#include "rtrMaterialsAndLighting.h"
#include "DrawableObject.h"

namespace rtrModelLoading
{
	/////////////
	// Classes //
	/////////////

	struct Vertex
	{
		glm::vec3 position;
		glm::vec3 normal;
		glm::vec2 texCoords;
		glm::vec3 tangent;
		glm::vec3 bitangent;
	};

	struct Texture
	{
		unsigned int id;
		unsigned int type;	// 0 = None, 1 = diffuse, 2 = specular 3 = normalMap
		unsigned int dimension; // 2 = 2d texture, 3 = cube texture (3d)
		std::string path;	// relative path within the model's folder
	};

	class Mesh
	{
	public:
		//member variables
		std::vector<Vertex> vertices;
		std::vector<unsigned int> indices;
		std::vector<Texture> textures;		// Must be leq than 3 at the moment, see shader class
		unsigned int VAO, VBO, EBO;			// Should be private but is now public because the instancing project:
		unsigned int numOfFeatures = 3;		// Num of features in Vertex Vector 1=only position 2=position + texture 3 = psition + Normal + TExture
		bool hasDiffuseTextures = true;
		bool hasSpecularTextures = true;
		bool hasNormalMapTextures = true;
		unsigned int shadowCubeMap = 0;
		unsigned int maxNumOfTextures = 1;

		// constructors
		Mesh(std::vector<Vertex> verticesInput, std::vector<unsigned int> indicesInput, std::vector<Texture> texturesInput);
		Mesh(float verticesInput[], const int numOfVertices, std::vector<Texture> texturesInput);
		Mesh(float verticesInput[], const int numOfVertices, std::string texturePath);
		Mesh(float verticesInput[], const int numOfVertices, const int numOfFeatures, std::vector<Texture> texturesInput);

		// methods
		void Draw(rtrShader::Shader& inputShader);
		void DrawDepth(rtrShader::Shader& inputShader);
		void drawVAOWithTextures(rtrShader::Shader& inputShader, const int numberOfvertices);
		void setupMesh(bool hasTangent = false, bool hasBitangent = false); // creates VAO_Body tha can be drawn
		void setupMeshfromVertexArray(float verticesInput[], const int numOfVertices); //creates VAO_Body without indices
	};

	void Mesh::setupMesh(bool hasTangent, bool hasBitangent)
	{
		// Vertex Array Object for the mesh and Buffers for Vertices & Indices 
		glGenVertexArrays(1, &VAO);
		glGenBuffers(1, &VBO);
		glGenBuffers(1, &EBO);

		// Bind VAO_Body and bind VBO / EBO as part of the VAO_Body
		glBindVertexArray(VAO);
		glBindBuffer(GL_ARRAY_BUFFER, VBO);
		glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
		// Write data into previously bound VBO and EBO that belong to the VAO_Body
		glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex), &vertices[0], GL_STATIC_DRAW);
		glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(unsigned int), &indices[0], GL_STATIC_DRAW);

		// Announce that the VBO has 5 different attributes: 0: Position, 1: Normal, 2: TexCoords 3:tangent 4:bitangent
		glEnableVertexAttribArray(0);
		glEnableVertexAttribArray(1);
		glEnableVertexAttribArray(2);
		if (hasTangent)
		{
			glEnableVertexAttribArray(3);
			if (hasBitangent)
			{
				glEnableVertexAttribArray(4);
			}
		}


		// Lay out the structure of each component:
		glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)0);
		glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, normal));
		glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, texCoords));
		if (hasTangent)
		{
			if (hasBitangent)
			{
				glVertexAttribPointer(4, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, tangent));
				glVertexAttribPointer(5, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, bitangent));
			}
		}

		// Unbind VAO_Body of this mesh and bind Default Vertex Array
		glBindVertexArray(0);
	};

	void Mesh::setupMeshfromVertexArray(float verticesInput[], const int numOfVertices)
	{
		// Vertex Array Object and Buffer for the veritces
		glGenVertexArrays(1, &VAO);
		glGenBuffers(1, &VBO);

		//Bind VAO_Body and VBO as part of it
		glBindVertexArray(VAO);
		glBindBuffer(GL_ARRAY_BUFFER, VBO);

		//Write Vertices into Vertex Buffer
		int numOfComponentsPerVertex = 8;
		switch (numOfFeatures)
		{
		case 1:	numOfComponentsPerVertex = 3; //pos
			break;
		case 2: numOfComponentsPerVertex = 5; // pos, texCoord
			break;
		case 3: numOfComponentsPerVertex = 8; // pos, texCoord, N
			break;
		case 4: numOfComponentsPerVertex = 11; // pos, texCoord, N, T
			break;
		case 5: numOfComponentsPerVertex = 14; // pos, texCoord, N, T ,B
			break;
		default: numOfComponentsPerVertex = 8; // pos, TExCoord, N
		}
		glBufferData(GL_ARRAY_BUFFER, sizeof(float) * numOfComponentsPerVertex * numOfVertices, verticesInput, GL_STATIC_DRAW);

		// Determine stride of vertex Array;
		unsigned int stride = sizeof(float) * numOfComponentsPerVertex;

		// Layout structure of each element
		glEnableVertexAttribArray(0);
		glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, stride, (void*)0);
		if (numOfFeatures == 2)
		{
			glEnableVertexAttribArray(1);
			glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, stride, (void*)(3 * sizeof(float)));
		}
		else if (numOfFeatures == 3)
		{
			glEnableVertexAttribArray(1);
			glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, stride, (void*)(3 * sizeof(float)));
			glEnableVertexAttribArray(2);
			glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, stride, (void*)(6 * sizeof(float)));
		}
		else if (numOfFeatures == 5) // pos, texCoord, N,T,B
		{
			// Announce that the VBO has 5 different attributes: 0: Position, 1: Normal, 2: TexCoords 3:tangent 4:bitangent
			glEnableVertexAttribArray(0);
			glEnableVertexAttribArray(1);
			glEnableVertexAttribArray(2);
			glEnableVertexAttribArray(3);
			glEnableVertexAttribArray(4);

			// Lay out the structure of each component:
			glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)0);
			glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, normal));
			glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, texCoords));
			glVertexAttribPointer(4, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, tangent));
			glVertexAttribPointer(5, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, bitangent));
		}

		// Unbind VAO_Body of this mesh and bind Default Vertex Array
		glBindVertexArray(0);

	};

	Mesh::Mesh(std::vector<Vertex> verticesInput, std::vector<unsigned int> indicesInput, std::vector<Texture> texturesInput) :
		vertices(verticesInput),
		indices(indicesInput),
		textures(texturesInput)
	{
		setupMesh();
	};

	Mesh::Mesh(float verticesInput[], const int numOfVertices, std::vector<Texture> texturesInput) :
		textures(texturesInput)
	{
		setupMeshfromVertexArray(verticesInput, numOfVertices);
	}

	Mesh::Mesh(float verticesInput[], const int numOfVertices, std::string texturePath) :
		// creates as vao and 1 texture that is used as diffuse texture from a provided imagefile path
		numOfFeatures(2)
	{
		unsigned int textureId = rtrTexture::loadTextureFromFile(texturePath.c_str());
		Texture texture = { textureId, 1,2, texturePath };
		textures = { texture };
		setupMeshfromVertexArray(verticesInput, numOfVertices);
	}

	Mesh::Mesh(float verticesInput[], const int numOfVertices, const int numOfFeaturesInput, std::vector<Texture> texturesInput) :
		textures(texturesInput),
		numOfFeatures(numOfFeaturesInput)
	{
		setupMeshfromVertexArray(verticesInput, numOfVertices);
	}

	void Mesh::Draw(rtrShader::Shader& inputShader)
	{
		//Requires a shader that has the correpsonding naming convention etc.
		//Assumes so far only work for a shader that has one diffuse, specular and one normalMap uniform 
		unsigned int diffuseMapIndex = 0;
		unsigned int specularMapIndex = 0;
		unsigned int normalMapIndex = 0;
		inputShader.use();
		for (unsigned int i = 0; i < textures.size(); i++)
		{
			glActiveTexture(GL_TEXTURE0 + i+1); // index shift: Use Texture unit 1,2,3... because unit 0 is for shadow depth cube map
			if (textures[i].type == 1 && diffuseMapIndex <= maxNumOfTextures-1 ) // diffuse texture map
			{
				inputShader.setInt("material.diffuseMap" + std::to_string(diffuseMapIndex++), i + 1);
			}
			else if (textures[i].type == 2 && specularMapIndex <= maxNumOfTextures-1) // specular map
			{
				inputShader.setInt("material.specularMap" + std::to_string(specularMapIndex++), i+1);
			}
			else if (textures[i].type == 3 && normalMapIndex <= maxNumOfTextures -1) // normal map
			{
				inputShader.setInt("material.normalMap" + std::to_string(normalMapIndex++), i+1);
			}
			glBindTexture(GL_TEXTURE_2D, textures[i].id);
		}
		if (diffuseMapIndex == 0) hasDiffuseTextures = false;
		if (specularMapIndex == 0) hasSpecularTextures = false;
		if (normalMapIndex == 0) hasNormalMapTextures = false;
		inputShader.setBool("material.hasDiffuseMap", hasDiffuseTextures);
		inputShader.setBool("material.hasSpecularMap", hasSpecularTextures);
		inputShader.setBool("material.hasNormalMap", hasNormalMapTextures);
		glActiveTexture(GL_TEXTURE0);
		glBindTexture(GL_TEXTURE_CUBE_MAP, shadowCubeMap);
		glBindVertexArray(VAO);
		glDrawElements(GL_TRIANGLES, (GLsizei)indices.size(), GL_UNSIGNED_INT, 0);
		glBindVertexArray(0);
	};

	void Mesh::DrawDepth(rtrShader::Shader& inputShader)
	{
		//Requires a shader that has the correpsonding naming convention etc.
		//Usually used for drawing shadow depth map
		inputShader.use();
		glBindVertexArray(VAO);
		glDrawElements(GL_TRIANGLES, (GLsizei)indices.size(), GL_UNSIGNED_INT, 0);
		glBindVertexArray(0);
	}


	void Mesh::drawVAOWithTextures(rtrShader::Shader& inputShader, const int numberOfVertices)
	{
		unsigned int diffuseMapIndex = 0;
		unsigned int specularMapIndex = 0;
		inputShader.use();
		for (unsigned int i = 0; i < textures.size(); i++) // for all textures in mesh
		{
			glActiveTexture(GL_TEXTURE0 + i);
			if (textures[i].type == 1)
			{
				inputShader.setInt("material.diffuseMap" + std::to_string(diffuseMapIndex++), i);
			}
			else if (textures[i].type == 2)
			{
				inputShader.setInt("material.specularMap" + std::to_string(specularMapIndex++), i);
			}
			glBindTexture(GL_TEXTURE_2D, textures[i].id);
		}
		glActiveTexture(GL_TEXTURE0);

		glBindVertexArray(VAO);
		glDrawArrays(GL_TRIANGLES, 0, numberOfVertices);
		glad_glBindVertexArray(0);
	}


	///////////
	// Model //
	///////////

	class Model
	{
	public:
		// Constructors
		Model(std::string path, unsigned int shadowCubeMapInput): shadowCubeMap(shadowCubeMapInput)
		{
			loadModelFromFile(path);
		};

		// Methods
		void Draw(rtrShader::Shader& inputShader);
		void DrawDepth(rtrShader::Shader& inputShader);

		//private: // Should this be private in the future?
			// Members
		std::vector<Mesh> meshes;
		std::string directory;
		std::vector<Texture> loadedTextures;
		bool hasTangent;
		bool hasBitangent;
		unsigned int shadowCubeMap = 0;

		// Methods 
		void loadModelFromFile(std::string path, bool hasTangent = true, bool hasBitangent = true);
		void processNode(aiNode* node, const aiScene* scene);
		Mesh processMesh(aiMesh* mesh, const aiScene* scene);
		std::vector<Texture> loadMaterialTextures(aiMaterial* materialAi, aiTextureType textureTypeAi, unsigned int textureType);
	};

	void Model::Draw(rtrShader::Shader& inputShader)
	{
		for (auto& it : meshes)
		{
			it.Draw(inputShader);
		}
	}
	void Model::DrawDepth(rtrShader::Shader& inputShader)
	{
		for (auto& it : meshes)
		{
			it.DrawDepth(inputShader);
		}
	}

	void Model::loadModelFromFile(std::string path, bool hasTangent, bool hasBitangent)
	{
		Assimp::Importer importer;
		const aiScene* scene = importer.ReadFile(path, aiProcess_Triangulate | aiProcess_FlipUVs | aiProcess_CalcTangentSpace);
		if (!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode)
		{
			std::cout << "ERROR::ASSIMP::" << importer.GetErrorString() << "\n";
			return;
		}
		if (path.find_last_of("\\") == std::string::npos)
		{
			directory = "";
		}
		else
		{
			directory = path.substr(0, path.find_last_of("\\")) + "\\";
		}
		processNode(scene->mRootNode, scene);
	}

	void Model::processNode(aiNode* node, const aiScene* scene)
	{
		//process all the node's meshes
		for (unsigned int i = 0; i < node->mNumMeshes; i++)
		{
			aiMesh* mesh = scene->mMeshes[node->mMeshes[i]];
			Mesh processedMesh = processMesh(mesh, scene);
			meshes.push_back(processedMesh);

		}

		//process all the nodes children 
		for (unsigned int i = 0; i < node->mNumChildren; i++)
		{
			processNode(node->mChildren[i], scene);
		}
	}

	Mesh Model::processMesh(aiMesh* inputMesh, const aiScene* scene)
	{
		//process vertices
		std::vector<Vertex> processedVertices;
		Vertex processedVertex;
		for (unsigned int i = 0; i < inputMesh->mNumVertices; i++)
		{
			//Convert Vertex Position
			processedVertex.position.x = inputMesh->mVertices[i].x;
			processedVertex.position.y = inputMesh->mVertices[i].y;
			processedVertex.position.z = inputMesh->mVertices[i].z;

			//Convert Vertex Normal
			processedVertex.normal.x = inputMesh->mNormals[i].x;
			processedVertex.normal.y = inputMesh->mNormals[i].y;
			processedVertex.normal.z = inputMesh->mNormals[i].z;

			if (hasTangent)
			{
				// Convert tangent 
				processedVertex.tangent.x = inputMesh->mTangents[i].x;
				processedVertex.tangent.y = inputMesh->mTangents[i].y;
				processedVertex.tangent.z = inputMesh->mTangents[i].z;

				if (hasBitangent)
				{
					// Convert bitangents
					processedVertex.bitangent.x = inputMesh->mBitangents[i].x;
					processedVertex.bitangent.y = inputMesh->mBitangents[i].y;
					processedVertex.bitangent.z = inputMesh->mBitangents[i].z;
				}
			}

			//Convert Vertex Tex Coords
			//AssImp contains up to 8 textures per vertex. We only want the first one if available
			if (inputMesh->mTextureCoords[0])
			{
				processedVertex.texCoords.x = inputMesh->mTextureCoords[0][i].x;
				processedVertex.texCoords.y = inputMesh->mTextureCoords[0][i].y;
			}
			else
			{
				processedVertex.texCoords = glm::vec2(0.0f, 0.0f);
			}
			processedVertices.push_back(processedVertex);
		}
		//process indices of vertices per face
		std::vector<unsigned int> processedIndices;
		for (unsigned int i = 0; i < inputMesh->mNumFaces; i++) //loop over faces
		{
			aiFace faceFromAi = inputMesh->mFaces[i];
			for (unsigned int j = 0; j < faceFromAi.mNumIndices; j++) // loop over vertex indices per face
			{
				processedIndices.push_back(faceFromAi.mIndices[j]);
			}
		}
		//process material textures
		std::vector<Texture> processedTextures;
		if (inputMesh->mMaterialIndex > 0)
		{
			aiMaterial* materialFromAi = scene->mMaterials[inputMesh->mMaterialIndex];
			std::vector<Texture> diffuseMaps = loadMaterialTextures(materialFromAi, aiTextureType_DIFFUSE, 1);
			processedTextures.insert(processedTextures.end(), diffuseMaps.begin(), diffuseMaps.end());
			std::vector<Texture> specularMaps = loadMaterialTextures(materialFromAi, aiTextureType_SPECULAR, 2);
			processedTextures.insert(processedTextures.end(), specularMaps.begin(), specularMaps.end());
			std::vector<Texture> normalMaps = loadMaterialTextures(materialFromAi, aiTextureType_HEIGHT, 3);
			processedTextures.insert(processedTextures.end(), specularMaps.begin(), specularMaps.end());
		}
		Mesh processedMesh(processedVertices, processedIndices, processedTextures);
		processedMesh.shadowCubeMap = this->shadowCubeMap;
		return processedMesh;
	}

	std::vector<Texture> Model::loadMaterialTextures(aiMaterial* materialAi, aiTextureType textureTypeAi, unsigned int textureType)
	{
		std::vector<Texture> processedTextures;
		for (unsigned int i = 0; i < materialAi->GetTextureCount(textureTypeAi); i++)
		{
			//write path of ith texture to string
			aiString texturePathAi;

			materialAi->GetTexture(textureTypeAi, i, &texturePathAi);
			std::string texturePath = std::string(texturePathAi.C_Str());
			// create texture object from retrieved path if not already loaded
			bool skip = false;
			for (unsigned int j = 0; j < loadedTextures.size(); j++)
			{
				if (std::strcmp(loadedTextures[j].path.c_str(), texturePath.c_str()) == 0)
				{
					processedTextures.push_back(loadedTextures[j]);
					skip = true;
					break;
				}
			}

			if (!skip)
			{

				std::string completePath = directory + texturePath;
				Texture processedTexture;
				processedTexture.id = rtrTexture::loadTextureFromFile(completePath.c_str());
				processedTexture.type = textureType;
				processedTexture.path = std::string(texturePath.c_str());
				processedTextures.push_back(processedTexture);
			}
		}
		return processedTextures;
	}


	/////////////////////
	// physical Object //
	/////////////////////

	class PhysicalObject : public DrawableObject
	{
	public:
		Model Model;
		rtrMaterialsAndLighting::Material Material;
		rtrShader::Shader Shader;
		glm::mat4 ModelMatrixBody;
		glm::mat4 NormalMatrix;
		glm::vec3 InitialPosition;
		glm::vec3 Position;
		glm::vec3 InitialOrientation;
		glm::vec3 Orientation;
		float Speed;
		glm::vec3 Velocity;
		glm::vec3 Acceleration;
		float ScaleFactor;

		PhysicalObject(std::string modelPath, std::string vertexShaderPath, std::string fragmentShaderPath, unsigned int shadowCubeMapInput):
			Model(rtrModelLoading::Model(modelPath, shadowCubeMapInput)),
			Shader(rtrShader::Shader(vertexShaderPath.c_str(), fragmentShaderPath.c_str())),
			ModelMatrixBody(glm::mat4(1)),
			NormalMatrix(glm::transpose(glm::inverse(ModelMatrixBody))),
			Position(glm::vec3(0, 0, 0)),
			Orientation(glm::vec3(1, 0, 0)),
			Speed(0),
			Velocity(glm::vec3(0, 0, 0)),
			Acceleration(glm::vec3(0, 0, 0)),
			ScaleFactor(1)
		{
			// Nothing happening here
		}
		void SetPosition(const glm::vec3& newPosition)
		{
			ModelMatrixBody = glm::translate(glm::mat4(1), newPosition);
		}

		void SetScale(const glm::vec3& newScale)
		{
			ModelMatrixBody = glm::scale(ModelMatrixBody, newScale);
		}

		void UpdateShader(const glm::mat4& viewMatrix, const glm::mat4& projectionMatrix, const glm::vec3& cameraPosition)
		{
			NormalMatrix = glm::transpose(glm::inverse(ModelMatrixBody));
			Shader.use();
			Shader.setVec3F("cameraPosition", cameraPosition);
			Shader.setMat4F("viewMatrix", viewMatrix);
			Shader.setMat4F("projectionMatrix", projectionMatrix);
			Shader.setMat4F("modelMatrix", ModelMatrixBody);
			Shader.setMat4F("normalMatrix", NormalMatrix);
			Shader.setVec3F("material.specularColor", Material.specularColor);
			Shader.setFloat("material.diffuseStrength", Material.diffuseStrength);
			Shader.setFloat("material.shininess", Material.shininess);
			Shader.setFloat("material.specularStrength", Material.specularStrength);
			Shader.setInt("material.specularStrength", Material.specularStrength);
		}

		void Draw()
		{
			Shader.use();
			Model.Draw(Shader);
		}

		void Draw(rtrShader::Shader& shaderInput)
		{
			shaderInput.use();
			Model.Draw(shaderInput);
		}

		void DrawDepth(rtrShader::Shader& shaderInput)
		{
			shaderInput.use();
			Model.DrawDepth(shaderInput);
		}



	};
}
#endif