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

#include <algorithm>

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

#include "../src/meshoptimizer.h"

#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif

std::string getVersion()
{
	char result[32];
	sprintf(result, "%d.%d", MESHOPTIMIZER_VERSION / 1000, (MESHOPTIMIZER_VERSION % 1000) / 10);
	return result;
}

static void finalizeBufferViews(std::string& json, std::vector<BufferView>& views, std::string& bin, std::string* fallback, size_t& fallback_size)
{
	for (size_t i = 0; i < views.size(); ++i)
	{
		BufferView& view = views[i];

		size_t bin_offset = bin.size();
		size_t fallback_offset = fallback_size;

		size_t count = view.data.size() / view.stride;

		if (view.compression == BufferView::Compression_None)
		{
			bin += view.data;
		}
		else
		{
			switch (view.compression)
			{
			case BufferView::Compression_Attribute:
				compressVertexStream(bin, view.data, count, view.stride);
				break;
			case BufferView::Compression_Index:
				compressIndexStream(bin, view.data, count, view.stride);
				break;
			case BufferView::Compression_IndexSequence:
				compressIndexSequence(bin, view.data, count, view.stride);
				break;
			default:
				assert(!"Unknown compression type");
			}

			if (fallback)
				*fallback += view.data;
			fallback_size += view.data.size();
		}

		size_t raw_offset = (view.compression != BufferView::Compression_None) ? fallback_offset : bin_offset;

		comma(json);
		writeBufferView(json, view.kind, view.filter, count, view.stride, raw_offset, view.data.size(), view.compression, bin_offset, bin.size() - bin_offset);

		// record written bytes for statistics
		view.bytes = bin.size() - bin_offset;

		// align each bufferView by 4 bytes
		bin.resize((bin.size() + 3) & ~3);
		if (fallback)
			fallback->resize((fallback->size() + 3) & ~3);
		fallback_size = (fallback_size + 3) & ~3;
	}
}

static void printMeshStats(const std::vector<Mesh>& meshes, const char* name)
{
	size_t mesh_triangles = 0;
	size_t mesh_vertices = 0;
	size_t total_triangles = 0;
	size_t total_instances = 0;
	size_t total_draws = 0;

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

		mesh_triangles += mesh.indices.size() / 3;
		mesh_vertices += mesh.streams.empty() ? 0 : mesh.streams[0].data.size();

		size_t instances = std::max(size_t(1), mesh.nodes.size() + mesh.instances.size());

		total_triangles += mesh.indices.size() / 3 * instances;
		total_instances += instances;
		total_draws += std::max(size_t(1), mesh.nodes.size());
	}

	printf("%s: %d mesh primitives (%d triangles, %d vertices); %d draw calls (%d instances, %lld triangles)\n", name,
	       int(meshes.size()), int(mesh_triangles), int(mesh_vertices),
	       int(total_draws), int(total_instances), (long long)total_triangles);
}

static void printSceneStats(const std::vector<BufferView>& views, const std::vector<Mesh>& meshes, size_t node_offset, size_t mesh_offset, size_t material_offset, size_t json_size, size_t bin_size)
{
	size_t bytes[BufferView::Kind_Count] = {};

	for (size_t i = 0; i < views.size(); ++i)
	{
		const BufferView& view = views[i];
		bytes[view.kind] += view.bytes;
	}

	printf("output: %d nodes, %d meshes (%d primitives), %d materials\n", int(node_offset), int(mesh_offset), int(meshes.size()), int(material_offset));
	printf("output: JSON %d bytes, buffers %d bytes\n", int(json_size), int(bin_size));
	printf("output: buffers: vertex %d bytes, index %d bytes, skin %d bytes, time %d bytes, keyframe %d bytes, instance %d bytes, image %d bytes\n",
	       int(bytes[BufferView::Kind_Vertex]), int(bytes[BufferView::Kind_Index]), int(bytes[BufferView::Kind_Skin]),
	       int(bytes[BufferView::Kind_Time]), int(bytes[BufferView::Kind_Keyframe]), int(bytes[BufferView::Kind_Instance]),
	       int(bytes[BufferView::Kind_Image]));
}

static void printAttributeStats(const std::vector<BufferView>& views, BufferView::Kind kind, const char* name)
{
	for (size_t i = 0; i < views.size(); ++i)
	{
		const BufferView& view = views[i];

		if (view.kind != kind)
			continue;

		const char* variant = "unknown";

		switch (kind)
		{
		case BufferView::Kind_Vertex:
			variant = attributeType(cgltf_attribute_type(view.variant));
			break;

		case BufferView::Kind_Index:
			variant = "index";
			break;

		case BufferView::Kind_Keyframe:
		case BufferView::Kind_Instance:
			variant = animationPath(cgltf_animation_path_type(view.variant));
			break;

		default:;
		}

		size_t count = view.data.size() / view.stride;

		printf("stats: %s %s: compressed %d bytes (%.1f bits), raw %d bytes (%d bits)\n",
		       name, variant,
		       int(view.bytes), double(view.bytes) / double(count) * 8,
		       int(view.data.size()), int(view.stride * 8));
	}
}

