#include "client/render/loader/ModelLoader.h"

#include "blitz/misc/Format.h"
#include "blitz/misc/Log.h"
#include "client/AssetsManager.h"
#include <assimp/Importer.hpp>	 // C++ importer interface
#include <assimp/postprocess.h>	 // Post processing flags
#include <assimp/scene.h>		 // Output data structure

#define VERTEX_SIZE 3
#define UV_SIZE 2

#define VERTEX_POSITION_INDEX 0
#define VERTEX_UV_INDEX 1
#define VERTEX_NORMAL_INDEX 2

namespace blitz {
namespace ModelLoader {

static VaoPtr ProcessMesh(aiMesh* mesh, const aiScene* scene, const aiMatrix4x4& transform) {
	std::vector<float> positions;
	std::vector<float> textureCoords;
	std::vector<float> normals;

	std::vector<unsigned int> indicies;

	aiFace* faces = mesh->mFaces;
	std::size_t faceNumber = mesh->mNumFaces;

	for (std::size_t j = 0; j < faceNumber; j++) {
		std::size_t offset = indicies.size();
		std::size_t numIndices = faces[j].mNumIndices;

		indicies.resize(indicies.size() + numIndices);
		std::memcpy(indicies.data() + offset, faces[j].mIndices, numIndices * sizeof(unsigned int));
	}

	std::size_t vertNumber = mesh->mNumVertices;
	aiVector3D* vertecies = mesh->mVertices;

	for (std::size_t j = 0; j < vertNumber; j++) {
		aiVector3D vertex = transform * vertecies[j];

		positions.push_back(vertex.x);
		positions.push_back(vertex.y);
		positions.push_back(vertex.z);
	}

	aiVector3D* vertexNormals = mesh->mNormals;

	for (std::size_t j = 0; j < vertNumber; j++) {
		aiVector3D normal = transform * vertexNormals[j];

		normals.push_back(normal.x);
		normals.push_back(normal.y);
		normals.push_back(normal.z);
	}

	aiVector3D** vertexTexture = mesh->mTextureCoords;

	for (std::size_t j = 0; j < vertNumber; j++) {
		textureCoords.push_back(vertexTexture[0][j].x);
		textureCoords.push_back(vertexTexture[0][j].y);
	}

	GL::VertexBuffer positionVBO(positions, VERTEX_SIZE);
	positionVBO.AddVertexAttribPointer(VERTEX_POSITION_INDEX, VERTEX_SIZE, 0);

	GL::VertexBuffer textureVBO(textureCoords, UV_SIZE);
	textureVBO.AddVertexAttribPointer(VERTEX_UV_INDEX, UV_SIZE, 0);

	GL::VertexBuffer normalVBO(normals, VERTEX_SIZE);
	normalVBO.AddVertexAttribPointer(VERTEX_NORMAL_INDEX, VERTEX_SIZE, 0);

	auto Vao = std::make_unique<GL::VertexArray>(GL::ElementBuffer{indicies});
	Vao->Bind();
	Vao->BindVertexBuffer(positionVBO);
	Vao->BindVertexBuffer(textureVBO);
	Vao->BindVertexBuffer(normalVBO);
	Vao->Unbind();

	return Vao;
}

static void ProcessNode(aiNode* node, const aiScene* scene, std::vector<VaoPtr>& meshes, const aiMatrix4x4& transform) {
	// recursive
	for (unsigned int i = 0; i < node->mNumChildren; i++) {
		ProcessNode(node->mChildren[i], scene, meshes, transform * node->mTransformation);
	}

	for (unsigned int i = 0; i < node->mNumMeshes; i++) {
		aiMesh* mesh = scene->mMeshes[node->mMeshes[i]];
		meshes.push_back(ProcessMesh(mesh, scene, transform * node->mTransformation));
	}
}

Model LoadModel(const std::string& fileName) {
	DataBuffer fileData = utils::AssetsManager::GetAsset(fileName);

	Assimp::Importer importer;

	const aiScene* scene = importer.ReadFileFromMemory(fileData.data(), fileData.GetSize(),
		aiProcess_CalcTangentSpace | aiProcess_Triangulate | aiProcess_SortByPType | aiProcess_OptimizeGraph |
			aiProcess_OptimizeMeshes);

	if (nullptr == scene) {
		utils::LOGE("[ModelLoader] Failed to load model !");
		return {};
	}

	utils::LOGD(utils::Format("[ModelLoader]\tModel nodes : %i", scene->mRootNode->mNumMeshes));

	Model model;
	ProcessNode(scene->mRootNode, scene, model.mVaos, {});

	return model;
}

}  // namespace ModelLoader
}  // namespace blitz
