#ifndef UFBXT_TESTING_UTILS_INCLUDED
#define UFBXT_TESTING_UTILS_INCLUDED

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

#include "testing_basics.h"

#ifndef ufbxt_soft_assert
#define ufbxt_soft_assert(cond) ufbxt_assert(cond)
#endif

// -- Vector helpers

static ufbx_real ufbxt_dot2(ufbx_vec2 a, ufbx_vec2 b)
{
	return a.x*b.x + a.y*b.y;
}

static ufbx_real ufbxt_dot3(ufbx_vec3 a, ufbx_vec3 b)
{
	return a.x*b.x + a.y*b.y + a.z*b.z;
}

static ufbx_real ufbxt_length2(ufbx_vec2 a)
{
	return (ufbx_real)sqrt(ufbxt_dot2(a, a));
}

static ufbx_real ufbxt_length3(ufbx_vec3 a)
{
	return (ufbx_real)sqrt(ufbxt_dot3(a, a));
}

static ufbx_vec2 ufbxt_add2(ufbx_vec2 a, ufbx_vec2 b)
{
	ufbx_vec2 v;
	v.x = a.x + b.x;
	v.y = a.y + b.y;
	return v;
}

static ufbx_vec3 ufbxt_add3(ufbx_vec3 a, ufbx_vec3 b)
{
	ufbx_vec3 v;
	v.x = a.x + b.x;
	v.y = a.y + b.y;
	v.z = a.z + b.z;
	return v;
}

static ufbx_vec2 ufbxt_sub2(ufbx_vec2 a, ufbx_vec2 b)
{
	ufbx_vec2 v;
	v.x = a.x - b.x;
	v.y = a.y - b.y;
	return v;
}

static ufbx_vec3 ufbxt_sub3(ufbx_vec3 a, ufbx_vec3 b)
{
	ufbx_vec3 v;
	v.x = a.x - b.x;
	v.y = a.y - b.y;
	v.z = a.z - b.z;
	return v;
}

static ufbx_vec2 ufbxt_mul2(ufbx_vec2 a, ufbx_real b)
{
	ufbx_vec2 v;
	v.x = a.x * b;
	v.y = a.y * b;
	return v;
}

static ufbx_vec3 ufbxt_mul3(ufbx_vec3 a, ufbx_real b)
{
	ufbx_vec3 v;
	v.x = a.x * b;
	v.y = a.y * b;
	v.z = a.z * b;
	return v;
}

static ufbx_vec3 ufbxt_cross3(ufbx_vec3 a, ufbx_vec3 b)
{
	ufbx_vec3 v = { a.y*b.z - a.z*b.y, a.z*b.x - a.x*b.z, a.x*b.y - a.y*b.x };
	return v;
}

static ufbx_vec3 ufbxt_normalize(ufbx_vec3 a) {
	ufbx_real len = (ufbx_real)sqrt(ufbxt_dot3(a, a));
	if (len != 0.0) {
		return ufbxt_mul3(a, (ufbx_real)1.0 / len);
	} else {
		ufbx_vec3 zero = { (ufbx_real)0 };
		return zero;
	}
}

static ufbx_real ufbxt_min(ufbx_real a, ufbx_real b) { return a < b ? a : b; }
static ufbx_real ufbxt_max(ufbx_real a, ufbx_real b) { return a < b ? b : a; }
static ufbx_real ufbxt_clamp(ufbx_real v, ufbx_real min_v, ufbx_real max_v) { return ufbxt_min(ufbxt_max(v, min_v), max_v); }


// -- obj load and diff

typedef struct {
	const char **groups;
	const char *original_group;
	size_t num_groups;

	size_t num_faces;
	size_t num_indices;

	ufbx_face *faces;

	ufbx_vertex_vec3 vertex_position;
	ufbx_vertex_vec3 vertex_normal;
	ufbx_vertex_vec2 vertex_uv;

} ufbxt_obj_mesh;

typedef enum {
	UFBXT_OBJ_EXPORTER_UNKNOWN,
	UFBXT_OBJ_EXPORTER_BLENDER,
} ufbxt_obj_exporter;

typedef struct {

	ufbxt_obj_mesh *meshes;
	size_t num_meshes;

	bool bad_normals;
	bool bad_order;
	bool bad_uvs;
	bool bad_faces;
	bool line_faces;
	bool point_faces;
	bool no_subdivision;
	bool root_groups_at_bone;
	bool remove_namespaces;
	bool match_by_order;
	ufbx_real tolerance;
	int32_t animation_frame;

	bool normalize_units;
	bool ignore_duplicates;

	ufbxt_obj_exporter exporter;

	double position_scale;

	char animation_name[128];

} ufbxt_obj_file;

typedef struct {

	const char *name_end_chars;

} ufbxt_load_obj_opts;

static int ufbxt_cmp_obj_mesh(const void *va, const void *vb)
{
	const ufbxt_obj_mesh *a = (const ufbxt_obj_mesh*)va, *b = (const ufbxt_obj_mesh*)vb;
	if (a->num_groups < b->num_groups) return -1;
	if (a->num_groups > b->num_groups) return +1;
	return 0;
}

static uint16_t ufbxt_read_u16(const void *ptr) {
	const char *p = (const char*)ptr;
	return (uint16_t)(
		(unsigned)(uint8_t)p[0] << 0u |
		(unsigned)(uint8_t)p[1] << 8u );
}

static uint32_t ufbxt_read_u32(const void *ptr) {
	const char *p = (const char*)ptr;
	return (uint32_t)(
		(unsigned)(uint8_t)p[0] <<  0u |
		(unsigned)(uint8_t)p[1] <<  8u |
		(unsigned)(uint8_t)p[2] << 16u |
		(unsigned)(uint8_t)p[3] << 24u );
}

static ufbxt_noinline void *ufbxt_decompress_gzip(const void *gz_data, size_t gz_size, size_t *p_size)
{
	const uint8_t *gz = (const uint8_t*)gz_data;
	if (gz_size < 10) return NULL;
	if (gz[0] != 0x1f || gz[1] != 0x8b || gz[2] != 0x08) return NULL;
	uint8_t flags = gz[3];

	size_t offset = 10;

	if (flags & 0x4) {
		// FEXTRA
		if (gz_size - offset < 2) return NULL;
		uint16_t xlen = ufbxt_read_u16(gz + offset);
		offset += 2;
		if (gz_size - offset < xlen) return NULL;
		offset += xlen;
	}

	if (flags & 0x8) {
		// FNAME
		const uint8_t *end = (const uint8_t*)memchr(gz + offset, 0, gz_size - offset);
		if (!end) return NULL;
		offset = (size_t)(end - gz) + 1;
	}

	if (flags & 0x10) {
		// FCOMMENT
		const uint8_t *end = (const uint8_t*)memchr(gz + offset, 0, gz_size - offset);
		if (!end) return NULL;
		offset = (size_t)(end - gz) + 1;
	}

	if (flags & 0x2) {
		// FHCRC
		if (gz_size - offset < 2) return NULL;
		offset += 2;
	}

	uint32_t isize = ufbxt_read_u32(gz + gz_size - 4);

	void *dst = malloc(isize + 1);
	if (!dst) return NULL;

	ufbx_inflate_retain retain;
	retain.initialized = false;

	ufbx_inflate_input input = { 0 };

	engine.input.data = gz + offset;
	engine.input.data_size = gz_size - offset;

	engine.input.total_size = engine.input.data_size;

	engine.input.no_header = true;
	engine.input.no_checksum = true;

	ptrdiff_t result = ufbx_inflate(dst, isize, &input, &retain);
	if (result != isize) {
		free(dst);
		return NULL;
	}

	((char*)dst)[isize] = '\0';

	*p_size = isize;
	return dst;
}

