#include "vkcv/asset/asset_loader.hpp"
#include <iostream>
#include <string.h>	// memcpy(3)
#include <set>
#include <stdlib.h>	// calloc(3)
#include <vulkan/vulkan.hpp>
#include <fx/gltf.h>
#include <stb_image.h>
#include <vkcv/Logger.hpp>
#include <algorithm>


namespace vkcv::asset {

	/**
	 * This function unrolls nested exceptions via recursion and prints them
	 * @param e	The exception being thrown
	 * @param path	The path to the file that was responsible for the exception
	 */
	static void recurseExceptionPrint(const std::exception& e, const std::string &path) {
		vkcv_log(LogLevel::ERROR, "Loading file %s: %s", path.c_str(), e.what());
		
		try {
			std::rethrow_if_nested(e);
		} catch (const std::exception& nested) {
			recurseExceptionPrint(nested, path);
		}
	}

	/**
	 * Returns the component count for an accessor type of the fx-gltf library.
	 * @param type	The accessor type
	 * @return	An unsigned integer count
	 */
	static uint32_t getComponentCount(const fx::gltf::Accessor::Type type) {
		switch (type) {
		case fx::gltf::Accessor::Type::Scalar:
			return 1;
		case fx::gltf::Accessor::Type::Vec2:
			return 2;
		case fx::gltf::Accessor::Type::Vec3:
			return 3;
		case fx::gltf::Accessor::Type::Vec4:
		case fx::gltf::Accessor::Type::Mat2:
			return 4;
		case fx::gltf::Accessor::Type::Mat3:
			return 9;
		case fx::gltf::Accessor::Type::Mat4:
			return 16;
		case fx::gltf::Accessor::Type::None:
		default:
			return 0;
		}
	}
	
	static uint32_t getComponentSize(ComponentType type) {
		switch (type) {
			case ComponentType::INT8:
			case ComponentType::UINT8:
				return 1;
			case ComponentType::INT16:
			case ComponentType::UINT16:
				return 2;
			case ComponentType::UINT32:
			case ComponentType::FLOAT32:
				return 4;
			default:
				return 0;
		}
	}

	/**
	 * Translate the component type used in the index accessor of fx-gltf to our
	 * enum for index type. The reason we have defined an incompatible enum that
	 * needs translation is that only a subset of component types is valid for
	 * indices and we want to catch these incompatibilities here.
	 * @param t	The component type
	 * @return 	The vkcv::IndexType enum representation
	 */
	enum IndexType getIndexType(const enum fx::gltf::Accessor::ComponentType &type) {
		switch (type) {
		case fx::gltf::Accessor::ComponentType::UnsignedByte:
			return IndexType::UINT8;
		case fx::gltf::Accessor::ComponentType::UnsignedShort:
			return IndexType::UINT16;
		case fx::gltf::Accessor::ComponentType::UnsignedInt:
			return IndexType::UINT32;
		default:
			vkcv_log(LogLevel::ERROR, "Index type not supported: %u", static_cast<uint16_t>(type));
			return IndexType::UNDEFINED;
		}
	}

