text: // Copyright (C) 2022 Jérôme "Lynix" Leclercq (lynix680@gmail.com)
  // This file is part of the "Nazara Engine - Utility module"
  // For conditions of distribution and use, see copyright notice in Config.hpp
  
  #include <Nazara/Utility/Formats/OBJLoader.hpp>
  #include <Nazara/Core/ErrorFlags.hpp>
  #include <Nazara/Utility/IndexMapper.hpp>
  #include <Nazara/Utility/MaterialData.hpp>
  #include <Nazara/Utility/Mesh.hpp>
  #include <Nazara/Utility/StaticMesh.hpp>
  #include <Nazara/Utility/VertexMapper.hpp>
  #include <Nazara/Utility/Formats/MTLParser.hpp>
  #include <Nazara/Utility/Formats/OBJParser.hpp>
  #include <limits>
  #include <memory>
  #include <unordered_map>
  #include <Nazara/Utility/Debug.hpp>
  
  ///TODO: N'avoir qu'un seul VertexBuffer communs à tous les meshes
  
  namespace Nz
  {
  	namespace
  	{
  		bool IsOBJSupported(const std::string_view& extension)
  		{
  			return (extension == "obj");
  		}
  
  		Ternary CheckOBJ(Stream& stream, const MeshParams& parameters)
  		{
  			NazaraUnused(stream);
  
  			bool skip;
  			if (parameters.custom.GetBooleanParameter("SkipBuiltinOBJLoader", &skip) && skip)
  				return Ternary::False;
  
  			OBJParser parser;
  			if (!parser.Check(stream))
  				return Ternary::False;
  
  			return Ternary::Unknown;
  		}
  
  		bool ParseMTL(Mesh& mesh, const std::filesystem::path& filePath, const std::string* materials, const OBJParser::Mesh* meshes, std::size_t meshCount)
  		{
  			File file(filePath);
  			if (!file.Open(OpenMode::ReadOnly | OpenMode::Text))
  			{
  				NazaraError("Failed to open MTL file (" + file.GetPath().generic_u8string() + ')');
  				return false;
  			}
  
  			MTLParser materialParser;
  			if (!materialParser.Parse(file))
  			{
  				NazaraError("MTL parser failed");
  				return false;
  			}
  
  			std::unordered_map<std::string, ParameterList> materialCache;
  			std::filesystem::path baseDir = file.GetDirectory();
  			for (std::size_t i = 0; i < meshCount; ++i)
  			{
  				const std::string& matName = materials[meshes[i].material];
  				const MTLParser::Material* mtlMat = materialParser.GetMaterial(matName);
  				if (!mtlMat)
  				{
  					NazaraWarning("MTL has no material \"" + matName + '"');
  					continue;
  				}
  
  				auto it = materialCache.find(matName);
  				if (it == materialCache.end())
  				{
  					ParameterList data;
  
  					float alphaValue = mtlMat->alpha;
  
  					Color ambientColor(mtlMat->ambient);
  					Color diffuseColor(mtlMat->diffuse);
  					Color specularColor(mtlMat->specular);
  					ambientColor.a = alphaValue;
  					diffuseColor.a = alphaValue;
  					specularColor.a = alphaValue;
  
  					data.SetParameter(MaterialData::AmbientColor, ambientColor);
  					data.SetParameter(MaterialData::DiffuseColor, diffuseColor);
  					data.SetParameter(MaterialData::Shininess, mtlMat->shininess);
  					data.SetParameter(MaterialData::SpecularColor, specularColor);
  
  					if (!mtlMat->alphaMap.empty())
  					{
  						std::filesystem::path fullPath = mtlMat->alphaMap;
  						if (!fullPath.is_absolute())
  							fullPath = baseDir / fullPath;
  
  						data.SetParameter(MaterialData::AlphaTexturePath, fullPath.generic_u8string());
  					}
  
  					if (!mtlMat->diffuseMap.empty())
  					{
  						std::filesystem::path fullPath = mtlMat->diffuseMap;
  						if (!fullPath.is_absolute())
  							fullPath = baseDir / fullPath;
  
  						data.SetParameter(MaterialData::DiffuseTexturePath, fullPath.generic_u8string());
  					}
  
  					if (!mtlMat->emissiveMap.empty())
  					{
  						std::filesystem::path fullPath = mtlMat->emissiveMap;
  						if (!fullPath.is_absolute())
  							fullPath = baseDir / fullPath;
  
  						data.SetParameter(MaterialData::EmissiveTexturePath, fullPath.generic_u8string());
  					}
  
  					if (!mtlMat->normalMap.empty())
  					{
  						std::filesystem::path fullPath = mtlMat->normalMap;
  						if (!fullPath.is_absolute())
  							fullPath = baseDir / fullPath;
  
  						data.SetParameter(MaterialData::NormalTexturePath, fullPath.generic_u8string());
  					}
  
  					if (!mtlMat->specularMap.empty())
  					{
  						std::filesystem::path fullPath = mtlMat->specularMap;
  						if (!fullPath.is_absolute())
  							fullPath = baseDir / fullPath;
  
  						data.SetParameter(MaterialData::SpecularTexturePath, fullPath.generic_u8string());
  					}
  
  					// If we either have an alpha value or an alpha map, let's configure the material for transparency
  					if (alphaValue != 255 || !mtlMat->alphaMap.empty())
  					{
  						// Some default settings
  						data.SetParameter(MaterialData::Blending, true);
  						data.SetParameter(MaterialData::DepthWrite, true);
  						data.SetParameter(MaterialData::BlendDstAlpha, static_cast<long long>(BlendFunc::Zero));
  						data.SetParameter(MaterialData::BlendDstColor, static_cast<long long>(BlendFunc::InvSrcAlpha));
  						data.SetParameter(MaterialData::BlendModeAlpha, static_cast<long long>(BlendEquation::Add));
  						data.SetParameter(MaterialData::BlendModeColor, static_cast<long long>(BlendEquation::Add));
  						data.SetParameter(MaterialData::BlendSrcAlpha, static_cast<long long>(BlendFunc::One));
  						data.SetParameter(MaterialData::BlendSrcColor, static_cast<long long>(BlendFunc::SrcAlpha));
  					}
  
  					it = materialCache.emplace(matName, std::move(data)).first;
  				}
  
  				mesh.SetMaterialData(meshes[i].material, it->second);
  			}
  
  			return true;
  		}
  
  		std::shared_ptr<Mesh> LoadOBJ(Stream& stream, const MeshParams& parameters)
  		{
  			long long reservedVertexCount;
  			if (!parameters.custom.GetIntegerParameter("NativeOBJLoader_VertexCount", &reservedVertexCount))
  				reservedVertexCount = 100;
  
  			OBJParser parser;
  			if (!parser.Parse(stream, reservedVertexCount))
  			{
  				NazaraError("OBJ parser failed");
  				return nullptr;
  			}
  
  			std::shared_ptr<Mesh> mesh = std::make_shared<Mesh>();
  			mesh->CreateStatic();
  
  			const std::string* materials = parser.GetMaterials();
  			const Vector4f* positions = parser.GetPositions();
  			const Vector3f* normals = parser.GetNormals();
  			const Vector3f* texCoords = parser.GetTexCoords();
  
  			const OBJParser::Mesh* meshes = parser.GetMeshes();
  			std::size_t meshCount = parser.GetMeshCount();
  
  			NazaraAssert(materials != nullptr && positions != nullptr && normals != nullptr &&
  			             texCoords != nullptr && meshes != nullptr && meshCount > 0,
  			             "Invalid OBJParser output");
  
  			// Triangulation temporary vector
  			std::vector<UInt32> faceIndices;
  			for (std::size_t i = 0; i < meshCount; ++i)
  			{
  				std::size_t faceCount = meshes[i].faces.size();
  				if (faceCount == 0)
  					continue;
  
  				std::vector<UInt32> indices;
  				indices.reserve(faceCount*3); // Pire cas si les faces sont des triangles
  
  				// Afin d'utiliser OBJParser::FaceVertex comme clé dans un unordered_map,
  				// nous devons fournir un foncteur de hash ainsi qu'un foncteur de comparaison
  
  				// Hash
  				struct FaceVertexHasher
  				{
  					std::size_t operator()(const OBJParser::FaceVertex& o) const
  					{
  						std::size_t seed = 0;
  						HashCombine(seed, o.normal);
  						HashCombine(seed, o.position);
  						HashCombine(seed, o.texCoord);
  
  						return seed;
  					}
  				};
  
  				// Comparaison
  				struct FaceVertexComparator
  				{
  					bool operator()(const OBJParser::FaceVertex& lhs, const OBJParser::FaceVertex& rhs) const
  					{
  						return lhs.normal   == rhs.normal   &&
  						       lhs.position == rhs.position &&
  						       lhs.texCoord == rhs.texCoord;
  					}
  				};
  
  				std::unordered_map<OBJParser::FaceVertex, unsigned int, FaceVertexHasher, FaceVertexComparator> vertices;
  				vertices.reserve(meshes[i].vertices.size());
  
  				UInt32 vertexCount = 0;
  				for (unsigned int j = 0; j < faceCount; ++j)
  				{
  					std::size_t faceVertexCount = meshes[i].faces[j].vertexCount;
  					faceIndices.resize(faceVertexCount);
  
  					for (std::size_t k = 0; k < faceVertexCount; ++k)
  					{
  						const OBJParser::FaceVertex& vertex = meshes[i].vertices[meshes[i].faces[j].firstVertex + k];
  
  						auto it = vertices.find(vertex);
  						if (it == vertices.end())
  							it = vertices.emplace(vertex, vertexCount++).first;
  
  						faceIndices[k] = it->second;
  					}
  
  					// Triangulation
  					for (std::size_t k = 1; k < faceVertexCount-1; ++k)
  					{
  						indices.push_back(faceIndices[0]);
  						indices.push_back(faceIndices[k]);
  						indices.push_back(faceIndices[k+1]);
  					}
  				}
  
  				// Création des buffers
  				bool largeIndices = (vertexCount > std::numeric_limits<UInt16>::max());
  
  				std::shared_ptr<IndexBuffer> indexBuffer = std::make_shared<IndexBuffer>((largeIndices) ? IndexType::U32 : IndexType::U16, indices.size(), parameters.indexBufferFlags, parameters.bufferFactory);
  				std::shared_ptr<VertexBuffer> vertexBuffer = std::make_shared<VertexBuffer>(parameters.vertexDeclaration, vertexCount, parameters.vertexBufferFlags, parameters.bufferFactory);
  
  				// Remplissage des indices
  				IndexMapper indexMapper(*indexBuffer);
  				for (std::size_t j = 0; j < indices.size(); ++j)
  					indexMapper.Set(j, indices[j]);
  
  				indexMapper.Unmap(); // Pour laisser les autres tâches affecter l'index buffer
  
  				if (parameters.optimizeIndexBuffers)
  					indexBuffer->Optimize();
  
  				// Remplissage des vertices
  
  				// Make sure the normal matrix won't rescale our normals
  				Nz::Matrix4f normalMatrix = parameters.matrix;
  				if (normalMatrix.HasScale())
  					normalMatrix.ApplyScale(1.f / normalMatrix.GetScale());
  
  				bool hasNormals = true;
  				bool hasTexCoords = true;
  
  				VertexMapper vertexMapper(*vertexBuffer);
  
  				auto normalPtr = vertexMapper.GetComponentPtr<Vector3f>(VertexComponent::Normal);
  				auto posPtr = vertexMapper.GetComponentPtr<Vector3f>(VertexComponent::Position);
  				auto uvPtr = vertexMapper.GetComponentPtr<Vector2f>(VertexComponent::TexCoord);
  
  				if (!normalPtr)
  					hasNormals = false;
  
  				if (!uvPtr)
  					hasTexCoords = false;
  
  				for (auto& vertexPair : vertices)
  				{
  					const OBJParser::FaceVertex& vertexIndices = vertexPair.first;
  					unsigned int index = vertexPair.second;
  
  					if (posPtr)
  					{
  						const Vector4f& vec = positions[vertexIndices.position - 1];
  						posPtr[index] = Vector3f(parameters.matrix * vec);
  					}
  
  					if (hasNormals)
  					{
  						if (vertexIndices.normal > 0)
  							normalPtr[index] = normalMatrix.Transform(normals[vertexIndices.normal - 1], 0.f);
  						else
  							hasNormals = false;
  					}
  
  					if (hasTexCoords)
  					{
  						if (vertexIndices.texCoord > 0)
  						{
  							Vector2f uv = Vector2f(texCoords[vertexIndices.texCoord - 1]);
  							uv.y = 1.f - uv.y; //< OBJ model texcoords seems to majority start from bottom left
  
  							uvPtr[index] = Vector2f(parameters.texCoordOffset + uv * parameters.texCoordScale);
  						}
  						else
  							hasTexCoords = false;
  					}
  				}
  
  				// Official .obj files have no vertex color, fill it with white
  				if (auto colorPtr = vertexMapper.GetComponentPtr<Color>(VertexComponent::Color))
  				{
  					for (UInt32 j = 0; j < vertexCount; ++j)
  						colorPtr[j] = Color::White;
  				}
  
  				vertexMapper.Unmap();
  
  				std::shared_ptr<StaticMesh> subMesh = std::make_shared<StaticMesh>(std::move(vertexBuffer), indexBuffer);
  				subMesh->GenerateAABB();
  				subMesh->SetMaterialIndex(meshes[i].material);
  
  				// Ce que nous pouvons générer dépend des données à disposition (par exemple les tangentes nécessitent des coordonnées de texture)
  				if (hasNormals && hasTexCoords)
  					subMesh->GenerateTangents();
  				else if (hasTexCoords)
  					subMesh->GenerateNormalsAndTangents();
  				else if (normalPtr)
  					subMesh->GenerateNormals();
  
  				mesh->AddSubMesh(meshes[i].name + '_' + materials[meshes[i].material], subMesh);
  			}
  			mesh->SetMaterialCount(parser.GetMaterialCount());
  
  			if (parameters.center)
  				mesh->Recenter();
  
  			// On charge les matériaux si demandé
  			std::filesystem::path mtlLib = parser.GetMtlLib();
  			if (!mtlLib.empty())
  			{
  				ErrorFlags flags(ErrorMode::ThrowExceptionDisabled);
  				ParseMTL(*mesh, stream.GetDirectory() / mtlLib, materials, meshes, meshCount);
  			}
  
  			return mesh;
  		}
  	}
  
  	namespace Loaders
  	{
  		MeshLoader::Entry GetMeshLoader_OBJ()
  		{
  			MeshLoader::Entry loader;
  			loader.extensionSupport = IsOBJSupported;
  			loader.streamChecker = CheckOBJ;
  			loader.streamLoader = LoadOBJ;
  
  			return loader;
  		}
  	}
  }
  