#define UFBXT_MAX_PARSED_INT 
#define UFBXT_MAX_EXPONENT 325

static double ufbxt_pow10_table[UFBXT_MAX_EXPONENT*2 + 1];
static volatile bool ufbxt_parsing_init_done = false;

static void ufbxt_init_parsing()
{
	if (ufbxt_parsing_init_done) return;

	for (int32_t i = 0; i <= UFBXT_MAX_EXPONENT*2; i++) {
		ufbxt_pow10_table[i] = pow(10.0, (double)(i - UFBXT_MAX_EXPONENT));
	}

	ufbxt_parsing_init_done = true;
}

static size_t ufbxt_parse_ints(int32_t *p_result, size_t max_count, const char **str)
{
	const char *c = *str;

	size_t num;
	for (num = 0; num < max_count; num++) {
		if (*c == '/') {
			c++;
			p_result[num] = 0;
			continue;
		}

		int32_t sign = 1;
		if (*c == '+') {
			c++;
		} else if (*c == '-') {
			sign = -1;
			c++;
		}

		if (!(*c >= '0' && *c <= '9')) return num;

		const uint32_t max_value = UINT64_C(100000000);
		uint32_t value = 0;

		do {
			if (value < max_value) {
				value = value * 10 + (uint64_t)(*c++ - '0');
			} else {
				return num;
			}
		} while (*c >= '0' && *c <= '9');

		p_result[num] = (int32_t)value * sign;

		if (*c != '/') return num;
		c++;
	}

	*str = c;
	return num;
}

static bool ufbxt_parse_reals(ufbx_real *p_result, size_t count, const char *str)
{
	const char *c = str;

	for (size_t i = 0; i < count; i++) {
		// Skip separator
		while (*c == ' ') c++;

		const uint64_t max_value = UINT64_C(1000000000000000000);
		uint64_t value = 0;

		double sign = 1.0;
		if (*c == '-') {
			sign = -1.0;
			c++;
		} else if (*c == '+') {
			c++;
		}

		int32_t exponent = 0;

		if (!(*c >= '0' && *c <= '9')) return false;

		do {
			if (value < max_value) {
				value = value * 10 + (uint64_t)(*c - '0');
			} else {
				exponent--;
			}
			c++;
		} while (*c >= '0' && *c <= '9');

		if (*c == '.') {
			c++;

			while (*c >= '0' && *c <= '9') {
				if (value < max_value) {
					value = value * 10 + (uint64_t)(*c - '0');
					exponent--;
				}
				c++;
			}

			if ((*c | 0x20) == 'e') {
				c++;
				int32_t expsign = 1;
				if (*c == '+') {
					c++;
				} else if (*c == '-') {
					expsign = -1;
					c++;
				}

				int32_t exp = 0;
				while (*c >= '0' && *c <= '9') {
					if (exp < 4096) {
						exp = exp * 10 + (int32_t)(*c - '0');
					}
					c++;
				}

				exponent += expsign * exp;
			}
		}

		if (exponent >= -UFBXT_MAX_EXPONENT && exponent <= UFBXT_MAX_EXPONENT) {
			p_result[i] = (ufbx_real)(sign * ufbxt_pow10_table[exponent + UFBXT_MAX_EXPONENT] * (double)value);
		} else if (exponent < 0) {
			p_result[i] = 0.0f;
		} else {
			p_result[i] = (ufbx_real)INFINITY;
		}
	}

	return true;
}

static char *ufbxt_find_newline(char *src)
{
	for (;;) {
		char c = *src;
		if (c == '\0') return NULL;
		if (c == '\r' || c == '\n') return src;
		src++;
	}
}

static bool ufbxt_is_space(char c)
{
	return c == ' ' || c == '\t' || c == '\r' || c == '\n';
}

