/*
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
 */

#include "MeshWriter.hpp"
#include "Base/Utils.hpp"
#include "IO/MemMappedFile.hpp"
#include "Scene/Geometry.hpp"
#include "Scene/Vertex.hpp"
#include "Scene/Export/UsdEncoder.hpp"
#include "ObjEncoder.hpp"
#include "IO/Archive/ArchiveWriter.hpp"
#include "IO/Archive/ZipWriter.hpp"

#include <fstream>
#include <fmt/core.h>

#if __has_include("assimp/Exporter.hpp")
#include <assimp/scene.h>
#include <assimp/Exporter.hpp>
#include <assimp/mesh.h>
#include <assimp/material.h>
#endif

namespace OpenVulkano::Scene
{
	void MeshWriter::WriteAsOBJ(Geometry* geometry, const std::string& filePath)
	{
		std::ofstream file(filePath);

		if (!file.is_open()) [[unlikely]]
			throw std::runtime_error("Failed to open file '" + filePath + "' for writing!");
		
		WriteObjContents(geometry, "", file);
		file.close();
	}

	void MeshWriter::WriteAsUSD(Geometry* geometry, const std::string& filePath)
	{
		std::ofstream file(filePath);
		if (!file.is_open()) [[unlikely]]
			throw std::runtime_error("Failed to open file '" + filePath + "' for writing!");
		WriteUsdContents(file, geometry);
		file.close();
	}

	void MeshWriter::WriteObjAsZip(Geometry* geometry, const std::string& texturePath, const std::string& zipPath)
	{
		OpenVulkano::ArchiveWriter zipWriter(zipPath.c_str());

		{
			std::stringstream objContents;
			WriteObjContents(geometry, DEFAULT_OBJ_MATERIAL_NAME, objContents);
			std::string objContentsStr = objContents.str();
			FileDescription objDesc = FileDescription::MkFile("model.obj", objContentsStr.size());
			zipWriter.AddFile(objDesc, objContentsStr.data());
		}
		{
			FileDescription mtlDesc = FileDescription::MkFile("material.mtl", DEFAULT_OBJ_MATERIAL_CONTENTS.size());
			zipWriter.AddFile(mtlDesc, DEFAULT_OBJ_MATERIAL_CONTENTS.data());
		}

		if (!texturePath.empty() && std::filesystem::exists(texturePath))
		{
			MemMappedFile textureFile(texturePath);
			FileDescription texDesc = FileDescription::MkFile("texture.png", textureFile.Size());
			zipWriter.AddFile(texDesc, textureFile.Data());
		}
	}

	void MeshWriter::WriteAsUSDZ(Geometry* geometry, const std::string& texturePath, const std::string& usdzPath)
	{
		OpenVulkano::ZipWriter zipWriter(usdzPath, true);
		
		{
			std::stringstream usdFile;
			WriteUsdContents(usdFile, geometry);
			std::string usdFileStr = usdFile.str();
			FileDescription usdDesc = FileDescription::MkFile("geometry.usda", usdFileStr.size());
			zipWriter.AddFile(usdDesc, usdFileStr.data());
		}

		if (!texturePath.empty() && std::filesystem::exists(texturePath))
		{
			MemMappedFile textureFile(texturePath);
			FileDescription texDesc = FileDescription::MkFile("texture.png", textureFile.Size());
			zipWriter.AddFile(texDesc, textureFile.Data());
		}
	}

	void MeshWriter::WriteAsFBX(Geometry* geometry, const std::string& texturePath, const std::string& fbxPath)
	{
#if __has_include("assimp/Exporter.hpp")
		aiNode rootNode;

		aiScene scene;
		scene.mRootNode = &rootNode;

		aiMesh mesh;
		mesh.mNumVertices = geometry->vertexCount;
		mesh.mMaterialIndex = 0;
		mesh.mPrimitiveTypes = aiPrimitiveType_TRIANGLE;
		mesh.mNumUVComponents[0] = 2;
		
		std::unique_ptr<aiVector3D[]> vertices = std::make_unique<aiVector3D[]>(geometry->vertexCount);
		mesh.mVertices = vertices.get();

		std::unique_ptr<aiVector3D[]> normals = std::make_unique<aiVector3D[]>(geometry->vertexCount);
		mesh.mNormals = normals.get();

		std::unique_ptr<aiVector3D[]> texCoords = std::make_unique<aiVector3D[]>(geometry->vertexCount);
		mesh.mTextureCoords[0] = texCoords.get();
		
		float scaling = 100; // fbx units are centimeters...
		for (uint32_t i = 0; i < geometry->vertexCount; ++i)
		{
			const Vertex& vertex = geometry->vertices[i];
			mesh.mVertices[i] = aiVector3D(vertex.position.x, vertex.position.y, vertex.position.z) * scaling;
			mesh.mNormals[i] = aiVector3D(vertex.normal.x, vertex.normal.y, vertex.normal.z);
			mesh.mTextureCoords[0][i] = aiVector3D(vertex.textureCoordinates.x, vertex.textureCoordinates.y, 0.0f);
		}

		mesh.mNumFaces = geometry->indexCount / 3;
		std::unique_ptr<aiFace[]> faces = std::make_unique<aiFace[]>(mesh.mNumFaces);
		mesh.mFaces = faces.get();

		std::unique_ptr<unsigned int[]> indices = std::make_unique<unsigned int[]>(geometry->indexCount);
		size_t lastUsedIndex = 0;

		for (uint32_t i = 0; i < mesh.mNumFaces; ++i)
		{
			aiFace& face = mesh.mFaces[i];
			face.mNumIndices = 3;
			face.mIndices = &indices[lastUsedIndex];

			face.mIndices[0] = geometry->GetIndex(i * 3 + 0);
			face.mIndices[1] = geometry->GetIndex(i * 3 + 1);
			face.mIndices[2] = geometry->GetIndex(i * 3 + 2);

			lastUsedIndex += face.mNumIndices;
		}

		aiMesh* meshes[1] = { &mesh };
		scene.mMeshes = meshes;
		scene.mNumMeshes = 1;

		unsigned int meshIndices[1];
		scene.mRootNode->mMeshes = meshIndices;
		scene.mRootNode->mMeshes[0] = 0;
		scene.mRootNode->mNumMeshes = 1;

		aiMaterial material;
		aiMaterial* materials[1] = { &material };
		scene.mMaterials = materials;
		scene.mNumMaterials = 1;

		aiString externalPath(texturePath);
		scene.mMaterials[0]->AddProperty(&externalPath, AI_MATKEY_TEXTURE(aiTextureType_DIFFUSE, 0));

		Assimp::Exporter exporter;
		aiReturn result = exporter.Export(&scene, "fbx", fbxPath);

		mesh.mVertices = nullptr;
		mesh.mNormals = nullptr;
		mesh.mTextureCoords[0] = nullptr;
		for (uint32_t i = 0; i < mesh.mNumFaces; ++i)
		{
			aiFace& face = mesh.mFaces[i];
			face.mIndices = nullptr;
		}
		mesh.mFaces = nullptr;

		rootNode.mMeshes = nullptr;
		scene.mRootNode = nullptr;
		scene.mMeshes = nullptr;
		scene.mMaterials = nullptr;

		if (result != aiReturn_SUCCESS)
		{
			throw std::runtime_error("Unable to write a fbx file to " + fbxPath + ": " + exporter.GetErrorString());
		}
#else
		throw std::runtime_error("Unable to convert the scene to FBX: Assimp is not available!");
#endif
	}
}