	/**
	 * This function fills the array of vertex attributes of a VertexGroup (usually
	 * part of a vkcv::asset::Mesh) object based on the description of attributes
	 * for a fx::gltf::Primitive.
	 *
	 * @param src	The description of attribute objects from the fx-gltf library
	 * @param gltf	The main glTF document
	 * @param dst	The array of vertex attributes stored in an asset::Mesh object
	 * @return	ASSET_ERROR when at least one VertexAttribute could not be
	 * 		constructed properly, otherwise ASSET_SUCCESS
	 */
	static int loadVertexAttributes(const fx::gltf::Attributes &src,
									const std::vector<fx::gltf::Accessor> &accessors,
									const std::vector<fx::gltf::BufferView> &bufferViews,
									std::vector<VertexAttribute> &dst) {
		for (const auto &attrib : src) {
			VertexAttribute att {};
			
			if (attrib.first == "POSITION") {
				att.type = PrimitiveType::POSITION;
			} else if (attrib.first == "NORMAL") {
				att.type = PrimitiveType::NORMAL;
			} else if (attrib.first == "TANGENT") {
				att.type = PrimitiveType::TANGENT;
			} else if (attrib.first == "TEXCOORD_0") {
				att.type = PrimitiveType::TEXCOORD_0;
			} else if (attrib.first == "TEXCOORD_1") {
				att.type = PrimitiveType::TEXCOORD_1;
			} else if (attrib.first == "COLOR_0") {
				att.type = PrimitiveType::COLOR_0;
			} else if (attrib.first == "COLOR_1") {
				att.type = PrimitiveType::COLOR_1;
			} else if (attrib.first == "JOINTS_0") {
				att.type = PrimitiveType::JOINTS_0;
			} else if (attrib.first == "WEIGHTS_0") {
				att.type = PrimitiveType::WEIGHTS_0;
			} else {
				att.type = PrimitiveType::UNDEFINED;
			}
			
			if (att.type != PrimitiveType::UNDEFINED) {
				const fx::gltf::Accessor &accessor = accessors[attrib.second];
				const fx::gltf::BufferView &buf = bufferViews[accessor.bufferView];
				
				att.offset = buf.byteOffset;
				att.length = buf.byteLength;
				att.stride = buf.byteStride;
				att.componentType = static_cast<ComponentType>(accessor.componentType);
				att.componentCount = getComponentCount(accessor.type);
				
				/* Assume tightly packed stride as not explicitly provided */
				if (att.stride == 0) {
					att.stride = att.componentCount * getComponentSize(att.componentType);
				}
			}
			
			if ((att.type == PrimitiveType::UNDEFINED) ||
				(att.componentCount == 0)) {
				return ASSET_ERROR;
			}
			
			dst.push_back(att);
		}
		
		return ASSET_SUCCESS;
	}

	/**
	 * This function calculates the modelMatrix out of the data given in the gltf file.
	 * It also checks, whether a modelMatrix was given.
	 *
	 * @param translation possible translation vector (default 0,0,0)
	 * @param scale possible scale vector (default 1,1,1)
	 * @param rotation possible rotation, given in quaternion (default 0,0,0,1)
	 * @param matrix possible modelmatrix (default identity)
	 * @return model Matrix as an array of floats
	 */
	static std::array<float, 16> calculateModelMatrix(const std::array<float, 3>& translation,
													  const std::array<float, 3>& scale,
													  const std::array<float, 4>& rotation,
													  const std::array<float, 16>& matrix){
		std::array<float, 16> modelMatrix = {
				1,0,0,0,
				0,1,0,0,
				0,0,1,0,
				0,0,0,1
		};
		
		if (matrix != modelMatrix){
			return matrix;
		} else {
			// translation
			modelMatrix[3] = translation[0];
			modelMatrix[7] = translation[1];
			modelMatrix[11] = translation[2];
			
			// rotation and scale
			auto a = rotation[0];
			auto q1 = rotation[1];
			auto q2 = rotation[2];
			auto q3 = rotation[3];
	
			modelMatrix[0] = (2 * (a * a + q1 * q1) - 1) * scale[0];
			modelMatrix[1] = (2 * (q1 * q2 - a * q3)) * scale[1];
			modelMatrix[2] = (2 * (q1 * q3 + a * q2)) * scale[2];
	
			modelMatrix[4] = (2 * (q1 * q2 + a * q3)) * scale[0];
			modelMatrix[5] = (2 * (a * a + q2 * q2) - 1) * scale[1];
			modelMatrix[6] = (2 * (q2 * q3 - a * q1)) * scale[2];
	
			modelMatrix[8] = (2 * (q1 * q3 - a * q2)) * scale[0];
			modelMatrix[9] = (2 * (q2 * q3 + a * q1)) * scale[1];
			modelMatrix[10] = (2 * (a * a + q3 * q3) - 1) * scale[2];
	
			// flip y, because GLTF uses y up, but vulkan -y up
			modelMatrix[5] *= -1;
	
			return modelMatrix;
		}
	}