static ufbxt_noinline ufbxt_obj_file *ufbxt_load_obj(void *obj_data, size_t obj_size, const ufbxt_load_obj_opts *opts)
{
	ufbxt_load_obj_opts zero_opts;
	if (!opts) {
		memset(&zero_opts, 0, sizeof(zero_opts));
		opts = &zero_opts;
	}

	ufbxt_init_parsing();

	size_t num_positions = 0;
	size_t num_normals = 0;
	size_t num_uvs = 0;
	size_t num_faces = 0;
	size_t num_meshes = 0;
	size_t num_indices = 0;
	size_t num_groups = 0;
	size_t total_name_length = 0;

	bool merge_groups = false;

	char *line = (char*)obj_data;
	for (;;) {
		char *end = ufbxt_find_newline(line);
		char prev = '\0';
		if (end) {
			prev = *end;
			*end = '\0';
		}

		if (!strncmp(line, "v ", 2)) num_positions++;
		else if (!strncmp(line, "vt ", 3)) num_uvs++;
		else if (!strncmp(line, "vn ", 3)) num_normals++;
		else if (!strncmp(line, "f ", 2)) {
			num_faces++;
			bool prev_space = false;
			for (char *c = line; *c; c++) {
				bool space = *c == ' ' || *c == '\t';
				if (space && !prev_space) num_indices++;
				prev_space = space;
			}
		}
		else if (!strncmp(line, "g default", 7)) { /* ignore default group */ }
		else if ((!strncmp(line, "g ", 2) && !merge_groups) || !strncmp(line, "o ", 2)) {
			bool prev_space = false;
			num_groups++;
			for (char *c = line; *c; c++) {
				bool space = *c == ' ' || *c == '\t';
				if (space && !prev_space) num_groups++;
				if (!space) total_name_length++;
				total_name_length++;
				prev_space = space;
			}
			total_name_length++;
			num_meshes++;
		} else if (strstr(line, "ufbx:merge_groups")) {
			merge_groups = true;
		}

		if (end) {
			*end = prev;
			line = end + 1;
		} else {
			break;
		}
	}

	bool implicit_mesh = false;
	if (num_meshes == 0) {
		num_meshes = 1;
		implicit_mesh = true;
	}

	total_name_length += num_groups;

	size_t alloc_size = 0;
	alloc_size += sizeof(ufbxt_obj_file);
	alloc_size += num_meshes * sizeof(ufbxt_obj_mesh);
	alloc_size += num_groups * sizeof(const char*);
	alloc_size += num_positions * sizeof(ufbx_vec3);
	alloc_size += num_normals * sizeof(ufbx_vec3);
	alloc_size += num_uvs * sizeof(ufbx_vec2);
	alloc_size += num_faces * sizeof(ufbx_face);
	alloc_size += num_indices * 3 * sizeof(int32_t);
	alloc_size += total_name_length * sizeof(char);

	void *data = malloc(alloc_size);
	ufbxt_assert(data);

	ufbxt_obj_file *obj = (ufbxt_obj_file*)data;
	const char **group_ptrs = (const char**)(obj + 1);
	ufbxt_obj_mesh *meshes = (ufbxt_obj_mesh*)(group_ptrs + num_groups);
	ufbx_vec3 *positions = (ufbx_vec3*)(meshes + num_meshes);
	ufbx_vec3 *normals = (ufbx_vec3*)(positions + num_positions);
	ufbx_vec2 *uvs = (ufbx_vec2*)(normals + num_normals);
	ufbx_face *faces = (ufbx_face*)(uvs + num_uvs);
	int32_t *position_indices = (int32_t*)(faces + num_faces);
	int32_t *normal_indices = (int32_t*)(position_indices + num_indices);
	int32_t *uv_indices = (int32_t*)(normal_indices + num_indices);
	char *name_data = (char*)(uv_indices + num_indices);
	void *data_end = name_data + total_name_length;
	ufbxt_assert((char*)data_end - (char*)data == alloc_size);

	memset(obj, 0, sizeof(ufbxt_obj_file));

	ufbx_vec3 *dp = positions;
	ufbx_vec3 *dn = normals;
	ufbx_vec2 *du = uvs;
	ufbxt_obj_mesh *mesh = NULL;

	int32_t *dpi = position_indices;
	int32_t *dni = normal_indices;
	int32_t *dui = uv_indices;

	ufbx_face *df = faces;

	obj->meshes = meshes;
	obj->num_meshes = num_meshes;
	obj->tolerance = 0.001f;
	obj->normalize_units = false;
	obj->animation_frame = -1;
	obj->exporter = UFBXT_OBJ_EXPORTER_UNKNOWN;
	obj->position_scale = 1.0;

	if (implicit_mesh) {
		mesh = meshes;
		memset(mesh, 0, sizeof(ufbxt_obj_mesh));

		mesh->faces = df;
		mesh->vertex_position.values.data = positions;
		mesh->vertex_normal.values.data = normals;
		mesh->vertex_uv.values.data = uvs;
		mesh->vertex_position.indices.data = (uint32_t*)dpi;
		mesh->vertex_normal.indices.data = (uint32_t*)dni;
		mesh->vertex_uv.indices.data = (uint32_t*)dui;
	}

	line = (char*)obj_data;
	for (;;) {
		char *line_end = ufbxt_find_newline(line);
		char prev = '\0';
		if (line_end) {
			prev = *line_end;
			*line_end = '\0';
		}

		if (!strncmp(line, "v ", 2)) {
			line += 2;
			ufbxt_assert(ufbxt_parse_reals(dp->v, 3, line));
			dp++;
		} else if (!strncmp(line, "vt ", 3)) {
			line += 3;
			ufbxt_assert(ufbxt_parse_reals(du->v, 2, line));
			du++;
		} else if (!strncmp(line, "vn ", 3)) {
			line += 3;
			ufbxt_assert(ufbxt_parse_reals(dn->v, 3, line));
			dn++;
		} else if (!strncmp(line, "f ", 2)) {
			ufbxt_assert(mesh);

			df->index_begin = (uint32_t)mesh->num_indices;
			df->num_indices = 0;

			char *begin = line + 2;
			do {
				char *end = strchr(begin, ' ');
				if (end) *end++ = '\0';

				while (ufbxt_is_space(*begin)) {
					begin++;
				}
				if (*begin == '\0') {
					begin = end;
					continue;
				}

				int32_t indices[3] = { 0, 0, 0 };
				ufbxt_parse_ints(indices, 3, (const char**)&begin);

				mesh->vertex_position.indices.count++;
				mesh->vertex_normal.indices.count++;
				mesh->vertex_uv.indices.count++;

				mesh->vertex_position.values.count = (size_t)(dp - positions);
				mesh->vertex_normal.values.count = (size_t)(dn - normals);
				mesh->vertex_uv.values.count = (size_t)(du - uvs);

				*dpi++ = indices[0] - 1;
				*dni++ = indices[2] - 1;
				*dui++ = indices[1] - 1;
				mesh->num_indices++;
				df->num_indices++;

				begin = end;
			} while (begin);

			mesh->num_faces++;
			df++;
		} else if (!strncmp(line, "g default", 7)) {
			/* ignore default group */
		} else if ((!strncmp(line, "g ", 2) && !merge_groups) || !strncmp(line, "o ", 2)) {
			mesh = mesh ? mesh + 1 : meshes;
			memset(mesh, 0, sizeof(ufbxt_obj_mesh));

			size_t groups_len = strlen(line + 2);
			memcpy(name_data, line + 2, groups_len + 1);
			mesh->original_group = name_data;
			name_data += groups_len + 1;

			mesh->groups = group_ptrs;

			const char *c = line + 2;
			for (;;) {
				while (*c == ' ' || *c == '\t' || *c == '\r') {
					c++;
				}

				if (*c == '\n' || *c == '\0') break;

				const char *group_begin = c;
				char *group_copy = name_data;
				char *dst = group_copy;

				while (*c != ' ' && *c != '\t' && *c != '\r' && *c != '\n' && *c != '\0') {
					if (!strncmp(c, "FBXASC", 6)) {
						c += 6;
						char num[4] = { 0 };
						if (*c != '\0') num[0] = *c++;
						if (*c != '\0') num[1] = *c++;
						if (*c != '\0') num[2] = *c++;
						*dst++ = (char)atoi(num);
					} else {
						*dst++ = *c++;
					}
				}
				*dst++ = '\0';
				name_data = dst;

				*group_ptrs++ = group_copy;
			}

			mesh->num_groups = group_ptrs - mesh->groups;

			mesh->faces = df;
			mesh->vertex_position.values.data = positions;
			mesh->vertex_normal.values.data = normals;
			mesh->vertex_uv.values.data = uvs;
			mesh->vertex_position.indices.data = (uint32_t*)dpi;
			mesh->vertex_normal.indices.data = (uint32_t*)dni;
			mesh->vertex_uv.indices.data = (uint32_t*)dui;
		}

		if (line[0] == '#') {
			line += 1;
			char *end = line_end;
			while (line < end && (line[0] == ' ' || line[0] == '\t')) {
				line++;
			}
			while (end > line && (end[-1] == ' ' || end[-1] == '\t')) {
				*--end = '\0';
			}
			if (!strcmp(line, "ufbx:bad_normals")) {
				obj->bad_normals = true;
			}
			if (!strcmp(line, "ufbx:bad_order")) {
				obj->bad_order = true;
			}
			if (!strcmp(line, "ufbx:bad_uvs")) {
				obj->bad_uvs = true;
			}
			if (!strcmp(line, "ufbx:bad_faces")) {
				obj->bad_faces = true;
			}
			if (!strcmp(line, "ufbx:line_faces")) {
				obj->line_faces = true;
			}
			if (!strcmp(line, "ufbx:point_faces")) {
				obj->point_faces = true;
			}
			if (!strcmp(line, "ufbx:no_subdivision")) {
				obj->no_subdivision = true;
			}
			if (!strcmp(line, "ufbx:ignore_duplicates")) {
				obj->ignore_duplicates = true;
			}
			if (!strcmp(line, "ufbx:root_groups_at_bone")) {
				obj->root_groups_at_bone = true;
			}
			if (!strcmp(line, "ufbx:remove_namespaces")) {
				obj->remove_namespaces = true;
			}
			if (!strcmp(line, "ufbx:match_by_order")) {
				obj->match_by_order = true;
			}
			if (!strcmp(line, "www.blender.org")) {
				obj->exporter = UFBXT_OBJ_EXPORTER_BLENDER;
			}
			if (!strncmp(line, "ufbx:animation=", 15)) {
				line += 15;
				size_t len = strcspn(line, "\r\n");
				ufbxt_assert(len + 1 < sizeof(obj->animation_name));
				memcpy(obj->animation_name, line, len);
			}
			double tolerance = 0.0;
			if (sscanf(line, "ufbx:tolerance=%lf", &tolerance) == 1) {
				obj->tolerance = (ufbx_real)tolerance;
			}
			double position_scale = 0.0;
			if (sscanf(line, "ufbx:position_scale=%lf", &position_scale) == 1) {
				obj->position_scale = (ufbx_real)position_scale;
			}
			int frame = 0;
			if (sscanf(line, "ufbx:frame=%d", &frame) == 1) {
				obj->animation_frame = (int32_t)frame;
			}
		}

		if (line_end) {
			*line_end = prev;
			line = line_end + 1;
		} else {
			break;
		}
	}

	qsort(obj->meshes, obj->num_meshes, sizeof(ufbxt_obj_mesh), ufbxt_cmp_obj_mesh);

	return obj;
}

