// This file is part of gltfpack; see gltfpack.h for version/license details
#include "gltfpack.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static const char* getError(cgltf_result result, cgltf_data* data)
{
	switch (result)
	{
	case cgltf_result_file_not_found:
		return data ? "resource not found" : "file not found";

	case cgltf_result_io_error:
		return "I/O error";

	case cgltf_result_invalid_json:
		return "invalid JSON";

	case cgltf_result_invalid_gltf:
		return "invalid GLTF";

	case cgltf_result_out_of_memory:
		return "out of memory";

	case cgltf_result_legacy_gltf:
		return "legacy GLTF";

	case cgltf_result_data_too_short:
		return data ? "buffer too short" : "not a GLTF file";

	case cgltf_result_unknown_format:
		return data ? "unknown resource format" : "not a GLTF file";

	default:
		return "unknown error";
	}
}

static void readAccessor(std::vector<float>& data, const cgltf_accessor* accessor)
{
	assert(accessor->type == cgltf_type_scalar);

	data.resize(accessor->count);
	cgltf_accessor_unpack_floats(accessor, &data[0], data.size());
}

static void readAccessor(std::vector<Attr>& data, const cgltf_accessor* accessor)
{
	size_t components = cgltf_num_components(accessor->type);

	std::vector<float> temp(accessor->count * components);
	cgltf_accessor_unpack_floats(accessor, &temp[0], temp.size());

	data.resize(accessor->count);

	for (size_t i = 0; i < accessor->count; ++i)
	{
		for (size_t k = 0; k < components && k < 4; ++k)
			data[i].f[k] = temp[i * components + k];
	}
}

static void fixupIndices(std::vector<unsigned int>& indices, cgltf_primitive_type& type)
{
	if (type == cgltf_primitive_type_line_loop)
	{
		std::vector<unsigned int> result;
		result.reserve(indices.size() * 2 + 2);

		for (size_t i = 1; i <= indices.size(); ++i)
		{
			result.push_back(indices[i - 1]);
			result.push_back(indices[i % indices.size()]);
		}

		indices.swap(result);
		type = cgltf_primitive_type_lines;
	}
	else if (type == cgltf_primitive_type_line_strip)
	{
		std::vector<unsigned int> result;
		result.reserve(indices.size() * 2);

		for (size_t i = 1; i < indices.size(); ++i)
		{
			result.push_back(indices[i - 1]);
			result.push_back(indices[i]);
		}

		indices.swap(result);
		type = cgltf_primitive_type_lines;
	}
	else if (type == cgltf_primitive_type_triangle_strip)
	{
		std::vector<unsigned int> result;
		result.reserve(indices.size() * 3);

		for (size_t i = 2; i < indices.size(); ++i)
		{
			int flip = i & 1;

			result.push_back(indices[i - 2 + flip]);
			result.push_back(indices[i - 1 - flip]);
			result.push_back(indices[i]);
		}

		indices.swap(result);
		type = cgltf_primitive_type_triangles;
	}
	else if (type == cgltf_primitive_type_triangle_fan)
	{
		std::vector<unsigned int> result;
		result.reserve(indices.size() * 3);

		for (size_t i = 2; i < indices.size(); ++i)
		{
			result.push_back(indices[0]);
			result.push_back(indices[i - 1]);
			result.push_back(indices[i]);
		}

		indices.swap(result);
		type = cgltf_primitive_type_triangles;
	}
}