	bool Material::hasTexture(const PBRTextureTarget target) const {
		return textureMask & bitflag(target);
	}

	/**
	 * This function translates a given fx-gltf-sampler-wrapping-mode-enum to its vulkan sampler-adress-mode counterpart.
	 * @param mode: wrapping mode of a sampler given as fx-gltf-enum
	 * @return int vulkan-enum representing the same wrapping mode
	 */
	static int translateSamplerMode(const fx::gltf::Sampler::WrappingMode mode) {
		switch (mode) {
		case fx::gltf::Sampler::WrappingMode::ClampToEdge:
			return VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
		case fx::gltf::Sampler::WrappingMode::MirroredRepeat:
			return VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT;
		case fx::gltf::Sampler::WrappingMode::Repeat:
		default:
			return VK_SAMPLER_ADDRESS_MODE_REPEAT;
		}
	}

	/**
	 * If the glTF doesn't define samplers, we use the defaults defined by fx-gltf.
	 * The following are details about the glTF/OpenGL to Vulkan translation.
	 * magFilter (VkFilter?):
	 * 	GL_NEAREST -> VK_FILTER_NEAREST
	 * 	GL_LINEAR -> VK_FILTER_LINEAR
	 * minFilter (VkFilter?):
	 * mipmapMode (VkSamplerMipmapMode?):
	 * Vulkans minFilter and mipmapMode combined correspond to OpenGLs
	 * GL_minFilter_MIPMAP_mipmapMode:
	 * 	GL_NEAREST_MIPMAP_NEAREST:
	 * 		minFilter=VK_FILTER_NEAREST
	 * 		mipmapMode=VK_SAMPLER_MIPMAP_MODE_NEAREST
	 * 	GL_LINEAR_MIPMAP_NEAREST:
	 * 		minFilter=VK_FILTER_LINEAR
	 * 		mipmapMode=VK_SAMPLER_MIPMAP_MODE_NEAREST
	 * 	GL_NEAREST_MIPMAP_LINEAR:
	 * 		minFilter=VK_FILTER_NEAREST
	 * 		mipmapMode=VK_SAMPLER_MIPMAP_MODE_LINEAR
	 * 	GL_LINEAR_MIPMAP_LINEAR:
	 * 		minFilter=VK_FILTER_LINEAR
	 * 		mipmapMode=VK_SAMPLER_MIPMAP_MODE_LINEAR
	 * The modes of GL_LINEAR and GL_NEAREST have to be emulated using
	 * mipmapMode=VK_SAMPLER_MIPMAP_MODE_NEAREST with specific minLOD and maxLOD:
	 * 	GL_LINEAR:
	 * 		minFilter=VK_FILTER_LINEAR
	 * 		mipmapMode=VK_SAMPLER_MIPMAP_MODE_NEAREST
	 * 		minLOD=0, maxLOD=0.25
	 * 	GL_NEAREST:
	 * 		minFilter=VK_FILTER_NEAREST
	 * 		mipmapMode=VK_SAMPLER_MIPMAP_MODE_NEAREST
	 * 		minLOD=0, maxLOD=0.25
	 * Setting maxLOD=0 causes magnification to always be performed (using
	 * the defined magFilter), this may be valid if the min- and magFilter
	 * are equal, otherwise it won't be the expected behaviour from OpenGL
	 * and glTF; instead using maxLod=0.25 allows the minFilter to be
	 * performed while still always rounding to the base level.
	 * With other modes, minLOD and maxLOD default to:
	 * 	minLOD=0
	 * 	maxLOD=VK_LOD_CLAMP_NONE
	 * wrapping:
	 * gltf has wrapS, wrapT with {clampToEdge, MirroredRepeat, Repeat} while
	 * Vulkan has addressModeU, addressModeV, addressModeW with values
	 * VK_SAMPLER_ADDRESS_MODE_{REPEAT,MIRRORED_REPEAT,CLAMP_TO_EDGE,
	 * 			    CAMP_TO_BORDER,MIRROR_CLAMP_TO_EDGE}
	 * Translation from glTF to Vulkan is straight forward for the 3 existing
	 * modes, default is repeat, the other modes aren't available.
	 */
	static vkcv::asset::Sampler loadSampler(const fx::gltf::Sampler &src) {
		Sampler dst {};
		
		dst.minLOD = 0;
		dst.maxLOD = VK_LOD_CLAMP_NONE;
	
		switch (src.minFilter) {
		case fx::gltf::Sampler::MinFilter::None:
		case fx::gltf::Sampler::MinFilter::Nearest:
			dst.minFilter = VK_FILTER_NEAREST;
			dst.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST;
			dst.maxLOD = 0.25;
			break;
		case fx::gltf::Sampler::MinFilter::Linear:
			dst.minFilter = VK_FILTER_LINEAR;
			dst.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST;
			dst.maxLOD = 0.25;
			break;
		case fx::gltf::Sampler::MinFilter::NearestMipMapNearest:
			dst.minFilter = VK_FILTER_NEAREST;
			dst.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST;
			break;
		case fx::gltf::Sampler::MinFilter::LinearMipMapNearest:
			dst.minFilter = VK_FILTER_LINEAR;
			dst.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST;
			break;
		case fx::gltf::Sampler::MinFilter::NearestMipMapLinear:
			dst.minFilter = VK_FILTER_NEAREST;
			dst.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
			break;
		case fx::gltf::Sampler::MinFilter::LinearMipMapLinear:
			dst.minFilter = VK_FILTER_LINEAR;
			dst.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
			break;
		default:
			break;
		}
	
		switch (src.magFilter) {
		case fx::gltf::Sampler::MagFilter::None:
		case fx::gltf::Sampler::MagFilter::Nearest:
			dst.magFilter = VK_FILTER_NEAREST;
			break;
		case fx::gltf::Sampler::MagFilter::Linear:
			dst.magFilter = VK_FILTER_LINEAR;
			break;
		default:
			break;
		}
	
		dst.addressModeU = translateSamplerMode(src.wrapS);
		dst.addressModeV = translateSamplerMode(src.wrapT);
		
		// There is no information about wrapping for a third axis in glTF and
		// we have to hardcode this value.
		dst.addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT;
		
		return dst;
	}