static ufbxt_noinline void ufbxt_debug_dump_obj_mesh(const char *file, ufbx_node *node, ufbx_mesh *mesh)
{
	FILE *f = fopen(file, "wb");
	ufbxt_assert(f);

	fprintf(f, "s 1\n");

	for (size_t i = 0; i < mesh->vertex_position.values.count; i++) {
		ufbx_vec3 v = mesh->vertex_position.values.data[i];
		v = ufbx_transform_position(&node->geometry_to_world, v);
		fprintf(f, "v %f %f %f\n", v.x, v.y, v.z);
	}
	for (size_t i = 0; i < mesh->vertex_uv.values.count; i++) {
		ufbx_vec2 v = mesh->vertex_uv.values.data[i];
		fprintf(f, "vt %f %f\n", v.x, v.y);
	}

	ufbx_matrix mat = ufbx_matrix_for_normals(&node->geometry_to_world);
	for (size_t i = 0; i < mesh->vertex_normal.values.count; i++) {
		ufbx_vec3 v = mesh->vertex_normal.values.data[i];
		v = ufbx_transform_direction(&mat, v);
		fprintf(f, "vn %f %f %f\n", v.x, v.y, v.z);
	}


	for (size_t fi = 0; fi < mesh->num_faces; fi++) {
		ufbx_face face = mesh->faces.data[fi];
		fprintf(f, "f");
		for (size_t ci = 0; ci < face.num_indices; ci++) {
			int32_t vi = mesh->vertex_position.indices.data[face.index_begin + ci];
			int32_t ti = mesh->vertex_uv.indices.data[face.index_begin + ci];
			int32_t ni = mesh->vertex_normal.indices.data[face.index_begin + ci];
			fprintf(f, " %d/%d/%d", vi + 1, ti + 1, ni + 1);
		}
		fprintf(f, "\n");
	}

	fclose(f);
}

static ufbxt_noinline void ufbxt_debug_dump_obj_scene(const char *file, ufbx_scene *scene)
{
	FILE *f = fopen(file, "wb");
	ufbxt_assert(f);

	for (size_t mi = 0; mi < scene->meshes.count; mi++) {
		ufbx_mesh *mesh = scene->meshes.data[mi];
		for (size_t ni = 0; ni < mesh->instances.count; ni++) {
			ufbx_node *node = mesh->instances.data[ni];

			for (size_t i = 0; i < mesh->vertex_position.values.count; i++) {
				ufbx_vec3 v = mesh->skinned_position.values.data[i];
				if (mesh->skinned_is_local) {
					v = ufbx_transform_position(&node->geometry_to_world, v);
				}
				fprintf(f, "v %f %f %f\n", v.x, v.y, v.z);
			}

			for (size_t i = 0; i < mesh->vertex_uv.values.count; i++) {
				ufbx_vec2 v = mesh->vertex_uv.values.data[i];
				fprintf(f, "vt %f %f\n", v.x, v.y);
			}

			ufbx_matrix mat = ufbx_matrix_for_normals(&node->geometry_to_world);
			for (size_t i = 0; i < mesh->skinned_normal.values.count; i++) {
				ufbx_vec3 v = mesh->skinned_normal.values.data[i];
				if (mesh->skinned_is_local) {
					v = ufbx_transform_direction(&mat, v);
				}
				fprintf(f, "vn %f %f %f\n", v.x, v.y, v.z);
			}

			fprintf(f, "\n");
		}
	}

	int32_t v_off = 0, t_off = 0, n_off = 0;
	for (size_t mi = 0; mi < scene->meshes.count; mi++) {
		ufbx_mesh *mesh = scene->meshes.data[mi];
		for (size_t ni = 0; ni < mesh->instances.count; ni++) {
			ufbx_node *node = mesh->instances.data[ni];
			fprintf(f, "g %s\n", node->name.data);

			for (size_t fi = 0; fi < mesh->num_faces; fi++) {
				ufbx_face face = mesh->faces.data[fi];
				fprintf(f, "f");
				for (size_t ci = 0; ci < face.num_indices; ci++) {
					int32_t vi = v_off + mesh->skinned_position.indices.data[face.index_begin + ci];
					int32_t ni = n_off + mesh->skinned_normal.indices.data[face.index_begin + ci];
					if (mesh->vertex_uv.exists) {
						int32_t ti = t_off + mesh->vertex_uv.indices.data[face.index_begin + ci];
						fprintf(f, " %d/%d/%d", vi + 1, ti + 1, ni + 1);
					} else {
						fprintf(f, " %d//%d", vi + 1, ni + 1);
					}
				}
				fprintf(f, "\n");
			}

			fprintf(f, "\n");

			v_off += (int32_t)mesh->skinned_position.values.count;
			t_off += (int32_t)mesh->vertex_uv.values.count;
			n_off += (int32_t)mesh->skinned_normal.values.count;
		}
	}

	fclose(f);
}