static void parseMeshesGltf(cgltf_data* data, std::vector<Mesh>& meshes, std::vector<std::pair<size_t, size_t> >& mesh_remap)
{
	size_t total_primitives = 0;

	for (size_t mi = 0; mi < data->meshes_count; ++mi)
		total_primitives += data->meshes[mi].primitives_count;

	meshes.reserve(total_primitives);
	mesh_remap.resize(data->meshes_count);

	for (size_t mi = 0; mi < data->meshes_count; ++mi)
	{
		const cgltf_mesh& mesh = data->meshes[mi];

		size_t remap_offset = meshes.size();

		for (size_t pi = 0; pi < mesh.primitives_count; ++pi)
		{
			const cgltf_primitive& primitive = mesh.primitives[pi];

			if (primitive.type == cgltf_primitive_type_points && primitive.indices)
			{
				fprintf(stderr, "Warning: ignoring primitive %d of mesh %d because indexed points are not supported\n", int(pi), int(mi));
				continue;
			}

			meshes.push_back(Mesh());
			Mesh& result = meshes.back();

			result.scene = -1;

			result.material = primitive.material;

			result.type = primitive.type;

			result.streams.reserve(primitive.attributes_count);

			if (primitive.indices)
			{
				result.indices.resize(primitive.indices->count);
				for (size_t i = 0; i < primitive.indices->count; ++i)
					result.indices[i] = unsigned(cgltf_accessor_read_index(primitive.indices, i));
			}
			else if (primitive.type != cgltf_primitive_type_points)
			{
				size_t count = primitive.attributes ? primitive.attributes[0].data->count : 0;

				// note, while we could generate a good index buffer, reindexMesh will take care of this
				result.indices.resize(count);
				for (size_t i = 0; i < count; ++i)
					result.indices[i] = unsigned(i);
			}

			fixupIndices(result.indices, result.type);

			for (size_t ai = 0; ai < primitive.attributes_count; ++ai)
			{
				const cgltf_attribute& attr = primitive.attributes[ai];

				if (attr.type == cgltf_attribute_type_invalid)
				{
					fprintf(stderr, "Warning: ignoring unknown attribute %s in primitive %d of mesh %d\n", attr.name, int(pi), int(mi));
					continue;
				}

				result.streams.push_back(Stream());
				Stream& s = result.streams.back();

				s.type = attr.type;
				s.index = attr.index;

				readAccessor(s.data, attr.data);

				if (attr.type == cgltf_attribute_type_color && attr.data->type == cgltf_type_vec3)
				{
					for (size_t i = 0; i < s.data.size(); ++i)
						s.data[i].f[3] = 1.0f;
				}
			}

			for (size_t ti = 0; ti < primitive.targets_count; ++ti)
			{
				const cgltf_morph_target& target = primitive.targets[ti];

				for (size_t ai = 0; ai < target.attributes_count; ++ai)
				{
					const cgltf_attribute& attr = target.attributes[ai];

					if (attr.type == cgltf_attribute_type_invalid)
					{
						fprintf(stderr, "Warning: ignoring unknown attribute %s in morph target %d of primitive %d of mesh %d\n", attr.name, int(ti), int(pi), int(mi));
						continue;
					}

					result.streams.push_back(Stream());
					Stream& s = result.streams.back();

					s.type = attr.type;
					s.index = attr.index;
					s.target = int(ti + 1);

					readAccessor(s.data, attr.data);
				}
			}

			result.targets = primitive.targets_count;
			result.target_weights.assign(mesh.weights, mesh.weights + mesh.weights_count);
			result.target_names.assign(mesh.target_names, mesh.target_names + mesh.target_names_count);
		}

		mesh_remap[mi] = std::make_pair(remap_offset, meshes.size());
	}
}

static void parseMeshNodesGltf(cgltf_data* data, std::vector<Mesh>& meshes, const std::vector<std::pair<size_t, size_t> >& mesh_remap)
{
	for (size_t i = 0; i < data->nodes_count; ++i)
	{
		cgltf_node& node = data->nodes[i];
		if (!node.mesh)
			continue;

		std::pair<size_t, size_t> range = mesh_remap[node.mesh - data->meshes];

		for (size_t mi = range.first; mi < range.second; ++mi)
		{
			Mesh* mesh = &meshes[mi];

			if (!mesh->nodes.empty() && mesh->skin != node.skin)
			{
				// this should be extremely rare - if the same mesh is used with different skins, we need to duplicate it
				// in this case we don't spend any effort on keeping the number of duplicates to the minimum, because this
				// should really never happen.
				meshes.push_back(*mesh);
				mesh = &meshes.back();
			}

			mesh->nodes.push_back(&node);
			mesh->skin = node.skin;
		}
	}

	for (size_t i = 0; i < meshes.size(); ++i)
	{
		Mesh& mesh = meshes[i];

		// because the rest of gltfpack assumes that empty nodes array = world-space mesh, we need to filter unused meshes
		if (mesh.nodes.empty())
		{
			mesh.streams.clear();
			mesh.indices.clear();
		}
	}
}

static void parseAnimationsGltf(cgltf_data* data, std::vector<Animation>& animations)
{
	animations.reserve(data->animations_count);

	for (size_t i = 0; i < data->animations_count; ++i)
	{
		const cgltf_animation& animation = data->animations[i];

		animations.push_back(Animation());
		Animation& result = animations.back();

		result.name = animation.name;

		result.tracks.reserve(animation.channels_count);

		for (size_t j = 0; j < animation.channels_count; ++j)
		{
			const cgltf_animation_channel& channel = animation.channels[j];

			if (!channel.target_node)
			{
				fprintf(stderr, "Warning: ignoring channel %d of animation %d because it has no target node\n", int(j), int(i));
				continue;
			}

			result.tracks.push_back(Track());
			Track& track = result.tracks.back();

			track.node = channel.target_node;
			track.path = channel.target_path;

			track.components = (channel.target_path == cgltf_animation_path_type_weights) ? track.node->mesh->primitives[0].targets_count : 1;

			track.interpolation = channel.sampler->interpolation;

			readAccessor(track.time, channel.sampler->input);
			readAccessor(track.data, channel.sampler->output);
		}

		if (result.tracks.empty())
		{
			fprintf(stderr, "Warning: ignoring animation %d because it has no valid tracks\n", int(i));
			animations.pop_back();
		}
	}
}

static bool requiresExtension(cgltf_data* data, const char* name)
{
	for (size_t i = 0; i < data->extensions_required_count; ++i)
		if (strcmp(data->extensions_required[i], name) == 0)
			return true;

	return false;
}