	/**
	 * Initializes vertex groups of a Mesh, including copying the data to
	 * index- and vertex-buffers.
	 */
	static int loadVertexGroups(const fx::gltf::Mesh &objectMesh,
								const fx::gltf::Document &sceneObjects,
								Scene &scene, Mesh &mesh) {
		mesh.vertexGroups.reserve(objectMesh.primitives.size());
	
		for (const auto &objectPrimitive : objectMesh.primitives) {
			VertexGroup vertexGroup;
			
			vertexGroup.vertexBuffer.attributes.reserve(
					objectPrimitive.attributes.size()
			);
	
			if (ASSET_SUCCESS != loadVertexAttributes(
					objectPrimitive.attributes,
					sceneObjects.accessors,
					sceneObjects.bufferViews,
					vertexGroup.vertexBuffer.attributes)) {
				vkcv_log(LogLevel::ERROR, "Failed to get vertex attributes of '%s'",
						 mesh.name.c_str());
				return ASSET_ERROR;
			}
			
			// The accessor for the position attribute is used for
			// 1) getting the vertex buffer view which is only needed to get
			//    the vertex buffer
			// 2) getting the vertex count for the VertexGroup
			// 3) getting the min/max of the bounding box for the VertexGroup
			fx::gltf::Accessor posAccessor;
			bool noPosition = true;
			
			for (auto const& attrib : objectPrimitive.attributes) {
				if (attrib.first == "POSITION") {
					posAccessor = sceneObjects.accessors[attrib.second];
					noPosition = false;
					break;
				}
			}
			
			if (noPosition) {
				vkcv_log(LogLevel::ERROR, "Position attribute not found from '%s'",
						 mesh.name.c_str());
				return ASSET_ERROR;
			}
	
			const fx::gltf::Accessor& indexAccessor = sceneObjects.accessors[objectPrimitive.indices];
			
			int indexBufferURI;
			if (objectPrimitive.indices >= 0) { // if there is no index buffer, -1 is returned from fx-gltf
				const fx::gltf::BufferView& indexBufferView = sceneObjects.bufferViews[indexAccessor.bufferView];
				const fx::gltf::Buffer& indexBuffer = sceneObjects.buffers[indexBufferView.buffer];
				
				// Because the buffers are already preloaded into the memory by the gltf-library,
				// it makes no sense to load them later on manually again into memory.
				vertexGroup.indexBuffer.data.resize(indexBufferView.byteLength);
				memcpy(vertexGroup.indexBuffer.data.data(),
					   indexBuffer.data.data() + indexBufferView.byteOffset,
					   indexBufferView.byteLength);
			} else {
				indexBufferURI = -1;
			}
			
			vertexGroup.indexBuffer.type = getIndexType(indexAccessor.componentType);
			
			if (IndexType::UNDEFINED == vertexGroup.indexBuffer.type) {
				vkcv_log(LogLevel::ERROR, "Index Type undefined or not supported.");
				return ASSET_ERROR;
			}
	
			if (posAccessor.bufferView >= sceneObjects.bufferViews.size()) {
				vkcv_log(LogLevel::ERROR, "Access to bufferView out of bounds: %d",
						posAccessor.bufferView);
				return ASSET_ERROR;
			}
			const fx::gltf::BufferView& vertexBufferView = sceneObjects.bufferViews[posAccessor.bufferView];
			if (vertexBufferView.buffer >= sceneObjects.buffers.size()) {
				vkcv_log(LogLevel::ERROR, "Access to buffer out of bounds: %d",
						vertexBufferView.buffer);
				return ASSET_ERROR;
			}
			const fx::gltf::Buffer& vertexBuffer = sceneObjects.buffers[vertexBufferView.buffer];
			
			// only copy relevant part of vertex data
			uint32_t relevantBufferOffset = std::numeric_limits<uint32_t>::max();
			uint32_t relevantBufferEnd = 0;
			
			for (const auto& attribute : vertexGroup.vertexBuffer.attributes) {
				relevantBufferOffset = std::min(attribute.offset, relevantBufferOffset);
				relevantBufferEnd = std::max(relevantBufferEnd, attribute.offset + attribute.length);
			}
			
			const uint32_t relevantBufferSize = relevantBufferEnd - relevantBufferOffset;
			
			vertexGroup.vertexBuffer.data.resize(relevantBufferSize);
			memcpy(vertexGroup.vertexBuffer.data.data(),
				   vertexBuffer.data.data() + relevantBufferOffset,
				   relevantBufferSize);
			
			// make vertex attributes relative to copied section
			for (auto& attribute : vertexGroup.vertexBuffer.attributes) {
				attribute.offset -= relevantBufferOffset;
			}
			
			vertexGroup.mode = static_cast<PrimitiveMode>(objectPrimitive.mode);
			vertexGroup.numIndices = sceneObjects.accessors[objectPrimitive.indices].count;
			vertexGroup.numVertices = posAccessor.count;
			
			memcpy(&(vertexGroup.min), posAccessor.min.data(), sizeof(vertexGroup.min));
			memcpy(&(vertexGroup.max), posAccessor.max.data(), sizeof(vertexGroup.max));
			
			vertexGroup.materialIndex = static_cast<uint8_t>(objectPrimitive.material);
			
			mesh.vertexGroups.push_back(static_cast<int>(scene.vertexGroups.size()));
			scene.vertexGroups.push_back(vertexGroup);
		}
		
		return ASSET_SUCCESS;
	}