typedef struct {
	size_t num;
	ufbx_real sum;
	ufbx_real max;
} ufbxt_diff_error;

static void ufbxt_assert_close_real(ufbxt_diff_error *p_err, ufbx_real a, ufbx_real b)
{
	ufbx_real err = fabs(a - b);
	ufbxt_soft_assert(err < 0.001);
	p_err->num++;
	p_err->sum += err;
	if (err > p_err->max) p_err->max = err;
}

static void ufbxt_assert_close_vec2(ufbxt_diff_error *p_err, ufbx_vec2 a, ufbx_vec2 b)
{
	ufbxt_assert_close_real(p_err, a.x, b.x);
	ufbxt_assert_close_real(p_err, a.y, b.y);
}

static void ufbxt_assert_close_vec3(ufbxt_diff_error *p_err, ufbx_vec3 a, ufbx_vec3 b)
{
	ufbxt_assert_close_real(p_err, a.x, b.x);
	ufbxt_assert_close_real(p_err, a.y, b.y);
	ufbxt_assert_close_real(p_err, a.z, b.z);
}

static void ufbxt_assert_close_vec4(ufbxt_diff_error *p_err, ufbx_vec4 a, ufbx_vec4 b)
{
	ufbxt_assert_close_real(p_err, a.x, b.x);
	ufbxt_assert_close_real(p_err, a.y, b.y);
	ufbxt_assert_close_real(p_err, a.z, b.z);
	ufbxt_assert_close_real(p_err, a.w, b.w);
}

static void ufbxt_assert_close_quat(ufbxt_diff_error *p_err, ufbx_quat a, ufbx_quat b)
{
	ufbxt_assert_close_real(p_err, a.x, b.x);
	ufbxt_assert_close_real(p_err, a.y, b.y);
	ufbxt_assert_close_real(p_err, a.z, b.z);
	ufbxt_assert_close_real(p_err, a.w, b.w);
}

static void ufbxt_assert_close_matrix(ufbxt_diff_error *p_err, ufbx_matrix a, ufbx_matrix b)
{
	ufbxt_assert_close_vec3(p_err, a.cols[0], b.cols[0]);
	ufbxt_assert_close_vec3(p_err, a.cols[1], b.cols[1]);
	ufbxt_assert_close_vec3(p_err, a.cols[2], b.cols[2]);
	ufbxt_assert_close_vec3(p_err, a.cols[3], b.cols[3]);
}

static void ufbxt_assert_close_real_threshold(ufbxt_diff_error *p_err, ufbx_real a, ufbx_real b, ufbx_real threshold)
{
	ufbx_real err = fabs(a - b);
	ufbxt_assert(err < threshold);
	p_err->num++;
	p_err->sum += err;
	if (err > p_err->max) p_err->max = err;
}

static void ufbxt_assert_close_vec2_threshold(ufbxt_diff_error *p_err, ufbx_vec2 a, ufbx_vec2 b, ufbx_real threshold)
{
	ufbxt_assert_close_real_threshold(p_err, a.x, b.x, threshold);
	ufbxt_assert_close_real_threshold(p_err, a.y, b.y, threshold);
}

static void ufbxt_assert_close_vec3_threshold(ufbxt_diff_error *p_err, ufbx_vec3 a, ufbx_vec3 b, ufbx_real threshold)
{
	ufbxt_assert_close_real_threshold(p_err, a.x, b.x, threshold);
	ufbxt_assert_close_real_threshold(p_err, a.y, b.y, threshold);
	ufbxt_assert_close_real_threshold(p_err, a.z, b.z, threshold);
}

static void ufbxt_assert_close_vec4_threshold(ufbxt_diff_error *p_err, ufbx_vec4 a, ufbx_vec4 b, ufbx_real threshold)
{
	ufbxt_assert_close_real_threshold(p_err, a.x, b.x, threshold);
	ufbxt_assert_close_real_threshold(p_err, a.y, b.y, threshold);
	ufbxt_assert_close_real_threshold(p_err, a.z, b.z, threshold);
	ufbxt_assert_close_real_threshold(p_err, a.w, b.w, threshold);
}

static void ufbxt_assert_close_quat_threshold(ufbxt_diff_error *p_err, ufbx_quat a, ufbx_quat b, ufbx_real threshold)
{
	ufbxt_assert_close_real_threshold(p_err, a.x, b.x, threshold);
	ufbxt_assert_close_real_threshold(p_err, a.y, b.y, threshold);
	ufbxt_assert_close_real_threshold(p_err, a.z, b.z, threshold);
	ufbxt_assert_close_real_threshold(p_err, a.w, b.w, threshold);
}

static ufbxt_noinline void ufbxt_check_source_vertices(ufbx_mesh *mesh, ufbx_mesh *src_mesh, ufbxt_diff_error *p_err)
{
	ufbx_subdivision_result *sub = mesh->subdivision_result;
	ufbxt_assert(sub);

	size_t num_vertices = mesh->num_vertices;
	ufbxt_assert(sub->source_vertex_ranges.count == num_vertices);
	for (size_t vi = 0; vi < num_vertices; vi++) {
		ufbx_subdivision_weight_range range = sub->source_vertex_ranges.data[vi];

		ufbx_vec3 sum = ufbx_zero_vec3;
		for (size_t i = 0; i < range.num_weights; i++) {
			ufbx_subdivision_weight weight = sub->source_vertex_weights.data[range.weight_begin + i];
			ufbx_vec3 v = src_mesh->vertices.data[weight.index];
			sum.x += v.x * weight.weight;
			sum.y += v.y * weight.weight;
			sum.z += v.z * weight.weight;
		}

		ufbx_vec3 ref = mesh->vertices.data[vi];
		ufbxt_assert_close_vec3(p_err, ref, sum);
		ref = ref;
	}
}

typedef struct {
	ufbx_vec3 pos;
	ufbx_vec3 normal;
	ufbx_vec2 uv;
} ufbxt_match_vertex;

static int ufbxt_cmp_sub_vertex(const void *va, const void *vb)
{
	const ufbxt_match_vertex *a = (const ufbxt_match_vertex*)va, *b = (const ufbxt_match_vertex*)vb;
	if (a->pos.x != b->pos.x) return a->pos.x < b->pos.x ? -1 : +1;
	if (a->pos.y != b->pos.y) return a->pos.y < b->pos.y ? -1 : +1;
	if (a->pos.z != b->pos.z) return a->pos.z < b->pos.z ? -1 : +1;
	if (a->normal.x != b->normal.x) return a->normal.x < b->normal.x ? -1 : +1;
	if (a->normal.y != b->normal.y) return a->normal.y < b->normal.y ? -1 : +1;
	if (a->normal.z != b->normal.z) return a->normal.z < b->normal.z ? -1 : +1;
	if (a->uv.x != b->uv.x) return a->uv.x < b->uv.x ? -1 : +1;
	if (a->uv.y != b->uv.y) return a->uv.y < b->uv.y ? -1 : +1;
	return 0;
}