static void process(cgltf_data* data, const char* input_path, const char* output_path, std::vector<Mesh>& meshes, std::vector<Animation>& animations, const std::string& extras, const Settings& settings, std::string& json, std::string& bin, std::string& fallback, size_t& fallback_size)
{
	if (settings.verbose)
	{
		printf("input: %d nodes, %d meshes (%d primitives), %d materials, %d skins, %d animations\n",
		       int(data->nodes_count), int(data->meshes_count), int(meshes.size()), int(data->materials_count), int(data->skins_count), int(animations.size()));
		printMeshStats(meshes, "input");
	}

	for (size_t i = 0; i < animations.size(); ++i)
	{
		processAnimation(animations[i], settings);
	}

	std::vector<NodeInfo> nodes(data->nodes_count);

	markAnimated(data, nodes, animations);

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

		// mesh is already world space, skip
		if (mesh.nodes.empty())
			continue;

		// note: when -kn is specified, we keep mesh-node attachment so that named nodes can be transformed
		if (settings.keep_nodes)
			continue;

		// we keep skinned meshes or meshes with morph targets as is
		// in theory we could transform both, but in practice transforming morph target meshes is more involved,
		// and reparenting skinned meshes leads to incorrect bounding box generated in three.js
		if (mesh.skin || mesh.targets)
			continue;

		bool any_animated = false;
		for (size_t j = 0; j < mesh.nodes.size(); ++j)
			any_animated |= nodes[mesh.nodes[j] - data->nodes].animated;

		// animated meshes will be anchored to the same node that they used to be in to retain the animation
		if (any_animated)
			continue;

		// we only merge multiple instances together if requested
		// this often makes the scenes faster to render by reducing the draw call count, but can result in larger files
		if (mesh.nodes.size() > 1 && !settings.mesh_merge && !settings.mesh_instancing)
			continue;

		// prefer instancing if possible, use merging otherwise
		if (mesh.nodes.size() > 1 && settings.mesh_instancing)
		{
			mesh.instances.resize(mesh.nodes.size());

			for (size_t j = 0; j < mesh.nodes.size(); ++j)
				cgltf_node_transform_world(mesh.nodes[j], mesh.instances[j].data);

			mesh.nodes.clear();
		}
		else
		{
			mergeMeshInstances(mesh);
		}

		assert(mesh.nodes.empty());
	}

	mergeMeshMaterials(data, meshes, settings);
	mergeMeshes(meshes, settings);
	filterEmptyMeshes(meshes);

	markNeededNodes(data, nodes, meshes, animations, settings);

	std::vector<MaterialInfo> materials(data->materials_count);

	markNeededMaterials(data, materials, meshes, settings);

#ifndef NDEBUG
	std::vector<Mesh> debug_meshes;

	for (size_t i = 0; i < meshes.size(); ++i)
	{
		if (settings.simplify_debug > 0)
		{
			Mesh kinds = {};
			Mesh loops = {};
			debugSimplify(meshes[i], kinds, loops, settings.simplify_debug);
			debug_meshes.push_back(kinds);
			debug_meshes.push_back(loops);
		}

		if (settings.meshlet_debug > 0)
		{
			Mesh meshlets = {};
			Mesh bounds = {};
			debugMeshlets(meshes[i], meshlets, bounds, settings.meshlet_debug);
			debug_meshes.push_back(meshlets);
			debug_meshes.push_back(bounds);
		}
	}
#endif

	for (size_t i = 0; i < meshes.size(); ++i)
	{
		processMesh(meshes[i], settings);
	}

#ifndef NDEBUG
	meshes.insert(meshes.end(), debug_meshes.begin(), debug_meshes.end());