	/**
	 * Returns an integer with specific bits set corresponding to the
	 * textures that appear in the given material. This mask is used in the
	 * vkcv::asset::Material struct and can be tested via the hasTexture
	 * method.
	 */
	static uint16_t generateTextureMask(fx::gltf::Material &material) {
		uint16_t textureMask = 0;
		
		if (material.pbrMetallicRoughness.baseColorTexture.index >= 0) {
			textureMask |= bitflag(asset::PBRTextureTarget::baseColor);
		}
		if (material.pbrMetallicRoughness.metallicRoughnessTexture.index >= 0) {
			textureMask |= bitflag(asset::PBRTextureTarget::metalRough);
		}
		if (material.normalTexture.index >= 0) {
			textureMask |= bitflag(asset::PBRTextureTarget::normal);
		}
		if (material.occlusionTexture.index >= 0) {
			textureMask |= bitflag(asset::PBRTextureTarget::occlusion);
		}
		if (material.emissiveTexture.index >= 0) {
			textureMask |= bitflag(asset::PBRTextureTarget::emissive);
		}
		
		return textureMask;
	}

	int probeScene(const std::filesystem::path& path, Scene& scene) {
		fx::gltf::Document sceneObjects;
	
		try {
			if (path.extension() == ".glb") {
				sceneObjects = fx::gltf::LoadFromBinary(path.string());
			} else {
				sceneObjects = fx::gltf::LoadFromText(path.string());
			}
		} catch (const std::system_error& err) {
			recurseExceptionPrint(err, path.string());
			return ASSET_ERROR;
		} catch (const std::exception& e) {
			recurseExceptionPrint(e, path.string());
			return ASSET_ERROR;
		}
		
		const auto directory = path.parent_path();
		
		scene.meshes.clear();
		scene.vertexGroups.clear();
		scene.materials.clear();
		scene.textures.clear();
		scene.samplers.clear();
	
		// file has to contain at least one mesh
		if (sceneObjects.meshes.empty()) {
			vkcv_log(LogLevel::ERROR, "No meshes found! (%s)", path.c_str());
			return ASSET_ERROR;
		} else {
			scene.meshes.reserve(sceneObjects.meshes.size());
			
			for (size_t i = 0; i < sceneObjects.meshes.size(); i++) {
				Mesh mesh;
				mesh.name = sceneObjects.meshes[i].name;
				
				if (loadVertexGroups(sceneObjects.meshes[i], sceneObjects, scene, mesh) != ASSET_SUCCESS) {
					vkcv_log(LogLevel::ERROR, "Failed to load vertex groups of '%s'! (%s)",
							 mesh.name.c_str(), path.c_str());
					return ASSET_ERROR;
				}
				
				scene.meshes.push_back(mesh);
			}
			
			// This only works if the node has a mesh and it only loads the meshes and ignores cameras and lights
			for (const auto& node : sceneObjects.nodes) {
				if ((node.mesh >= 0) && (node.mesh < scene.meshes.size())) {
					scene.meshes[node.mesh].modelMatrix = calculateModelMatrix(
							node.translation,
							node.scale,
							node.rotation,
							node.matrix
					);
				}
			}
		}
		
		if (sceneObjects.samplers.empty()) {
			vkcv_log(LogLevel::WARNING, "No samplers found! (%s)", path.c_str());
		} else {
			scene.samplers.reserve(sceneObjects.samplers.size());
			
			for (const auto &samplerObject : sceneObjects.samplers) {
				scene.samplers.push_back(loadSampler(samplerObject));
			}
		}
		
		if (sceneObjects.textures.empty()) {
			vkcv_log(LogLevel::WARNING, "No textures found! (%s)", path.c_str());
		} else {
			scene.textures.reserve(sceneObjects.textures.size());
			
			for (const auto& textureObject : sceneObjects.textures) {
				Texture texture;
				
				if (textureObject.sampler < 0) {
					texture.sampler = -1;
				} else
				if (static_cast<size_t>(textureObject.sampler) >= scene.samplers.size()) {
					vkcv_log(LogLevel::ERROR, "Sampler of texture '%s' missing (%s)",
							 textureObject.name.c_str(), path.c_str());
					return ASSET_ERROR;
				} else {
					texture.sampler = textureObject.sampler;
				}
				
				if ((textureObject.source < 0) ||
					(static_cast<size_t>(textureObject.source) >= sceneObjects.images.size())) {
					vkcv_log(LogLevel::ERROR, "Failed to load texture '%s' (%s)",
							 textureObject.name.c_str(), path.c_str());
					return ASSET_ERROR;
				}
				
				const auto& image = sceneObjects.images[textureObject.source];
				
				if (image.uri.empty()) {
					const fx::gltf::BufferView bufferView = sceneObjects.bufferViews[image.bufferView];
					
					texture.path.clear();
					texture.data.resize(bufferView.byteLength);
					memcpy(texture.data.data(),
						   sceneObjects.buffers[bufferView.buffer].data.data() + bufferView.byteOffset,
						   bufferView.byteLength);
				} else {
					texture.path = directory / image.uri;
				}
				
				scene.textures.push_back(texture);
			}
		}
		
		if (sceneObjects.materials.empty()) {
			vkcv_log(LogLevel::WARNING, "No materials found! (%s)", path.c_str());
		} else {
			scene.materials.reserve(sceneObjects.materials.size());
			
			for (auto material : sceneObjects.materials) {
				scene.materials.push_back({
						generateTextureMask(material),
						material.pbrMetallicRoughness.baseColorTexture.index,
						material.pbrMetallicRoughness.metallicRoughnessTexture.index,
						material.normalTexture.index,
						material.occlusionTexture.index,
						material.emissiveTexture.index,
						{
								material.pbrMetallicRoughness.baseColorFactor[0],
								material.pbrMetallicRoughness.baseColorFactor[1],
								material.pbrMetallicRoughness.baseColorFactor[2],
								material.pbrMetallicRoughness.baseColorFactor[3]
						},
						material.pbrMetallicRoughness.metallicFactor,
						material.pbrMetallicRoughness.roughnessFactor,
						material.normalTexture.scale,
						material.occlusionTexture.strength,
						{
								material.emissiveFactor[0],
								material.emissiveFactor[1],
								material.emissiveFactor[2]
						}
				});
			}
		}
	
		return ASSET_SUCCESS;
	}
	