static ufbxt_noinline void ufbxt_match_obj_mesh(ufbxt_obj_file *obj, ufbx_node *fbx_node, ufbx_mesh *fbx_mesh, ufbxt_obj_mesh *obj_mesh, ufbxt_diff_error *p_err, double scale)
{
	ufbx_real tolerance = obj->tolerance;

	ufbxt_assert(fbx_mesh->num_faces == obj_mesh->num_faces);
	ufbxt_assert(fbx_mesh->num_indices == obj_mesh->num_indices);

	// Check that all vertices exist, anything more doesn't really make sense
	ufbxt_match_vertex *obj_verts = (ufbxt_match_vertex*)calloc(obj_mesh->num_indices, sizeof(ufbxt_match_vertex));
	ufbxt_match_vertex *fbx_verts = (ufbxt_match_vertex*)calloc(fbx_mesh->num_indices, sizeof(ufbxt_match_vertex));
	ufbxt_assert(obj_verts && fbx_verts);

	ufbx_matrix norm_mat = ufbx_get_compatible_matrix_for_normals(fbx_node);

	for (size_t i = 0; i < obj_mesh->num_indices; i++) {
		obj_verts[i].pos = ufbx_get_vertex_vec3(&obj_mesh->vertex_position, i);
		obj_verts[i].normal = ufbx_get_vertex_vec3(&obj_mesh->vertex_normal, i);
		if (obj_mesh->vertex_uv.exists) {
			obj_verts[i].uv = ufbx_get_vertex_vec2(&obj_mesh->vertex_uv, i);
		}

		if (scale != 1.0) {
			obj_verts[i].pos.x *= scale;
			obj_verts[i].pos.y *= scale;
			obj_verts[i].pos.x *= scale;
		}
	}
	for (size_t i = 0; i < fbx_mesh->num_indices; i++) {
		ufbx_vec3 fp = ufbx_get_vertex_vec3(&fbx_mesh->skinned_position, i);
		ufbx_vec3 fn = fbx_mesh->skinned_normal.exists ? ufbx_get_vertex_vec3(&fbx_mesh->skinned_normal, i) : ufbx_zero_vec3;
		if (fbx_mesh->skinned_is_local) {
			fp = ufbx_transform_position(&fbx_node->geometry_to_world, fp);
			fn = ufbx_transform_direction(&norm_mat, fn);
			fn = ufbxt_normalize(fn);
		}
		fbx_verts[i].pos = fp;
		fbx_verts[i].normal = fn;
		if (obj_mesh->vertex_uv.exists) {
			ufbxt_assert(fbx_mesh->vertex_uv.exists);
			fbx_verts[i].uv = ufbx_get_vertex_vec2(&fbx_mesh->vertex_uv, i);
		}

		if (scale != 1.0) {
			fbx_verts[i].pos.x *= scale;
			fbx_verts[i].pos.y *= scale;
			fbx_verts[i].pos.x *= scale;
		}
	}

	qsort(obj_verts, obj_mesh->num_indices, sizeof(ufbxt_match_vertex), &ufbxt_cmp_sub_vertex);
	qsort(fbx_verts, fbx_mesh->num_indices, sizeof(ufbxt_match_vertex), &ufbxt_cmp_sub_vertex);

	for (int32_t i = (int32_t)fbx_mesh->num_indices - 1; i >= 0; i--) {
		ufbxt_match_vertex v = fbx_verts[i];

		bool found = false;
		for (int32_t j = i; j >= 0 && obj_verts[j].pos.x >= v.pos.x - tolerance; j--) {
			ufbx_real dx = obj_verts[j].pos.x - v.pos.x;
			ufbx_real dy = obj_verts[j].pos.y - v.pos.y;
			ufbx_real dz = obj_verts[j].pos.z - v.pos.z;
			ufbx_real dnx = obj_verts[j].normal.x - v.normal.x;
			ufbx_real dny = obj_verts[j].normal.y - v.normal.y;
			ufbx_real dnz = obj_verts[j].normal.z - v.normal.z;
			ufbx_real du = obj_verts[j].uv.x - v.uv.x;
			ufbx_real dv = obj_verts[j].uv.y - v.uv.y;

			if (obj->bad_normals) {
				dnx = 0.0f;
				dny = 0.0f;
				dnz = 0.0f;
			}

			if (obj->bad_uvs) {
				du = 0.0f;
				dv = 0.0f;
			}

			ufbxt_assert(dx <= tolerance);
			ufbx_real err = (ufbx_real)sqrt(dx*dx + dy*dy + dz*dz + dnx*dnx + dny*dny + dnz*dnz + du*du + dv*dv);
			if (err < tolerance) {
				if (err > p_err->max) p_err->max = err;
				p_err->sum += err;
				p_err->num++;

				obj_verts[j] = obj_verts[i];
				found = true;
				break;
			}
		}

		ufbxt_assert(found);
	}

	free(obj_verts);
	free(fbx_verts);

}

typedef struct {
	const char *cat_name;
	ufbx_node *node;
} ufbxt_obj_node;

static int ufbxt_cmp_string(const void *va, const void *vb)
{
	const char **a = (const char**)va, **b = (const char**)vb;
	return strcmp(*a, *b);
}

static int ufbxt_cmp_obj_node(const void *va, const void *vb)
{
	const ufbxt_obj_node *a = (const ufbxt_obj_node*)va, *b = (const ufbxt_obj_node*)vb;
	return strcmp(a->cat_name, b->cat_name);
}

static ufbxt_noinline size_t ufbxt_obj_group_key(char *cat_buf, size_t cat_cap, const char **groups, size_t num_groups, bool remove_namespaces)
{
	ufbxt_assert(num_groups > 0);
	qsort((void*)groups, num_groups, sizeof(const char*), &ufbxt_cmp_string);

	char *cat_ptr = cat_buf, *cat_end = cat_buf + cat_cap;
	for (size_t i = 0; i < num_groups; i++) {
		const char *group = groups[i];

		if (remove_namespaces) {
			for (;;) {
				const char *next = strchr(group, ':');
				if (!next) break;
				group = next + 1;
			}
		}

		size_t space = (size_t)(cat_end - cat_ptr);
		int res = snprintf(cat_ptr, space, "%s\n", group);
		ufbxt_assert(res >= 0 && (size_t)res < space);
		cat_ptr += res;
	}

	*--cat_ptr = '\0';
	return cat_ptr - cat_buf;
}

static ufbxt_noinline bool ufbxt_face_has_duplicate_vertex(ufbx_mesh *mesh, ufbx_face face)
{
	for (size_t i = 0; i < face.num_indices; i++) {
		uint32_t ix = mesh->vertex_indices.data[face.index_begin + i];
		for (size_t j = 0; j < i; j++) {
			uint32_t jx = mesh->vertex_indices.data[face.index_begin + j];
			if (ix == jx) {
				return true;
			}
		}
	}
	return false;
}

enum {
	UFBXT_OBJ_DIFF_FLAG_CHECK_DEFORMED_NORMALS = 0x1,
	UFBXT_OBJ_DIFF_FLAG_IGNORE_NORMALS = 0x2,
};