#endif

	filterEmptyMeshes(meshes); // some meshes may become empty after processing

	std::vector<ImageInfo> images(data->images_count);

	analyzeImages(data, images);

	QuantizationPosition qp = prepareQuantizationPosition(meshes, settings);

	std::vector<QuantizationTexture> qt_materials(materials.size());
	prepareQuantizationTexture(data, qt_materials, meshes, settings);

	QuantizationTexture qt_dummy = {};
	qt_dummy.bits = settings.tex_bits;

	std::string json_images;
	std::string json_textures;
	std::string json_materials;
	std::string json_accessors;
	std::string json_meshes;
	std::string json_nodes;
	std::string json_skins;
	std::string json_roots;
	std::string json_animations;
	std::string json_cameras;
	std::string json_lights;

	std::vector<BufferView> views;

	bool ext_pbr_specular_glossiness = false;
	bool ext_clearcoat = false;
	bool ext_transmission = false;
	bool ext_unlit = false;
	bool ext_instancing = false;

	size_t accr_offset = 0;
	size_t node_offset = 0;
	size_t mesh_offset = 0;
	size_t material_offset = 0;

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

		if (settings.verbose == 1 && settings.texture_ktx2)
		{
			const char* uri = image.uri;
			bool embedded = !uri || strncmp(uri, "data:", 5) == 0;

			printf("image %d (%s) is being encoded with %s\n", int(i), embedded ? "embedded" : uri, settings.texture_toktx ? "toktx" : "basisu");
		}

		comma(json_images);
		append(json_images, "{");
		writeImage(json_images, views, image, images[i], i, input_path, output_path, settings);
		append(json_images, "}");
	}

	for (size_t i = 0; i < data->textures_count; ++i)
	{
		const cgltf_texture& texture = data->textures[i];

		comma(json_textures);
		append(json_textures, "{");
		writeTexture(json_textures, texture, data, settings);
		append(json_textures, "}");
	}

	for (size_t i = 0; i < data->materials_count; ++i)
	{
		MaterialInfo& mi = materials[i];

		if (!mi.keep)
			continue;

		const cgltf_material& material = data->materials[i];

		comma(json_materials);
		append(json_materials, "{");
		writeMaterial(json_materials, data, material, settings.quantize ? &qt_materials[i] : NULL);
		if (settings.keep_extras)
			writeExtras(json_materials, extras, material.extras);
		append(json_materials, "}");

		mi.remap = int(material_offset);
		material_offset++;

		ext_pbr_specular_glossiness = ext_pbr_specular_glossiness || material.has_pbr_specular_glossiness;
		ext_clearcoat = ext_clearcoat || material.has_clearcoat;
		ext_transmission = ext_transmission || material.has_transmission;
		ext_unlit = ext_unlit || material.unlit;
	}

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

		comma(json_meshes);
		append(json_meshes, "{\"primitives\":[");

		size_t pi = i;
		for (; pi < meshes.size(); ++pi)
		{
			const Mesh& prim = meshes[pi];

			if (prim.skin != mesh.skin || prim.targets != mesh.targets)
				break;

			if (pi > i && (mesh.instances.size() || prim.instances.size()))
				break;

			if (!compareMeshNodes(mesh, prim))
				break;

			if (!compareMeshTargets(mesh, prim))
				break;

			const QuantizationTexture& qt = prim.material ? qt_materials[prim.material - data->materials] : qt_dummy;

			comma(json_meshes);
			append(json_meshes, "{\"attributes\":{");
			writeMeshAttributes(json_meshes, views, json_accessors, accr_offset, prim, 0, qp, qt, settings);
			append(json_meshes, "}");
			append(json_meshes, ",\"mode\":");
			append(json_meshes, size_t(prim.type));

			if (mesh.targets)
			{
				append(json_meshes, ",\"targets\":[");
				for (size_t j = 0; j < mesh.targets; ++j)
				{
					comma(json_meshes);
					append(json_meshes, "{");
					writeMeshAttributes(json_meshes, views, json_accessors, accr_offset, prim, int(1 + j), qp, qt, settings);
					append(json_meshes, "}");
				}
				append(json_meshes, "]");
			}

			if (!prim.indices.empty())
			{
				size_t index_accr = writeMeshIndices(views, json_accessors, accr_offset, prim, settings);

				append(json_meshes, ",\"indices\":");
				append(json_meshes, index_accr);
			}

			if (prim.material)
			{
				MaterialInfo& mi = materials[prim.material - data->materials];

				assert(mi.keep);
				append(json_meshes, ",\"material\":");
				append(json_meshes, size_t(mi.remap));
			}

			append(json_meshes, "}");
		}

		append(json_meshes, "]");

		if (mesh.target_weights.size())
		{
			append(json_meshes, ",\"weights\":[");
			for (size_t j = 0; j < mesh.target_weights.size(); ++j)
			{
				comma(json_meshes);
				append(json_meshes, mesh.target_weights[j]);
			}
			append(json_meshes, "]");
		}

		if (mesh.target_names.size())
		{
			append(json_meshes, ",\"extras\":{\"targetNames\":[");
			for (size_t j = 0; j < mesh.target_names.size(); ++j)
			{
				comma(json_meshes);
				append(json_meshes, "\"");
				append(json_meshes, mesh.target_names[j]);
				append(json_meshes, "\"");
			}
			append(json_meshes, "]}");
		}

		append(json_meshes, "}");

		assert(mesh.nodes.empty() || mesh.instances.empty());
		ext_instancing = ext_instancing || !mesh.instances.empty();

		if (mesh.nodes.size())
		{
			for (size_t j = 0; j < mesh.nodes.size(); ++j)
			{
				NodeInfo& ni = nodes[mesh.nodes[j] - data->nodes];

				assert(ni.keep);
				ni.meshes.push_back(node_offset);

				writeMeshNode(json_nodes, mesh_offset, mesh.nodes[j], mesh.skin, data, settings.quantize ? &qp : NULL);

				node_offset++;
			}
		}
		else if (mesh.instances.size())
		{
			comma(json_roots);
			append(json_roots, node_offset);

			size_t instance_accr = writeInstances(views, json_accessors, accr_offset, mesh.instances, qp, settings);

			assert(!mesh.skin);
			writeMeshNodeInstanced(json_nodes, mesh_offset, instance_accr);

			node_offset++;
		}
		else
		{
			comma(json_roots);
			append(json_roots, node_offset);

			writeMeshNode(json_nodes, mesh_offset, NULL, mesh.skin, data, settings.quantize ? &qp : NULL);

			node_offset++;
		}

		mesh_offset++;

		// skip all meshes that we've written in this iteration
		assert(pi > i);
		i = pi - 1;
	}

	remapNodes(data, nodes, node_offset);

	for (size_t i = 0; i < data->nodes_count; ++i)
	{
		NodeInfo& ni = nodes[i];

		if (!ni.keep)
			continue;

		const cgltf_node& node = data->nodes[i];

		if (!node.parent)
		{
			comma(json_roots);
			append(json_roots, size_t(ni.remap));
		}

		comma(json_nodes);
		append(json_nodes, "{");
		writeNode(json_nodes, node, nodes, data);
		if (settings.keep_extras)
			writeExtras(json_nodes, extras, node.extras);
		append(json_nodes, "}");
	}

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

		size_t matrix_accr = writeJointBindMatrices(views, json_accessors, accr_offset, skin, qp, settings);

		writeSkin(json_skins, skin, matrix_accr, nodes, data);
	}

	for (size_t i = 0; i < animations.size(); ++i)
	{
		const Animation& animation = animations[i];

		writeAnimation(json_animations, views, json_accessors, accr_offset, animation, i, data, nodes, settings);
	}

	for (size_t i = 0; i < data->cameras_count; ++i)
	{
		const cgltf_camera& camera = data->cameras[i];

		writeCamera(json_cameras, camera);
	}

	for (size_t i = 0; i < data->lights_count; ++i)
	{
		const cgltf_light& light = data->lights[i];

		writeLight(json_lights, light);
	}

	append(json, "\"asset\":{");
	append(json, "\"version\":\"2.0\",\"generator\":\"gltfpack ");
	append(json, getVersion());
	append(json, "\"");
	writeExtras(json, extras, data->asset.extras);
	append(json, "}");

	const ExtensionInfo extensions[] = {
	    {"KHR_mesh_quantization", settings.quantize, true},
	    {"EXT_meshopt_compression", settings.compress, !settings.fallback},
	    {"KHR_texture_transform", settings.quantize && !json_textures.empty(), false},
	    {"KHR_materials_pbrSpecularGlossiness", ext_pbr_specular_glossiness, false},
	    {"KHR_materials_clearcoat", ext_clearcoat, false},
	    {"KHR_materials_transmission", ext_transmission, false},
	    {"KHR_materials_unlit", ext_unlit, false},
	    {"KHR_lights_punctual", data->lights_count > 0, false},
	    {"KHR_texture_basisu", !json_textures.empty() && settings.texture_ktx2, true},
	    {"EXT_mesh_gpu_instancing", ext_instancing, true},
	};

	writeExtensions(json, extensions, sizeof(extensions) / sizeof(extensions[0]));

	std::string json_views;
	finalizeBufferViews(json_views, views, bin, settings.fallback ? &fallback : NULL, fallback_size);

	writeArray(json, "bufferViews", json_views);
	writeArray(json, "accessors", json_accessors);
	writeArray(json, "images", json_images);
	writeArray(json, "textures", json_textures);
	writeArray(json, "materials", json_materials);
	writeArray(json, "meshes", json_meshes);
	writeArray(json, "skins", json_skins);
	writeArray(json, "animations", json_animations);
	writeArray(json, "nodes", json_nodes);

	if (!json_roots.empty())
	{
		append(json, ",\"scenes\":[");
		append(json, "{\"nodes\":[");
		append(json, json_roots);
		append(json, "]}]");
	}

	writeArray(json, "cameras", json_cameras);

	if (!json_lights.empty())
	{
		append(json, ",\"extensions\":{\"KHR_lights_punctual\":{\"lights\":[");
		append(json, json_lights);
		append(json, "]}}");
	}
	if (!json_roots.empty())
	{
		append(json, ",\"scene\":0");
	}

	if (settings.verbose)
	{
		printMeshStats(meshes, "output");
		printSceneStats(views, meshes, node_offset, mesh_offset, material_offset, json.size(), bin.size());
	}

	if (settings.verbose > 1)
	{
		printAttributeStats(views, BufferView::Kind_Vertex, "vertex");
		printAttributeStats(views, BufferView::Kind_Index, "index");
		printAttributeStats(views, BufferView::Kind_Keyframe, "keyframe");
		printAttributeStats(views, BufferView::Kind_Instance, "instance");
	}
}