	/**
	 * Loads and decodes the textures data based on the textures file path.
	 * The path member is the only one that has to be initialized before
	 * calling this function, the others (width, height, channels, data)
	 * are set by this function and the sampler is of no concern here.
	 */
	static int loadTextureData(Texture& texture) {
		if ((texture.width > 0) && (texture.height > 0) && (texture.channels > 0) &&
			(!texture.data.empty())) {
			return ASSET_SUCCESS; // Texture data was loaded already!
		}
		
		uint8_t* data;
		
		if (texture.path.empty()) {
			data = stbi_load_from_memory(
					reinterpret_cast<uint8_t*>(texture.data.data()),
					static_cast<int>(texture.data.size()),
					&texture.width,
					&texture.height,
					&texture.channels, 4
			);
		} else {
			data = stbi_load(
					texture.path.string().c_str(),
					&texture.width,
					&texture.height,
					&texture.channels, 4
			);
		}
		
		if (!data) {
			vkcv_log(LogLevel::ERROR, "Texture could not be loaded from '%s'",
					 texture.path.c_str());
			
			texture.width = 0;
			texture.height = 0;
			texture.channels = 0;
			return ASSET_ERROR;
		}
		
		texture.data.resize(texture.width * texture.height * 4);
		memcpy(texture.data.data(), data, texture.data.size());
		stbi_image_free(data);
	
		return ASSET_SUCCESS;
	}