static ufbxt_noinline void ufbxt_diff_to_obj(ufbx_scene *scene, ufbxt_obj_file *obj, ufbxt_diff_error *p_err, uint32_t flags)
{
	ufbx_node **used_nodes = (ufbx_node**)malloc(obj->num_meshes * sizeof(ufbx_node*));
	ufbxt_assert(used_nodes);

	size_t num_used_nodes = 0;

	char cat_name[4096];
	const char *groups[64];

	char *cat_name_data = NULL;
	size_t cat_name_offset = 0;

	ufbxt_obj_node *obj_nodes = (ufbxt_obj_node*)calloc(scene->nodes.count, sizeof(ufbxt_obj_node));
	ufbxt_assert(obj_nodes);

	size_t num_obj_nodes = 0;

	for (int step = 0; step < 2; step++) {
		for (size_t node_i = 0; node_i < scene->nodes.count; node_i++) {
			ufbx_node *node = scene->nodes.data[node_i];
			if (!node->name.length) continue;

			size_t num_groups = 0;
			ufbx_node *parent = node;
			while (parent && !parent->is_root) {
				if (obj->root_groups_at_bone && parent->attrib_type == UFBX_ELEMENT_BONE) break;

				ufbxt_assert(num_groups < ufbxt_arraycount(groups));
				groups[num_groups++] = parent->name.data;
				parent = parent->parent;
			}

			if (num_groups == 0) continue;

			size_t cat_len = ufbxt_obj_group_key(cat_name, sizeof(cat_name), groups, num_groups, obj->remove_namespaces);
			if (cat_name_data) {
				char *dst = cat_name_data + cat_name_offset;
				memcpy(dst, cat_name, cat_len + 1);

				ufbxt_obj_node *obj_node = &obj_nodes[num_obj_nodes++];
				obj_node->cat_name = dst;
				obj_node->node = node;
			}
			cat_name_offset += cat_len + 1;
		}

		if (cat_name_data) break;

		cat_name_data = (char*)malloc(cat_name_offset);
		ufbxt_assert(cat_name_data);
		cat_name_offset = 0;
	}

	qsort(obj_nodes, num_obj_nodes, sizeof(ufbxt_obj_node), &ufbxt_cmp_obj_node);

	for (size_t mesh_i = 0; mesh_i < obj->num_meshes; mesh_i++) {
		ufbxt_obj_mesh *obj_mesh = &obj->meshes[mesh_i];
		if (obj_mesh->num_indices == 0) continue;

		ufbx_node *node = NULL;

		if (obj->match_by_order) {
			ufbxt_assert(mesh_i + 1 < scene->nodes.count);
			node = scene->nodes.data[mesh_i + 1];
		}

		// Search for a node containing all the groups
		if (!node && obj_mesh->num_groups > 0) {
			ufbxt_obj_group_key(cat_name, sizeof(cat_name), obj_mesh->groups, obj_mesh->num_groups, obj->remove_namespaces);
			ufbxt_obj_node key = { cat_name };
			ufbxt_obj_node *found = (ufbxt_obj_node*)bsearch(&key, obj_nodes, num_obj_nodes,
				sizeof(ufbxt_obj_node), &ufbxt_cmp_obj_node);

			if (found) {
				if (found->node && found->node->mesh) {
					bool seen = false;
					for (size_t i = 0; i < num_used_nodes; i++) {
						if (used_nodes[i] == found->node) {
							seen = true;
							break;
						}
					}
					if (!seen) {
						node = found->node;
					}
				}
			}
		}

		if (!node && obj_mesh->original_group && *obj_mesh->original_group) {
			node = ufbx_find_node(scene, obj_mesh->original_group);
		}

		if (!node) {
			for (size_t group_i = 0; group_i < obj_mesh->num_groups; group_i++) {
				const char *name = obj_mesh->groups[group_i];

				node = ufbx_find_node(scene, name);
				if (!node && obj->exporter == UFBXT_OBJ_EXPORTER_BLENDER) {
					// Blender concatenates _Material to names
					size_t name_len = strcspn(name, "_");
					node = ufbx_find_node_len(scene, name, name_len);
				}

				if (node && node->mesh) {
					bool seen = false;
					for (size_t i = 0; i < num_used_nodes; i++) {
						if (used_nodes[i] == node) {
							seen = true;
							break;
						}
					}
					if (!seen) break;
				}
			}
		}

		if (obj->ignore_duplicates) {
			if (!node) {
				bool is_ncl = false;
				for (size_t i = 0; i < obj_mesh->num_groups; i++) {
					if (strstr(obj_mesh->groups[i], "_ncl1_")) {
						is_ncl = true;
						break;
					}
				}
				if (is_ncl) continue;
			}

			if (node && node->parent) {
				bool duplicate_name = false;
				ufbx_node *parent = node->parent;
				for (size_t i = 0; i < parent->children.count; i++) {
					ufbx_node *child = parent->children.data[i];
					if (child == node) continue;
					if (!strcmp(child->name.data, node->name.data)) {
						duplicate_name = true;
						break;
					}
				}
				if (duplicate_name) continue;
			}
		}

		if (!node) {
			ufbxt_assert(scene->meshes.count == 1);
			ufbxt_assert(scene->meshes.data[0]->instances.count == 1);
			node = scene->meshes.data[0]->instances.data[0];
		}

		ufbxt_assert(node);
		if (node->geometry_transform_helper) {
			node = node->geometry_transform_helper;
		}

		ufbx_mesh *mesh = node->mesh;

		double scale = 1.0;

		if (obj->normalize_units && scene->settings.unit_meters != 1.0) {
			scale *= scene->settings.unit_meters;
		}

		if (obj->position_scale != 1.0) {
			scale *= obj->position_scale;
		}

		used_nodes[num_used_nodes++] = node;

		if (!mesh && node->attrib_type == UFBX_ELEMENT_NURBS_SURFACE) {
			ufbx_nurbs_surface *surface = (ufbx_nurbs_surface*)node->attrib;
			ufbx_tessellate_surface_opts opts = { 0 };
			opts.span_subdivision_u = surface->span_subdivision_u;
			opts.span_subdivision_v = surface->span_subdivision_v;
			ufbx_mesh *tess_mesh = ufbx_tessellate_nurbs_surface(surface, &opts, NULL);
			ufbxt_assert(tess_mesh);

			// ufbxt_debug_dump_obj_mesh("test.obj", node, tess_mesh);

			ufbxt_check_mesh(scene, tess_mesh);
			ufbxt_match_obj_mesh(obj, node, tess_mesh, obj_mesh, p_err, scale);
			ufbx_free_mesh(tess_mesh);

			continue;
		}

		ufbxt_assert(mesh);

		// Multiple skin deformers are not supported
		if (mesh->skin_deformers.count > 1) continue;

		ufbx_matrix *mat = &node->geometry_to_world;
		ufbx_matrix norm_mat = ufbx_get_compatible_matrix_for_normals(node);

		if (!obj->no_subdivision && (mesh->subdivision_display_mode == UFBX_SUBDIVISION_DISPLAY_SMOOTH || mesh->subdivision_display_mode == UFBX_SUBDIVISION_DISPLAY_HULL_AND_SMOOTH)) {
			ufbx_subdivide_opts opts = { 0 };
			opts.evaluate_source_vertices = true;
			opts.evaluate_skin_weights = true;

			ufbx_mesh *sub_mesh = ufbx_subdivide_mesh(mesh, mesh->subdivision_preview_levels, &opts, NULL);
			ufbxt_assert(sub_mesh);

			// Check that we didn't break the original mesh
			ufbxt_check_mesh(scene, mesh);

			ufbxt_check_source_vertices(sub_mesh, mesh, p_err);

			ufbxt_check_mesh(scene, sub_mesh);
			ufbxt_match_obj_mesh(obj, node, sub_mesh, obj_mesh, p_err, scale);
			ufbx_free_mesh(sub_mesh);

			continue;
		}

		if (!(obj->bad_faces || obj->line_faces || obj->point_faces)) {
			ufbxt_assert(obj_mesh->num_faces == mesh->num_faces);
			ufbxt_assert(obj_mesh->num_indices == mesh->num_indices);
		}

		bool check_normals = true;
		if (obj->bad_normals) check_normals = false;
		if ((flags & UFBXT_OBJ_DIFF_FLAG_CHECK_DEFORMED_NORMALS) == 0 && mesh->all_deformers.count > 0) check_normals = false;
		if ((flags & UFBXT_OBJ_DIFF_FLAG_IGNORE_NORMALS) != 0) check_normals = false;

		if (obj->bad_order) {
			ufbxt_match_obj_mesh(obj, node, mesh, obj_mesh, p_err, scale);
		} else {
			size_t obj_face_ix = 0;

			// Assume that the indices are in the same order!
			for (size_t face_ix = 0; face_ix < mesh->num_faces; face_ix++) {
				ufbx_face obj_face = obj_mesh->faces[obj_face_ix];
				ufbx_face face = mesh->faces.data[face_ix];

				if (obj->bad_faces && ufbxt_face_has_duplicate_vertex(mesh, face)) {
					continue;
				} else if (obj->line_faces && face.num_indices == 2) {
					continue;
				} else if (obj->point_faces && face.num_indices == 1) {
					continue;
				} else if (!obj->bad_faces) {
					ufbxt_assert(obj_face.index_begin == face.index_begin);
				}

				ufbxt_assert(obj_face.num_indices == face.num_indices);
				obj_face_ix++;

				for (size_t i = 0; i < face.num_indices; i++) {
					size_t oix = obj_face.index_begin + i;
					size_t fix = face.index_begin + i;

					ufbx_vec3 op = ufbx_get_vertex_vec3(&obj_mesh->vertex_position, oix);
					ufbx_vec3 fp = ufbx_get_vertex_vec3(&mesh->skinned_position, fix);
					ufbx_vec3 on = check_normals ? ufbx_get_vertex_vec3(&obj_mesh->vertex_normal, oix) : ufbx_zero_vec3;
					ufbx_vec3 fn = check_normals ? ufbx_get_vertex_vec3(&mesh->skinned_normal, fix) : ufbx_zero_vec3;

					if (mesh->skinned_is_local) {
						fp = ufbx_transform_position(mat, fp);
						fn = ufbx_transform_direction(&norm_mat, fn);

						ufbx_real fn_len = (ufbx_real)sqrt(fn.x*fn.x + fn.y*fn.y + fn.z*fn.z);
						if (fn_len > 0.00001f) {
							fn.x /= fn_len;
							fn.y /= fn_len;
							fn.z /= fn_len;
						}

						ufbx_real on_len = (ufbx_real)sqrt(on.x*on.x + on.y*on.y + on.z*on.z);
						if (on_len > 0.00001f) {
							on.x /= on_len;
							on.y /= on_len;
							on.z /= on_len;
						}
					}

					if (scale != 1.0) {
						fp.x *= (ufbx_real)scale;
						fp.y *= (ufbx_real)scale;
						fp.z *= (ufbx_real)scale;
						op.x *= (ufbx_real)scale;
						op.y *= (ufbx_real)scale;
						op.z *= (ufbx_real)scale;
					}

					ufbxt_assert_close_vec3(p_err, op, fp);

					if (check_normals && !mesh->generated_normals) {
						ufbx_real on2 = on.x*on.x + on.y*on.y + on.z*on.z;
						ufbx_real fn2 = fn.x*fn.x + fn.y*fn.y + fn.z*fn.z;
						if (on2 > 0.01f && fn2 > 0.01f) {
							ufbxt_assert_close_vec3(p_err, on, fn);
						}
					}

					if (obj_mesh->vertex_uv.exists && !obj->bad_uvs) {
						ufbxt_assert(mesh->vertex_uv.exists);
						ufbx_vec2 ou = ufbx_get_vertex_vec2(&obj_mesh->vertex_uv, oix);
						ufbx_vec2 fu = ufbx_get_vertex_vec2(&mesh->vertex_uv, fix);
						ufbxt_assert_close_vec2(p_err, ou, fu);
					}
				}
			}
		}
	}

	free(obj_nodes);
	free(cat_name_data);
	free(used_nodes);
}