static void writeU32(FILE* out, uint32_t data)
{
	fwrite(&data, 4, 1, out);
}

static const char* getBaseName(const char* path)
{
	const char* slash = strrchr(path, '/');
	const char* backslash = strrchr(path, '\\');

	const char* rs = slash ? slash + 1 : path;
	const char* bs = backslash ? backslash + 1 : path;

	return std::max(rs, bs);
}

static std::string getBufferSpec(const char* bin_path, size_t bin_size, const char* fallback_path, size_t fallback_size, bool fallback_ref)
{
	std::string json;
	append(json, "\"buffers\":[");
	append(json, "{");
	if (bin_path)
	{
		append(json, "\"uri\":\"");
		append(json, bin_path);
		append(json, "\"");
	}
	comma(json);
	append(json, "\"byteLength\":");
	append(json, bin_size);
	append(json, "}");
	if (fallback_ref)
	{
		comma(json);
		append(json, "{");
		if (fallback_path)
		{
			append(json, "\"uri\":\"");
			append(json, fallback_path);
			append(json, "\"");
		}
		comma(json);
		append(json, "\"byteLength\":");
		append(json, fallback_size);
		append(json, ",\"extensions\":{");
		append(json, "\"EXT_meshopt_compression\":{");
		append(json, "\"fallback\":true");
		append(json, "}}");
		append(json, "}");
	}
	append(json, "]");

	return json;
}