static bool needsDummyBuffers(cgltf_data* data)
{
	for (size_t i = 0; i < data->accessors_count; ++i)
	{
		cgltf_accessor* accessor = &data->accessors[i];

		if (accessor->buffer_view && accessor->buffer_view->buffer->data == NULL)
			return true;

		if (accessor->is_sparse)
		{
			cgltf_accessor_sparse* sparse = &accessor->sparse;

			if (sparse->indices_buffer_view->buffer->data == NULL)
				return true;
			if (sparse->values_buffer_view->buffer->data == NULL)
				return true;
		}
	}

	for (size_t i = 0; i < data->images_count; ++i)
	{
		cgltf_image* image = &data->images[i];

		if (image->buffer_view && image->buffer_view->buffer->data == NULL)
			return true;
	}

	return false;
}

static void evacuateExtras(cgltf_data* data, std::string& extras, cgltf_extras& item)
{
	size_t offset = extras.size();

	extras.append(data->json + item.start_offset, item.end_offset - item.start_offset);

	item.start_offset = offset;
	item.end_offset = extras.size();
}

static void evacuateExtras(cgltf_data* data, std::string& extras)
{
	size_t size = 0;

	size += data->asset.extras.end_offset - data->asset.extras.start_offset;

	for (size_t i = 0; i < data->materials_count; ++i)
		size += data->materials[i].extras.end_offset - data->materials[i].extras.start_offset;

	for (size_t i = 0; i < data->nodes_count; ++i)
		size += data->nodes[i].extras.end_offset - data->nodes[i].extras.start_offset;

	extras.reserve(size);

	evacuateExtras(data, extras, data->asset.extras);

	for (size_t i = 0; i < data->materials_count; ++i)
		evacuateExtras(data, extras, data->materials[i].extras);

	for (size_t i = 0; i < data->nodes_count; ++i)
		evacuateExtras(data, extras, data->nodes[i].extras);
}

static void freeFile(cgltf_data* data)
{
	data->json = NULL;
	data->bin = NULL;

	free(data->file_data);
	data->file_data = NULL;
}

static bool freeUnusedBuffers(cgltf_data* data)
{
	std::vector<char> used(data->buffers_count);

	for (size_t i = 0; i < data->skins_count; ++i)
	{
		const cgltf_skin& skin = data->skins[i];

		if (skin.inverse_bind_matrices && skin.inverse_bind_matrices->buffer_view)
		{
			assert(skin.inverse_bind_matrices->buffer_view->buffer);
			used[skin.inverse_bind_matrices->buffer_view->buffer - data->buffers] = 1;
		}
	}

	for (size_t i = 0; i < data->images_count; ++i)
	{
		const cgltf_image& image = data->images[i];

		if (image.buffer_view)
		{
			assert(image.buffer_view->buffer);
			used[image.buffer_view->buffer - data->buffers] = 1;
		}
	}

	bool free_bin = false;

	for (size_t i = 0; i < data->buffers_count; ++i)
	{
		cgltf_buffer& buffer = data->buffers[i];

		if (!used[i] && buffer.data)
		{
			if (buffer.data != data->bin)
				free(buffer.data);
			else
				free_bin = true;

			buffer.data = NULL;
		}
	}

	return free_bin;
}

cgltf_data* parseGltf(const char* path, std::vector<Mesh>& meshes, std::vector<Animation>& animations, std::string& extras, const char** error)
{
	cgltf_data* data = 0;

	cgltf_options options = {};
	cgltf_result result = cgltf_parse_file(&options, path, &data);

	if (data)
	{
		evacuateExtras(data, extras);

		if (!data->bin)
			freeFile(data);
	}

	result = (result == cgltf_result_success) ? cgltf_load_buffers(&options, data, path) : result;
	result = (result == cgltf_result_success) ? cgltf_validate(data) : result;

	*error = NULL;

	if (result != cgltf_result_success)
		*error = getError(result, data);
	else if (requiresExtension(data, "KHR_draco_mesh_compression"))
		*error = "file requires Draco mesh compression support";
	else if (requiresExtension(data, "EXT_meshopt_compression"))
		*error = "file has already been compressed using gltfpack";
	else if (requiresExtension(data, "KHR_texture_basisu"))
		*error = "file requires BasisU texture support";
	else if (requiresExtension(data, "EXT_mesh_gpu_instancing"))
		*error = "file requires mesh instancing support";
	else if (needsDummyBuffers(data))
		*error = "buffer has no data";

	if (*error)
	{
		cgltf_free(data);
		return 0;
	}

	if (requiresExtension(data, "KHR_mesh_quantization"))
		fprintf(stderr, "Warning: file uses quantized geometry; repacking may result in increased quantization error\n");

	std::vector<std::pair<size_t, size_t> > mesh_remap;

	parseMeshesGltf(data, meshes, mesh_remap);
	parseMeshNodesGltf(data, meshes, mesh_remap);
	parseAnimationsGltf(data, animations);

	bool free_bin = freeUnusedBuffers(data);

	if (data->bin && free_bin)
		freeFile(data);

	return data;
}