// -- IO

static ufbxt_noinline size_t ufbxt_file_size(const char *name)
{
	FILE *file = fopen(name, "rb");
	if (!file) return 0;
	fseek(file, 0, SEEK_END);
#if defined(_WIN32)
	size_t size = (size_t)_ftelli64(file);
#else
	size_t size = (size_t)ftell(file);
#endif
	fclose(file);
	return size;
}

static ufbxt_noinline void *ufbxt_read_file(const char *name, size_t *p_size)
{
	FILE *file = fopen(name, "rb");
	if (!file) return NULL;

	fseek(file, 0, SEEK_END);
#if defined(_WIN32)
	size_t size = (size_t)_ftelli64(file);
#else
	size_t size = (size_t)ftell(file);
#endif
	fseek(file, 0, SEEK_SET);

	char *data = malloc(size + 1);
	ufbxt_assert(data != NULL);
	size_t num_read = fread(data, 1, size, file);
	fclose(file);

	data[size] = '\0';

	if (num_read != size) {
		ufbxt_assert_fail(__FILE__, __LINE__, "Failed to load file");
	}

	*p_size = size;
	return data;
}

static ufbxt_noinline void *ufbxt_read_file_ex(const char *name, size_t *p_size)
{
	size_t name_len = strlen(name);
	if (name_len >= 3 && !memcmp(name + name_len - 3, ".gz", 3)) {
		size_t gz_size = 0;
		void *gz_data = ufbxt_read_file(name, &gz_size);
		if (!gz_data) return NULL;

		void *result = ufbxt_decompress_gzip(gz_data, gz_size, p_size);
		free(gz_data);

		return result;
	} else {
		return ufbxt_read_file(name, p_size);
	}
}

#endif