int gltfpack(const char* input, const char* output, const Settings& settings)
{
	cgltf_data* data = 0;
	std::vector<Mesh> meshes;
	std::vector<Animation> animations;
	std::string extras;

	const char* iext = strrchr(input, '.');

	if (iext && (strcmp(iext, ".gltf") == 0 || strcmp(iext, ".GLTF") == 0 || strcmp(iext, ".glb") == 0 || strcmp(iext, ".GLB") == 0))
	{
		const char* error = 0;
		data = parseGltf(input, meshes, animations, extras, &error);

		if (error)
		{
			fprintf(stderr, "Error loading %s: %s\n", input, error);
			return 2;
		}
	}
	else if (iext && (strcmp(iext, ".obj") == 0 || strcmp(iext, ".OBJ") == 0))
	{
		const char* error = 0;
		data = parseObj(input, meshes, &error);

		if (!data)
		{
			fprintf(stderr, "Error loading %s: %s\n", input, error);
			return 2;
		}
	}
	else
	{
		fprintf(stderr, "Error loading %s: unknown extension (expected .gltf or .glb or .obj)\n", input);
		return 2;
	}

	if (data->images_count && settings.texture_ktx2)
	{
		if (checkKtx(settings.verbose > 1))
		{
			settings.texture_toktx = true;
		}
		else if (!checkBasis(settings.verbose > 1))
		{
			fprintf(stderr, "Error: toktx is not present in PATH or TOKTX_PATH is not set\n");
			return 3;
		}

		if (settings.texture_scale < 1 && !settings.texture_toktx)
		{
			fprintf(stderr, "Error: -ts option is only supported by toktx\n");
			return 3;
		}
	}

	std::string json, bin, fallback;
	size_t fallback_size = 0;
	process(data, input, output, meshes, animations, extras, settings, json, bin, fallback, fallback_size);

	cgltf_free(data);

	if (!output)
	{
		return 0;
	}

	const char* oext = strrchr(output, '.');

	if (oext && (strcmp(oext, ".gltf") == 0 || strcmp(oext, ".GLTF") == 0))
	{
		std::string binpath = output;
		binpath.replace(binpath.size() - 5, 5, ".bin");

		std::string fbpath = output;
		fbpath.replace(fbpath.size() - 5, 5, ".fallback.bin");

		FILE* outjson = fopen(output, "wb");
		FILE* outbin = fopen(binpath.c_str(), "wb");
		FILE* outfb = settings.fallback ? fopen(fbpath.c_str(), "wb") : NULL;
		if (!outjson || !outbin || (!outfb && settings.fallback))
		{
			fprintf(stderr, "Error saving %s\n", output);
			return 4;
		}

		std::string bufferspec = getBufferSpec(getBaseName(binpath.c_str()), bin.size(), settings.fallback ? getBaseName(fbpath.c_str()) : NULL, fallback_size, settings.compress);

		fprintf(outjson, "{");
		fwrite(bufferspec.c_str(), bufferspec.size(), 1, outjson);
		fprintf(outjson, ",");
		fwrite(json.c_str(), json.size(), 1, outjson);
		fprintf(outjson, "}");

		fwrite(bin.c_str(), bin.size(), 1, outbin);

		if (settings.fallback)
			fwrite(fallback.c_str(), fallback.size(), 1, outfb);

		fclose(outjson);
		fclose(outbin);
		if (outfb)
			fclose(outfb);
	}
	else if (oext && (strcmp(oext, ".glb") == 0 || strcmp(oext, ".GLB") == 0))
	{
		std::string fbpath = output;
		fbpath.replace(fbpath.size() - 4, 4, ".fallback.bin");

		FILE* out = fopen(output, "wb");
		FILE* outfb = settings.fallback ? fopen(fbpath.c_str(), "wb") : NULL;
		if (!out || (!outfb && settings.fallback))
		{
			fprintf(stderr, "Error saving %s\n", output);
			return 4;
		}

		std::string bufferspec = getBufferSpec(NULL, bin.size(), settings.fallback ? getBaseName(fbpath.c_str()) : NULL, fallback_size, settings.compress);

		json.insert(0, "{" + bufferspec + ",");
		json.push_back('}');

		while (json.size() % 4)
			json.push_back(' ');

		while (bin.size() % 4)
			bin.push_back('\0');

		writeU32(out, 0x46546C67);
		writeU32(out, 2);
		writeU32(out, uint32_t(12 + 8 + json.size() + 8 + bin.size()));

		writeU32(out, uint32_t(json.size()));
		writeU32(out, 0x4E4F534A);
		fwrite(json.c_str(), json.size(), 1, out);

		writeU32(out, uint32_t(bin.size()));
		writeU32(out, 0x004E4942);
		fwrite(bin.c_str(), bin.size(), 1, out);

		if (settings.fallback)
			fwrite(fallback.c_str(), fallback.size(), 1, outfb);

		fclose(out);
		if (outfb)
			fclose(outfb);
	}
	else
	{
		fprintf(stderr, "Error saving %s: unknown extension (expected .gltf or .glb)\n", output);
		return 4;
	}

	return 0;
}

