/*
 * 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 "Geometry.hpp"
#include "Vertex.hpp"
#include "Base/Utils.hpp"
#include "Base/Logger.hpp"
#if __has_include("assimp/Importer.hpp")
#include <assimp/Importer.hpp>
#include <assimp/scene.h>
#include <assimp/mesh.h>
#include <assimp/postprocess.h>
#define ASSIMP_AVAILABLE
#endif
#include <stdexcept>

namespace OpenVulkano::Scene
{
	Geometry::Geometry(const Geometry& other) 
	{ 
		this->vertexCount = other.vertexCount;
		this->indexCount = other.indexCount;
		this->indexType = other.indexType;
		this->aabb = other.aabb;
		this->ownsMemory = other.ownsMemory;
		this->freeAfterUpload = other.freeAfterUpload;
		this->renderGeo = nullptr;
		this->vertices = new Vertex[vertexCount];
		if (other.vertices) 
		{
			std::copy(other.vertices, other.vertices + other.vertexCount, this->vertices); 
		}
		this->indices = malloc(static_cast<size_t>(Utils::EnumAsInt(other.indexType)) * other.indexCount);
		if (other.indices)
		{
			if (other.indexType == VertexIndexType::UINT16)
			{
				std::copy(static_cast<uint16_t*>(other.indices),
				          static_cast<uint16_t*>(other.indices) + other.indexCount,
				          static_cast<uint16_t*>(this->indices));
			}
			else
			{
				std::copy(static_cast<uint32_t*>(other.indices),
				          static_cast<uint32_t*>(other.indices) + other.indexCount,
				          static_cast<uint32_t*>(this->indices));
			}
		}
	}

	Geometry& Geometry::operator=(const Geometry& other) 
	{ 
		Geometry tmp(other);
		this->Swap(tmp); 
		return *this;
	}

	Geometry::Geometry(Geometry&& other) noexcept 
	{
		this->vertexCount = other.vertexCount;
		this->indexCount = other.indexCount;
		this->indexType = other.indexType;
		this->ownsMemory = other.ownsMemory;
		this->freeAfterUpload = other.freeAfterUpload;
		this->aabb = std::move(other.aabb);
		this->vertices = other.vertices;
		this->indices = other.indices;
		this->renderGeo = other.renderGeo;
		other.vertexCount = other.indexCount = 0;
		other.vertices = nullptr;
		other.indices = nullptr;
		other.renderGeo = nullptr;
	}

	Geometry& Geometry::operator=(Geometry&& other) noexcept 
	{ 
		if (this != &other)
		{
			Close();
			this->vertexCount = other.vertexCount;
			this->indexCount = other.indexCount;
			this->indexType = other.indexType;
			this->ownsMemory = other.ownsMemory;
			this->freeAfterUpload = other.freeAfterUpload;
			this->aabb = std::move(other.aabb);
			this->vertices = other.vertices;
			this->indices = other.indices;
			this->renderGeo = other.renderGeo;
			other.vertexCount = other.indexCount = 0;
			other.vertices = nullptr;
			other.indices = nullptr;
			other.renderGeo = nullptr;
		}
		return *this;
	}

	Geometry::~Geometry() 
	{
		Geometry::Close();
	}

	void Geometry::Swap(Geometry& other) noexcept
	{ 
		std::swap(this->vertexCount, other.vertexCount);
		std::swap(this->indexCount, other.indexCount);
		std::swap(this->aabb, other.aabb);
		std::swap(this->indexType, other.indexType);
		std::swap(this->vertices, other.vertices);
		std::swap(this->indices, other.indices);
		std::swap(this->renderGeo, other.renderGeo);
		std::swap(this->ownsMemory, other.ownsMemory);
		std::swap(this->freeAfterUpload, other.freeAfterUpload);
	}

	void Geometry::Init(uint32_t vertexCount, uint32_t indexCount)
	{
		if (this->vertexCount || this->indexCount) throw std::runtime_error("Geometry is already initialized.");
		this->vertexCount = vertexCount;
		this->indexCount = indexCount;
		indexType = (vertexCount > UINT16_MAX) ? VertexIndexType::UINT32 : VertexIndexType::UINT16;
		vertices = new Vertex[vertexCount];
		indices = malloc(static_cast<size_t>(Utils::EnumAsInt(indexType)) * indexCount);
		renderGeo = nullptr;
	}

	void Geometry::InitFromFile(const std::string& file)
	{
#ifdef ASSIMP_AVAILABLE
		Assimp::Importer importer;

		const uint32_t flags = aiProcess_CalcTangentSpace | aiProcess_Triangulate | aiProcess_JoinIdenticalVertices | aiProcess_GenNormals |
			aiProcess_ImproveCacheLocality | aiProcess_RemoveRedundantMaterials | aiProcess_GenUVCoords | aiProcess_TransformUVCoords |
			aiProcess_ConvertToLeftHanded | aiProcess_PreTransformVertices | aiProcess_OptimizeGraph;

		const aiScene* scene = importer.ReadFile(file, flags);
		if (!scene) throw std::runtime_error("Failed to load file \"" + file + "\" Error: " + importer.GetErrorString());
		if (!scene->HasMeshes()) throw std::runtime_error("File \"" + file + "\" does not have any meshes");
		if (scene->mNumMeshes > 1) Logger::DATA->warn("File {0} contains more than one mesh. Only first one will be loaded", file);
		Init(scene->mMeshes[0]);
		importer.FreeScene();
#else
		throw std::runtime_error("ASSIMP not available!");
#endif
	}

	void Geometry::Init(aiMesh* mesh)
	{
#ifdef ASSIMP_AVAILABLE
		aabb.Reset();
		Init(mesh->mNumVertices, mesh->mNumFaces * 3); // Reserve the space for the data
		for (unsigned int i = 0; i < mesh->mNumVertices; i++)
		{
			vertices[i].Set(mesh->mVertices[i]);
			if (mesh->HasNormals()) vertices[i].SetNormal(mesh->mNormals[i]);
			if (mesh->HasTangentsAndBitangents())
			{
				vertices[i].SetTangentAndBiTangent(mesh->mTangents[i], mesh->mBitangents[i]);
			}
			if (mesh->HasTextureCoords(0)) vertices[i].SetTextureCoordinates(mesh->mTextureCoords[0][i]);
			if (mesh->HasVertexColors(0)) vertices[i].SetColor(mesh->mColors[0][i]);
			aabb.Grow(vertices[i].position);
		}

		for (unsigned int i = 0; i < mesh->mNumFaces; i++)
		{
			const aiFace face = mesh->mFaces[i];
			if (face.mNumIndices != 3) throw std::runtime_error("Mesh is not a triangle mesh!");
			for (unsigned int j = 0; j < face.mNumIndices; j++)
			{
				if (indexType == VertexIndexType::UINT16)
				{
					static_cast<uint16_t*>(indices)[i * face.mNumIndices + j] = static_cast<uint16_t>(face.mIndices[j]);
				}
				else
				{
					static_cast<uint32_t*>(indices)[i * face.mNumIndices + j] = face.mIndices[j];
				}
			}
		}

		//TODO load bones
		//TODO load materials
#else
		throw std::runtime_error("ASSIMP not available!");
#endif
	}

	void Geometry::SetIndices(const uint32_t* data, uint32_t size, uint32_t dstOffset) const
	{
		for(uint32_t i = 0; i < size; i++)
		{
			if (indexType == VertexIndexType::UINT16)
			{
				static_cast<uint16_t*>(indices)[i + dstOffset] = static_cast<uint16_t>(data[i]);
			}
			else
			{ static_cast<uint32_t*>(indices)[i + dstOffset] = data[i];
			}
		}
	}

	void Geometry::Close()
	{
		if (ownsMemory)
		{
			vertexCount = 0;
			indexCount = 0;
			Free();
		}
		if (renderGeo)
		{
			renderGeo->Close();
			renderGeo = nullptr;
		}
	}

	void Geometry::Free()
	{
		delete[] vertices;
		free(indices);
		vertices = nullptr;
		indices = nullptr;
	}
}