	int loadMesh(Scene &scene, int index) {
		if ((index < 0) || (static_cast<size_t>(index) >= scene.meshes.size())) {
			vkcv_log(LogLevel::ERROR, "Mesh index out of range: %d", index);
			return ASSET_ERROR;
		}
		
		const Mesh &mesh = scene.meshes[index];
		
		for (const auto& vg : mesh.vertexGroups) {
			const VertexGroup &vertexGroup = scene.vertexGroups[vg];
			const Material& material = scene.materials[vertexGroup.materialIndex];
			
			if (material.hasTexture(PBRTextureTarget::baseColor)) {
				const int result = loadTextureData(scene.textures[material.baseColor]);
				if (ASSET_SUCCESS != result) {
					vkcv_log(LogLevel::ERROR, "Failed loading baseColor texture of mesh '%s'",
							 mesh.name.c_str())
					return result;
				}
			}
			
			if (material.hasTexture(PBRTextureTarget::metalRough)) {
				const int result = loadTextureData(scene.textures[material.metalRough]);
				if (ASSET_SUCCESS != result) {
					vkcv_log(LogLevel::ERROR, "Failed loading metalRough texture of mesh '%s'",
							 mesh.name.c_str())
					return result;
				}
			}
			
			if (material.hasTexture(PBRTextureTarget::normal)) {
				const int result = loadTextureData(scene.textures[material.normal]);
				if (ASSET_SUCCESS != result) {
					vkcv_log(LogLevel::ERROR, "Failed loading normal texture of mesh '%s'",
							 mesh.name.c_str())
					return result;
				}
			}
			
			if (material.hasTexture(PBRTextureTarget::occlusion)) {
				const int result = loadTextureData(scene.textures[material.occlusion]);
				if (ASSET_SUCCESS != result) {
					vkcv_log(LogLevel::ERROR, "Failed loading occlusion texture of mesh '%s'",
							 mesh.name.c_str())
					return result;
				}
			}
			
			if (material.hasTexture(PBRTextureTarget::emissive)) {
				const int result = loadTextureData(scene.textures[material.emissive]);
				if (ASSET_SUCCESS != result) {
					vkcv_log(LogLevel::ERROR, "Failed loading emissive texture of mesh '%s'",
							 mesh.name.c_str())
					return result;
				}
			}
		}
	
		return ASSET_SUCCESS;
	}
	
	int loadScene(const std::filesystem::path &path, Scene &scene) {
		int result = probeScene(path, scene);
		
		if (result != ASSET_SUCCESS) {
			vkcv_log(LogLevel::ERROR, "Loading scene failed '%s'",
					 path.c_str());
			return result;
		}
		
		for (size_t i = 0; i < scene.meshes.size(); i++) {
			result = loadMesh(scene, static_cast<int>(i));
			
			if (result != ASSET_SUCCESS) {
				vkcv_log(LogLevel::ERROR, "Loading mesh with index %d failed '%s'",
						 static_cast<int>(i), path.c_str());
				return result;
			}
		}
		
		return ASSET_SUCCESS;
	}
	
	Texture loadTexture(const std::filesystem::path& path) {
		Texture texture;
		texture.path = path;
		texture.sampler = -1;
		if (loadTextureData(texture) != ASSET_SUCCESS) {
			texture.path.clear();
			texture.w = texture.h = texture.channels = 0;
			texture.data.clear();
		}
		return texture;
	}

}