int main(int argc, char** argv)
{
	meshopt_encodeIndexVersion(1);

	Settings settings = {};
	settings.quantize = true;
	settings.pos_bits = 14;
	settings.tex_bits = 12;
	settings.nrm_bits = 8;
	settings.trn_bits = 16;
	settings.rot_bits = 12;
	settings.scl_bits = 16;
	settings.anim_freq = 30;
	settings.simplify_threshold = 1.f;
	settings.texture_quality = 8;
	settings.texture_scale = 1.f;

	const char* input = 0;
	const char* output = 0;
	bool help = false;
	bool test = false;

	std::vector<const char*> testinputs;

	for (int i = 1; i < argc; ++i)
	{
		const char* arg = argv[i];

		if (strcmp(arg, "-vp") == 0 && i + 1 < argc && isdigit(argv[i + 1][0]))
		{
			settings.pos_bits = atoi(argv[++i]);
		}
		else if (strcmp(arg, "-vt") == 0 && i + 1 < argc && isdigit(argv[i + 1][0]))
		{
			settings.tex_bits = atoi(argv[++i]);
		}
		else if (strcmp(arg, "-vn") == 0 && i + 1 < argc && isdigit(argv[i + 1][0]))
		{
			settings.nrm_bits = atoi(argv[++i]);
		}
		else if (strcmp(arg, "-at") == 0 && i + 1 < argc && isdigit(argv[i + 1][0]))
		{
			settings.trn_bits = atoi(argv[++i]);
		}
		else if (strcmp(arg, "-ar") == 0 && i + 1 < argc && isdigit(argv[i + 1][0]))
		{
			settings.rot_bits = atoi(argv[++i]);
		}
		else if (strcmp(arg, "-as") == 0 && i + 1 < argc && isdigit(argv[i + 1][0]))
		{
			settings.scl_bits = atoi(argv[++i]);
		}
		else if (strcmp(arg, "-af") == 0 && i + 1 < argc && isdigit(argv[i + 1][0]))
		{
			settings.anim_freq = atoi(argv[++i]);
		}
		else if (strcmp(arg, "-ac") == 0)
		{
			settings.anim_const = true;
		}
		else if (strcmp(arg, "-kn") == 0)
		{
			settings.keep_nodes = true;
		}
		else if (strcmp(arg, "-km") == 0)
		{
			settings.keep_materials = true;
		}
		else if (strcmp(arg, "-ke") == 0)
		{
			settings.keep_extras = true;
		}
		else if (strcmp(arg, "-mm") == 0)
		{
			settings.mesh_merge = true;
		}
		else if (strcmp(arg, "-mi") == 0)
		{
			settings.mesh_instancing = true;
		}
		else if (strcmp(arg, "-si") == 0 && i + 1 < argc && isdigit(argv[i + 1][0]))
		{
			settings.simplify_threshold = float(atof(argv[++i]));
		}
		else if (strcmp(arg, "-sa") == 0)
		{
			settings.simplify_aggressive = true;
		}
#ifndef NDEBUG
		else if (strcmp(arg, "-sd") == 0 && i + 1 < argc && isdigit(argv[i + 1][0]))
		{
			settings.simplify_debug = float(atof(argv[++i]));
		}
		else if (strcmp(arg, "-md") == 0 && i + 1 < argc && isdigit(argv[i + 1][0]))
		{
			settings.meshlet_debug = atoi(argv[++i]);
		}
#endif
		else if (strcmp(arg, "-te") == 0)
		{
			settings.texture_embed = true;
		}
		else if (strcmp(arg, "-tu") == 0)
		{
			settings.texture_ktx2 = true;
			settings.texture_uastc = true;
		}
		else if (strcmp(arg, "-tc") == 0)
		{
			settings.texture_ktx2 = true;
		}
		else if (strcmp(arg, "-tq") == 0 && i + 1 < argc && isdigit(argv[i + 1][0]))
		{
			settings.texture_quality = atoi(argv[++i]);
		}
		else if (strcmp(arg, "-ts") == 0 && i + 1 < argc && isdigit(argv[i + 1][0]))
		{
			settings.texture_scale = float(atof(argv[++i]));
		}
		else if (strcmp(arg, "-tp") == 0)
		{
			settings.texture_pow2 = true;
		}
		else if (strcmp(arg, "-noq") == 0)
		{
			settings.quantize = false;
		}
		else if (strcmp(arg, "-i") == 0 && i + 1 < argc && !input)
		{
			input = argv[++i];
		}
		else if (strcmp(arg, "-o") == 0 && i + 1 < argc && !output)
		{
			output = argv[++i];
		}
		else if (strcmp(arg, "-c") == 0)
		{
			settings.compress = true;
		}
		else if (strcmp(arg, "-cc") == 0)
		{
			settings.compress = true;
			settings.compressmore = true;
		}
		else if (strcmp(arg, "-cf") == 0)
		{
			settings.compress = true;
			settings.fallback = true;
		}
		else if (strcmp(arg, "-v") == 0)
		{
			settings.verbose = 1;
		}
		else if (strcmp(arg, "-vv") == 0)
		{
			settings.verbose = 2;
		}
		else if (strcmp(arg, "-h") == 0)
		{
			help = true;
		}
		else if (strcmp(arg, "-test") == 0)
		{
			test = true;
		}
		else if (arg[0] == '-')
		{
			fprintf(stderr, "Unrecognized option %s\n", arg);
			return 1;
		}
		else if (test)
		{
			testinputs.push_back(arg);
		}
		else
		{
			fprintf(stderr, "Expected option, got %s instead\n", arg);
			return 1;
		}
	}

	// shortcut for gltfpack -v
	if (settings.verbose && argc == 2)
	{
		printf("gltfpack %s\n", getVersion().c_str());
		return 0;
	}

	if (test)
	{
		for (size_t i = 0; i < testinputs.size(); ++i)
		{
			const char* path = testinputs[i];

			printf("%s\n", path);
			gltfpack(path, NULL, settings);
		}

		return 0;
	}

	if (!input || !output || help)
	{
		fprintf(stderr, "gltfpack %s\n", getVersion().c_str());
		fprintf(stderr, "Usage: gltfpack [options] -i input -o output\n");

		if (help)
		{
			fprintf(stderr, "\nBasics:\n");
			fprintf(stderr, "\t-i file: input file to process, .obj/.gltf/.glb\n");
			fprintf(stderr, "\t-o file: output file path, .gltf/.glb\n");
			fprintf(stderr, "\t-c: produce compressed gltf/glb files (-cc for higher compression ratio)\n");
			fprintf(stderr, "\nTextures:\n");
			fprintf(stderr, "\t-te: embed all textures into main buffer (.bin or .glb)\n");
			fprintf(stderr, "\t-tc: convert all textures to KTX2 with BasisU supercompression (using basisu/toktx executable)\n");
			fprintf(stderr, "\t-tu: use UASTC when encoding textures (much higher quality and much larger size)\n");
			fprintf(stderr, "\t-tq N: set texture encoding quality (default: 8; N should be between 1 and 10\n");
			fprintf(stderr, "\t-ts R: scale texture dimensions by the ratio R (default: 1; R should be between 0 and 1)\n");
			fprintf(stderr, "\t-tp: resize textures to nearest power of 2 to conform to WebGL1 restrictions\n");
			fprintf(stderr, "\nSimplification:\n");
			fprintf(stderr, "\t-si R: simplify meshes to achieve the ratio R (default: 1; R should be between 0 and 1)\n");
			fprintf(stderr, "\t-sa: aggressively simplify to the target ratio disregarding quality\n");
			fprintf(stderr, "\nVertices:\n");
			fprintf(stderr, "\t-vp N: use N-bit quantization for positions (default: 14; N should be between 1 and 16)\n");
			fprintf(stderr, "\t-vt N: use N-bit quantization for texture coordinates (default: 12; N should be between 1 and 16)\n");
			fprintf(stderr, "\t-vn N: use N-bit quantization for normals and tangents (default: 8; N should be between 1 and 16)\n");
			fprintf(stderr, "\nAnimations:\n");
			fprintf(stderr, "\t-at N: use N-bit quantization for translations (default: 16; N should be between 1 and 24)\n");
			fprintf(stderr, "\t-ar N: use N-bit quantization for rotations (default: 12; N should be between 4 and 16)\n");
			fprintf(stderr, "\t-as N: use N-bit quantization for scale (default: 16; N should be between 1 and 24)\n");
			fprintf(stderr, "\t-af N: resample animations at N Hz (default: 30)\n");
			fprintf(stderr, "\t-ac: keep constant animation tracks even if they don't modify the node transform\n");
			fprintf(stderr, "\nScene:\n");
			fprintf(stderr, "\t-kn: keep named nodes and meshes attached to named nodes so that named nodes can be transformed externally\n");
			fprintf(stderr, "\t-km: keep named materials and disable named material merging\n");
			fprintf(stderr, "\t-ke: keep extras data\n");
			fprintf(stderr, "\t-mm: merge instances of the same mesh together when possible\n");
			fprintf(stderr, "\t-mi: use EXT_mesh_gpu_instancing when serializing multiple mesh instances\n");
			fprintf(stderr, "\nMiscellaneous:\n");
			fprintf(stderr, "\t-cf: produce compressed gltf/glb files with fallback for loaders that don't support compression\n");
			fprintf(stderr, "\t-noq: disable quantization; produces much larger glTF files with no extensions\n");
			fprintf(stderr, "\t-v: verbose output (print version when used without other options)\n");
			fprintf(stderr, "\t-h: display this help and exit\n");
		}
		else
		{
			fprintf(stderr, "\nBasics:\n");
			fprintf(stderr, "\t-i file: input file to process, .obj/.gltf/.glb\n");
			fprintf(stderr, "\t-o file: output file path, .gltf/.glb\n");
			fprintf(stderr, "\t-c: produce compressed gltf/glb files (-cc for higher compression ratio)\n");
			fprintf(stderr, "\t-te: embed all textures into main buffer (.bin or .glb)\n");
			fprintf(stderr, "\t-tc: convert all textures to KTX2 with BasisU supercompression (using basisu/toktx executable)\n");
			fprintf(stderr, "\t-si R: simplify meshes to achieve the ratio R (default: 1; R should be between 0 and 1)\n");
			fprintf(stderr, "\nRun gltfpack -h to display a full list of options\n");
		}

		return 1;
	}

	if (settings.texture_scale < 1 && !settings.texture_ktx2)
	{
		fprintf(stderr, "Option -ts is only supported when -tc is set as well\n");
		return 1;
	}

	if (settings.texture_pow2 && !settings.texture_ktx2)
	{
		fprintf(stderr, "Option -tp is only supported when -tc is set as well\n");
		return 1;
	}

	return gltfpack(input, output, settings);
}
