#include "wavefront.h"

#include <assert.h>
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "../debug/logger.h"


wf_alloc_t   wf_alloc   = malloc;
wf_realloc_t wf_realloc = realloc;
wf_free_t    wf_free    = free;


#define WF_FREE_OPTIONAL(x) { if(x) wf_free(x); }


struct wf_parser
{
	char *data;
	u64 size;

	u64 cursor;

	u64 line;
	u64 line_start;

	const char *name; // Used for debug messages. It's not needed otherwise.
};


enum wf_token_type
{
	WF_NONE,

	WF_STRING,
	WF_INTEGER,
	WF_REAL,

	WF_SLASH,

	WF_EOF
};

#define CASE_STRING(x) case x: return #x;
const char *wf_token_type_string(wf_token_type tt)
{
	switch(tt)
	{
		CASE_STRING(WF_NONE)
		CASE_STRING(WF_STRING)
		CASE_STRING(WF_INTEGER)
		CASE_STRING(WF_REAL)
		CASE_STRING(WF_SLASH)
		CASE_STRING(WF_EOF)
		default: return "UNKNOWN";
	}
}

struct wf_string
{
	char *data;
	u32 size;
};

struct wf_token
{
	wf_token_type type;

	union
	{
		wf_string string;
		s64 integer;
		f64 real;
	};

	u64 line;
	u64 line_start;
	u64 start;
	u64 size;
};


#define WF_PRINT_ERROR(parser, message, ...)  \
{  \
	u64 column = (parser)->cursor - (parser)->line_start + 1;  \
  \
	u64 tmp_cursor = (parser)->cursor;  \
	while(tmp_cursor < (parser)->size && !wf_isnewline((parser)->data[tmp_cursor]))  \
		tmp_cursor++;  \
	u64 line_lenght = tmp_cursor - (parser)->line_start;  \
	LOG(LOG_ERROR, "Wavefront parsing error %s %lu,%lu: \n" message "\nLine: \"%.*s\"", (parser)->name, (parser)->line, column, ##__VA_ARGS__, (int)line_lenght, (parser)->data + (parser)->line_start);  \
}

#define WF_PRINT_ERROR_TOKEN(parser, token, message, ...)  \
{  \
	u64 column = (token)->start - (token)->line_start + 1;  \
  \
	u64 tmp_cursor = (token)->line_start;  \
	while(tmp_cursor < (parser)->size && !wf_isnewline((parser)->data[tmp_cursor]))  \
		tmp_cursor++;  \
	u64 line_lenght = tmp_cursor - (token)->line_start;  \
	LOG(LOG_ERROR, "Wavefront parsing error %s %lu,%lu: \n" message "\nLine: \"%.*s\"", (parser)->name, (token)->line, column, ##__VA_ARGS__, (int)line_lenght, (parser)->data + (token)->line_start);  \
}



static void wf_parser_init(wf_parser *p, char *data, u64 size, const char *name)
{
	p->data       = data;
	p->size       = size;
	p->cursor     = 0;
	p->line       = 1;
	p->line_start = 0;
	p->name       = name;
}

static bool wf_isnewline(char c)
{
	// 10 '\n' line feed --- 13 '\r' carriage return
	// For simplicity, we consider only \n as newline.
	// \n will be eaten as normal whitespace.
	return c == 10;
}

static void wf_eat_newline(wf_parser *p)
{
	assert(p->cursor < p->size);
	assert(wf_isnewline(p->data[p->cursor]));

	p->cursor++;
	while(p->cursor < p->size)
	{
		char c = p->data[p->cursor];
		if(c != 13)
			break;
		p->cursor++;
	}

	p->line++;
	p->line_start = p->cursor;
}

static void wf_skip_to_next_line(wf_parser *p)
{
	while(p->cursor < p->size)
	{
		char c = p->data[p->cursor];

		if(wf_isnewline(c))
		{
			wf_eat_newline(p);
			break;
		}

		p->cursor++;
	}
}

// Puts cursor at the newline (end of comment, not start of next token)
static void wf_eat_comment(wf_parser *p)
{
	assert(p->cursor < p->size);
	assert(p->data[p->cursor] == '#');
	wf_skip_to_next_line(p);
}

static void wf_eat_whitespace_and_comments(wf_parser *p)
{
	while(p->cursor < p->size)
	{
		char c = p->data[p->cursor];

		if(c == '#') // Comment
		{
			wf_eat_comment(p);
			continue;
		}

		if(wf_isnewline(c))
		{
			wf_eat_newline(p);
			continue;
		}

		if(!isspace(c))
			break;

		p->cursor++;
	}
}

static s64 wf_parse_integer(wf_parser *p)
{
	assert(p->cursor < p->size);
	assert(isdigit(p->data[p->cursor]));

	s64 integer = 0;
	while(p->cursor < p->size)
	{
		char c = p->data[p->cursor];

		if(!isdigit(c))
			break;

		integer = integer * 10 + (c - '0');

		p->cursor++;
	}

	return integer;
}

static f64 wf_parse_decimal_part(wf_parser *p)
{
	assert(p->cursor < p->size);
	assert(isdigit(p->data[p->cursor]));

	f64 decimal = 0;
	f64 factor = 0.1;
	while(p->cursor < p->size)
	{
		char c = p->data[p->cursor];

		if(!isdigit(c))
			break;

		decimal += factor * (c - '0');
		factor *= 0.1;

		p->cursor++;
	}

	return decimal;
}

static wf_string wf_parse_string(wf_parser *p)
{
	assert(p->cursor < p->size);

	wf_string s;

	s.data = p->data + p->cursor;
	s.size = 0;

	while(p->cursor < p->size)
	{
		char c = p->data[p->cursor];
		if(isspace(c) || c == '#')
			break;
		p->cursor++;
	}
	s.size = p->data + p->cursor - s.data;

	return s;
}

#define WF_PARSE_FORCE_STRING 1

static wf_token wf_parse_next(wf_parser *p, u32 flags = 0)
{
	wf_token token;
	token.type = WF_NONE;

	wf_eat_whitespace_and_comments(p);
	if(p->cursor >= p->size)
	{
		token.type = WF_EOF;
		return token;
	}

	token.line       = p->line;
	token.line_start = p->line_start;
	token.start      = p->cursor;
	token.size       = 0;
	char c = p->data[p->cursor];

	if(flags & WF_PARSE_FORCE_STRING)
	{
		token.type = WF_STRING;
		token.string = wf_parse_string(p);
	}
	else if(c == '-' || isdigit(c)) // Integer or real
	{
		s64 sign = 1;
		if(c == '-')
		{
			sign = -1;
			p->cursor++;
			if(p->cursor >= p->size)
			{
				WF_PRINT_ERROR(p, "End of file reached while parsing number.");
				token.type = WF_NONE;
				return token;
			}

			c = p->data[p->cursor];
			if(!isdigit(c))
			{
				WF_PRINT_ERROR(p, "Unexpected character after '-' while parsing number.");
				token.type = WF_NONE;
				return token;
			}
		}
		// Parse integer part first, then if it's a real, we convert it and add the decimal part
		token.type = WF_INTEGER;
		token.integer = sign * wf_parse_integer(p);
		if(p->cursor < p->size)
		{
			if(p->data[p->cursor] == '.') // Real
			{
				token.type = WF_REAL;
				token.real = token.integer;
				p->cursor++;
				// We are parsing a real number. We should have at least 1 digit after the '.'
				if(p->cursor >= p->size)
				{
					WF_PRINT_ERROR(p, "End of file reached while parsing real number.");
					token.type = WF_NONE;
					return token;
				}
				if(!isdigit(p->data[p->cursor]))
				{
					WF_PRINT_ERROR(p, "Unexpected character after '.' while parsing real number.");
					token.type = WF_NONE;
					return token;
				}
				token.real += sign * wf_parse_decimal_part(p);
			}
		}
	}
	else if(isalpha(c)) // Text
	{
		token.type = WF_STRING;
		token.string = wf_parse_string(p);
	}
	else if(c == '/')
	{
		token.type = WF_SLASH;
		p->cursor++;
	}
	else
	{
		WF_PRINT_ERROR(p, "Unrecognized token (starting with \'%c\').", c);
	}

	token.size = p->cursor - token.start;

	return token;
}

static wf_token wf_peek_next(wf_parser *p, u32 flags = 0)
{
	wf_parser copy = *p;
	return wf_parse_next(&copy, flags);
}

static bool wf_string_equal(wf_string *wfs, const char *s)
{
	u64 i = 0;
	while(i < wfs->size && s[i] != 0)
	{
		if(wfs->data[i] != s[i])
			return false;
		i++;
	}
	if(i != wfs->size || s[i] != 0)
		return false;

	return true;
}

bool wf_parse_obj(char *data, u64 size, const char *name, wf_obj_file *return_obj)
{
	wf_obj_file result;
	wf_object *curr_object;
	wf_parser p;
	wf_token token;

	wf_parser_init(&p, data, size, name);

	{
		u64 size = strlen(name);
		result.name = (char*)wf_alloc(size + 1);
		memcpy(result.name, name, size);
		result.name[size] = 0; // zero-terminated string
	}

	{
		result.mtllib_names = NULL;
		result.mtllib_names_count = 0;

		result.objects = NULL;
		result.objects_count = 0;
	}

	wf_size vertices_capacity;
	wf_size texture_coords_capacity;
	wf_size normals_capacity;
	wf_size faces_capacity;
	{
		result.objects_count = 1;
		result.objects = (wf_object*)wf_alloc(1 * sizeof(wf_object));
		curr_object = result.objects;

		curr_object->name = NULL;
		curr_object->material_name = NULL;

		vertices_capacity       = 32;
		texture_coords_capacity = 32;
		normals_capacity        = 32;
		faces_capacity          = 32;

		curr_object->vertices       =      (v4*)wf_alloc(vertices_capacity * sizeof(v4));
		curr_object->texture_coords =      (v3*)wf_alloc(texture_coords_capacity * sizeof(v3));
		curr_object->normals        =      (v3*)wf_alloc(normals_capacity * sizeof(v3));
		curr_object->faces          = (wf_face*)wf_alloc(faces_capacity * sizeof(wf_face));
		curr_object->vertices_count       = 0;
		curr_object->texture_coords_count = 0;
		curr_object->normals_count        = 0;
		curr_object->faces_count          = 0;
	}


	token = wf_parse_next(&p);
	while(token.type != WF_EOF && token.type != WF_NONE)
	{
		// At the start of the line we expect to find a string/command,
		// followed by its parameters.
		if(token.type != WF_STRING)
		{
			WF_PRINT_ERROR_TOKEN(&p, &token, "Expected string at start of line. Found: %s \"%.*s\"", wf_token_type_string(token.type), (int)token.size, p.data + token.start);
			goto error_cleanup_obj;
		}

		if(wf_string_equal(&token.string, "o")) // Object name
		{
			bool prev_object_empty = true;
			prev_object_empty &= (curr_object->name == NULL);
			prev_object_empty &= (curr_object->material_name == NULL);
			prev_object_empty &= (curr_object->vertices_count       == 0);
			prev_object_empty &= (curr_object->texture_coords_count == 0);
			prev_object_empty &= (curr_object->normals_count        == 0);
			prev_object_empty &= (curr_object->faces_count          == 0);

			if(prev_object_empty)
			{
				// Use previously allocated space
			}
			else
			{
				// Resize the old object to the smallest size possible
				if(vertices_capacity != curr_object->vertices_count)
					curr_object->vertices = (v4*)wf_realloc(curr_object->vertices, curr_object->vertices_count * sizeof(v4));
				if(texture_coords_capacity != curr_object->texture_coords_count)
					curr_object->texture_coords = (v3*)wf_realloc(curr_object->texture_coords, curr_object->texture_coords_count * sizeof(v3));
				if(normals_capacity != curr_object->normals_count)
					curr_object->normals = (v3*)wf_realloc(curr_object->normals, curr_object->normals_count * sizeof(v3));
				if(faces_capacity != curr_object->faces_count)
					curr_object->faces = (wf_face*)wf_realloc(curr_object->faces, curr_object->faces_count * sizeof(wf_face));

				// Allocate space for new object
				result.objects_count++;
				result.objects = (wf_object*)wf_realloc(result.objects, result.objects_count * sizeof(wf_object));
				curr_object = result.objects + (result.objects_count - 1);

				curr_object->name = NULL;
				curr_object->material_name = NULL;

				vertices_capacity       = 32;
				texture_coords_capacity = 32;
				normals_capacity        = 32;
				faces_capacity          = 32;

				curr_object->vertices       =      (v4*)wf_alloc(vertices_capacity * sizeof(v4));
				curr_object->texture_coords =      (v3*)wf_alloc(texture_coords_capacity * sizeof(v3));
				curr_object->normals        =      (v3*)wf_alloc(normals_capacity * sizeof(v3));
				curr_object->faces          = (wf_face*)wf_alloc(faces_capacity * sizeof(wf_face));
				curr_object->vertices_count       = 0;
				curr_object->texture_coords_count = 0;
				curr_object->normals_count        = 0;
				curr_object->faces_count          = 0;
			}

			token = wf_parse_next(&p, WF_PARSE_FORCE_STRING);
			if(token.type != WF_STRING)
			{
				WF_PRINT_ERROR_TOKEN(&p, &token, "Expected object name after \"o\". Found: \"%.*s\".", (int)token.size, p.data + token.start);
				goto error_cleanup_obj;
			}
			curr_object->name = (char*)wf_alloc(token.string.size + 1);
			memcpy(curr_object->name, token.string.data, token.string.size);
			curr_object->name[token.string.size] = 0; // zero-terminated string

			wf_skip_to_next_line(&p);
		}
		else if(wf_string_equal(&token.string, "v")) // Vertices
		{
			// A vertex is specified by 3 or 4 real numbers (x y z [w])
			v4 vertex;
			bool error = false;

			if(!error)
			{
				token = wf_parse_next(&p);
				if(token.type == WF_REAL)
					vertex.x = token.real;
				else
					error = true;
			}

			if(!error)
			{
				token = wf_parse_next(&p);
				if(token.type == WF_REAL)
					vertex.y = token.real;
				else
					error = true;
			}

			if(!error)
			{
				token = wf_parse_next(&p);
				if(token.type == WF_REAL)
					vertex.z = token.real;
				else
					error = true;
			}

			// w optional
			if(!error)
			{
				wf_token peeked = wf_peek_next(&p);
				if(peeked.type == WF_REAL)
				{
					token = wf_parse_next(&p);
					vertex.w = token.real;
				}
				else
				{
					vertex.w = 1.0; // Default value if omitted
				}
			}

			if(error)
			{
				// @Correctness: This prints the line after the one that has the actual error (instead of the correct one).
				WF_PRINT_ERROR_TOKEN(&p, &token, "Expected 3 or 4 real numbers when reading vertex data. Found: \"%.*s\".", (int)token.size, p.data + token.start);
				goto error_cleanup_obj;
			}

			// @Correctness: Right now, we skip everything we find after reading the last component.
			// We should report an error if there is any data before the next line.
			wf_skip_to_next_line(&p);

			if(vertices_capacity < curr_object->vertices_count + 1)
			{
				vertices_capacity *= 2;
				curr_object->vertices = (v4*)wf_realloc(curr_object->vertices, vertices_capacity * sizeof(v4));
			}
			curr_object->vertices[curr_object->vertices_count] = vertex;
			curr_object->vertices_count++;
		}
		else if(wf_string_equal(&token.string, "vt")) // Texture coordinates
		{
			// A texture coordinate is specified by 1, 2 or 3 real numbers (u [v] [w])
			v3 texture_coord;
			bool error = false;

			if(!error)
			{
				token = wf_parse_next(&p);
				if(token.type == WF_REAL)
					texture_coord.u = token.real;
				else
					error = true;
			}

			// v optional
			if(!error)
			{
				wf_token peeked = wf_peek_next(&p);
				if(peeked.type == WF_REAL)
				{
					token = wf_parse_next(&p);
					texture_coord.v = token.real;
				}
				else
				{
					texture_coord.v = 0.0; // Default value if omitted
					texture_coord.w_ = 0.0; // Default value if omitted
				}
			}

			// w optional
			if(!error)
			{
				wf_token peeked = wf_peek_next(&p);
				if(peeked.type == WF_REAL)
				{
					token = wf_parse_next(&p);
					texture_coord.w_ = token.real;
				}
				else
				{
					texture_coord.w_ = 0.0; // Default value if omitted
				}
			}

			if(error)
			{
				// @Correctness: This prints the line after the one that has the actual error (instead of the correct one).
				WF_PRINT_ERROR_TOKEN(&p, &token, "Expected 1, 2 or 3 real numbers when reading texture coordinates. Found: \"%.*s\".", (int)token.size, p.data + token.start);
				goto error_cleanup_obj;
			}

			// @Correctness: Right now, we skip everything we find after reading the last component.
			// We should report an error if there is any data before the next line.
			wf_skip_to_next_line(&p);

			if(texture_coords_capacity < curr_object->texture_coords_count + 1)
			{
				texture_coords_capacity *= 2;
				curr_object->texture_coords = (v3*)wf_realloc(curr_object->texture_coords, texture_coords_capacity * sizeof(v3));
			}
			curr_object->texture_coords[curr_object->texture_coords_count] = texture_coord;
			curr_object->texture_coords_count++;
		}
		else if(wf_string_equal(&token.string, "vn")) // Normals
		{
			// A normal is specified by 3 real numbers (x y z)
			v3 normal;
			bool error = false;

			if(!error)
			{
				token = wf_parse_next(&p);
				if(token.type == WF_REAL)
					normal.x = token.real;
				else
					error = true;
			}

			if(!error)
			{
				token = wf_parse_next(&p);
				if(token.type == WF_REAL)
					normal.y = token.real;
				else
					error = true;
			}

			if(!error)
			{
				token = wf_parse_next(&p);
				if(token.type == WF_REAL)
					normal.z = token.real;
				else
					error = true;
			}

			if(error)
			{
				// @Correctness: This prints the line after the one that has the actual error (instead of the correct one).
				WF_PRINT_ERROR_TOKEN(&p, &token, "Expected 3 real numbers when reading normal data. Found: \"%.*s\".", (int)token.size, p.data + token.start);
				goto error_cleanup_obj;
			}

			wf_skip_to_next_line(&p);

			if(normals_capacity < curr_object->normals_count + 1)
			{
				normals_capacity *= 2;
				curr_object->normals = (v3*)wf_realloc(curr_object->normals, normals_capacity * sizeof(v3));
			}
			curr_object->normals[curr_object->normals_count] = normal;
			curr_object->normals_count++;
		}
		else if(wf_string_equal(&token.string, "f")) // Faces
		{
			// A face is specified by 3 or more vertices/texture_coords/normal. Only the vertex index is mandatory.
			// For now we read only 3, ignoring the rest.
			wf_face face;

			u32 count = 0;
			token = wf_peek_next(&p);
			while(token.type == WF_INTEGER)
			{
				token = wf_parse_next(&p); // We peeked _token_, so now we have to actually advance the parser.

				wf_face_index fi;
				fi.vertex  = 0;
				fi.texture = 0;
				fi.normal  = 0;


				fi.vertex = token.integer;

				// Check for "/" separator (optional)
				token = wf_peek_next(&p);
				if(token.type == WF_SLASH)
				{
					token = wf_parse_next(&p);
					// Check for texture coordinate index (optional)
					token = wf_peek_next(&p);
					if(token.type != WF_INTEGER && token.type != WF_SLASH)
					{
						WF_PRINT_ERROR_TOKEN(&p, &token, "Expected texture coordinate index after vertex index. Found: \"%.*s\".", (int)token.size, p.data + token.start);
						goto error_cleanup_obj;
					}

					if(token.type == WF_INTEGER)
					{
						token = wf_parse_next(&p);
						fi.texture = token.integer;
						token = wf_peek_next(&p);
					}

					// Check for "/" separator (optional)
					if(token.type == WF_SLASH)
					{
						token = wf_parse_next(&p);
						// Check for normal index (optional)
						token = wf_peek_next(&p);
						if(token.type != WF_INTEGER)
						{
							WF_PRINT_ERROR_TOKEN(&p, &token, "Expected normal index after texture coordinate index. Found: \"%.*s\".", (int)token.size, p.data + token.start);
							goto error_cleanup_obj;
						}
						if(!isdigit(p.data[p.cursor]))
						{
							WF_PRINT_ERROR(&p, "Expected normal index after texture coordinate index. Found nothing.");
							goto error_cleanup_obj;
						}

						token = wf_parse_next(&p);
						fi.normal = token.integer;
					}

				}

				if(count < 3)
					face.indices[count] = fi;

				count++;
				token = wf_peek_next(&p);
			}

			if(count < 3)
			{
				WF_PRINT_ERROR(&p, "Expected 3 or more indices numbers when reading face data. Found only %u.", count);
				goto error_cleanup_obj;
			}

			// @Correctness: Right now, we skip everything we find after reading the last component.
			// We should report an error if there is any data before the next line.
			wf_skip_to_next_line(&p);

			if(faces_capacity < curr_object->faces_count + 1)
			{
				faces_capacity *= 2;
				curr_object->faces = (wf_face*)wf_realloc(curr_object->faces, faces_capacity * sizeof(wf_face));
			}
			curr_object->faces[curr_object->faces_count] = face;
			curr_object->faces_count++;
		}
		else if(wf_string_equal(&token.string, "mtllib")) // Material library file name
		{
			token = wf_parse_next(&p, WF_PARSE_FORCE_STRING);
			if(token.type != WF_STRING)
			{
				WF_PRINT_ERROR_TOKEN(&p, &token, "Expected file name after \"mtllib\". Found: \"%.*s\".", (int)token.size, p.data + token.start);
				goto error_cleanup_obj;
			}

			if(result.mtllib_names == NULL)
				result.mtllib_names = (char**)wf_alloc((result.mtllib_names_count + 1) * sizeof(char*));
			else
				result.mtllib_names = (char**)wf_realloc(result.mtllib_names, (result.mtllib_names_count + 1) * sizeof(char*));
			result.mtllib_names[result.mtllib_names_count] = (char*)wf_alloc(token.string.size + 1);
			memcpy(result.mtllib_names[result.mtllib_names_count], token.string.data, token.string.size);
			result.mtllib_names[result.mtllib_names_count][token.string.size] = 0; // zero-terminated
			result.mtllib_names_count++;

			wf_skip_to_next_line(&p);
		}
		else if(wf_string_equal(&token.string, "usemtl")) // Material name
		{
			token = wf_parse_next(&p, WF_PARSE_FORCE_STRING);
			if(token.type != WF_STRING)
			{
				WF_PRINT_ERROR_TOKEN(&p, &token, "Expected material name after \"usemtl\". Found: \"%.*s\".", (int)token.size, p.data + token.start);
				goto error_cleanup_obj;
			}
			curr_object->material_name = (char*)wf_alloc(token.string.size + 1);
			memcpy(curr_object->material_name, token.string.data, token.string.size);
			curr_object->material_name[token.string.size] = 0; // zero-terminated string

			wf_skip_to_next_line(&p);
		}
		else
		{
			WF_PRINT_ERROR_TOKEN(&p, &token, "Unrecognized command \"%.*s\". Skipping to next line.", (int)token.string.size, token.string.data);
			wf_skip_to_next_line(&p);
		}

		token = wf_parse_next(&p);
	}

	// Resize the object to the smallest size possible
	{
		if(vertices_capacity != curr_object->vertices_count)
			curr_object->vertices = (v4*)wf_realloc(curr_object->vertices, curr_object->vertices_count * sizeof(v4));
		if(texture_coords_capacity != curr_object->texture_coords_count)
			curr_object->texture_coords = (v3*)wf_realloc(curr_object->texture_coords, curr_object->texture_coords_count * sizeof(v3));
		if(normals_capacity != curr_object->normals_count)
			curr_object->normals = (v3*)wf_realloc(curr_object->normals, curr_object->normals_count * sizeof(v3));
		if(faces_capacity != curr_object->faces_count)
			curr_object->faces = (wf_face*)wf_realloc(curr_object->faces, curr_object->faces_count * sizeof(wf_face));
	}

	*return_obj = result;
	return true;

error_cleanup_obj:
	wf_cleanup_obj(&result);
	return false;
}


bool wf_parse_mtl(char *data, u64 size, const char *name, wf_mtl_file *return_mtl)
{
	wf_mtl_file result;
	wf_material *curr_material;
	wf_parser p;
	wf_token token;

	wf_parser_init(&p, data, size, name);

	{
		u64 size = strlen(name);
		result.name = (char*)wf_alloc(size + 1);
		memcpy(result.name, name, size);
		result.name[size] = 0; // zero-terminated string
	}

	{
		result.materials = NULL;
		result.materials_count = 0;
	}


	token = wf_parse_next(&p);
	while(token.type != WF_EOF && token.type != WF_NONE)
	{
		// At the start of the line we expect to find a string/command,
		// followed by its parameters.
		if(token.type != WF_STRING)
		{
			WF_PRINT_ERROR_TOKEN(&p, &token, "Expected string at start of line. Found: %s \"%.*s\"", wf_token_type_string(token.type), (int)token.size, p.data + token.start);
			goto error_cleanup_mtl;
		}

		if(wf_string_equal(&token.string, "newmtl")) // Material name
		{
			// Initialize new material
			{
				result.materials_count++;
				if(result.materials == NULL)
					result.materials = (wf_material*)wf_alloc(result.materials_count * sizeof(wf_material));
				else
					result.materials = (wf_material*)wf_realloc(result.materials, result.materials_count * sizeof(wf_material));
				curr_material = result.materials + (result.materials_count - 1);
			}

			{
				curr_material->name = NULL;
				curr_material->Ka = v3{0, 0, 0};
				curr_material->Kd = v3{0, 0, 0};
				curr_material->Ke = v3{0, 0, 0};
				curr_material->Ks = v3{0, 0, 0};
				curr_material->Ns = 0;
				curr_material->d = 1.0;
				curr_material->Ni = 1.0;
				curr_material->illum = 0;
				curr_material->map_Ka = NULL;
				curr_material->map_Kd = NULL;
				curr_material->map_Ke = NULL;
				curr_material->map_Ks = NULL;
				curr_material->map_Ns = NULL;
				curr_material->map_d = NULL;
				curr_material->bump = NULL;
				curr_material->disp = NULL;
				curr_material->decal = NULL;
			}

			token = wf_parse_next(&p, WF_PARSE_FORCE_STRING);
			if(token.type != WF_STRING)
			{
				WF_PRINT_ERROR_TOKEN(&p, &token, "Expected material name after \"newmtl\". Found: \"%.*s\".", (int)token.size, p.data + token.start);
				goto error_cleanup_mtl;
			}
			curr_material->name = (char*)wf_alloc(token.string.size + 1);
			memcpy(curr_material->name, token.string.data, token.string.size);
			curr_material->name[token.string.size] = 0; // zero-terminated string

			wf_skip_to_next_line(&p);
		}
		else if(wf_string_equal(&token.string, "Ka"))
		{
			// A color is specified by 3 real numbers (x y z)
			v3 color;
			bool error = false;

			if(!error)
			{
				token = wf_parse_next(&p);
				if(token.type == WF_REAL)
					color.r = token.real;
				else
					error = true;
			}

			if(!error)
			{
				token = wf_parse_next(&p);
				if(token.type == WF_REAL)
					color.g = token.real;
				else
					error = true;
			}

			if(!error)
			{
				token = wf_parse_next(&p);
				if(token.type == WF_REAL)
					color.b = token.real;
				else
					error = true;
			}

			if(error)
			{
				// @Correctness: This prints the line after the one that has the actual error (instead of the correct one).
				WF_PRINT_ERROR_TOKEN(&p, &token, "Expected 3 real numbers when reading normal data. Found: \"%.*s\".", (int)token.size, p.data + token.start);
				goto error_cleanup_mtl;
			}

			wf_skip_to_next_line(&p);

			curr_material->Ka = color;
		}
		else if(wf_string_equal(&token.string, "Kd"))
		{
			// A color is specified by 3 real numbers (x y z)
			v3 color;
			bool error = false;

			if(!error)
			{
				token = wf_parse_next(&p);
				if(token.type == WF_REAL)
					color.r = token.real;
				else
					error = true;
			}

			if(!error)
			{
				token = wf_parse_next(&p);
				if(token.type == WF_REAL)
					color.g = token.real;
				else
					error = true;
			}

			if(!error)
			{
				token = wf_parse_next(&p);
				if(token.type == WF_REAL)
					color.b = token.real;
				else
					error = true;
			}

			if(error)
			{
				// @Correctness: This prints the line after the one that has the actual error (instead of the correct one).
				WF_PRINT_ERROR_TOKEN(&p, &token, "Expected 3 real numbers when reading normal data. Found: \"%.*s\".", (int)token.size, p.data + token.start);
				goto error_cleanup_mtl;
			}

			wf_skip_to_next_line(&p);

			curr_material->Kd = color;
		}

		else if(wf_string_equal(&token.string, "Ke"))
		{
			// A color is specified by 3 real numbers (x y z)
			v3 color;
			bool error = false;

			if(!error)
			{
				token = wf_parse_next(&p);
				if(token.type == WF_REAL)
					color.r = token.real;
				else
					error = true;
			}

			if(!error)
			{
				token = wf_parse_next(&p);
				if(token.type == WF_REAL)
					color.g = token.real;
				else
					error = true;
			}

			if(!error)
			{
				token = wf_parse_next(&p);
				if(token.type == WF_REAL)
					color.b = token.real;
				else
					error = true;
			}

			if(error)
			{
				// @Correctness: This prints the line after the one that has the actual error (instead of the correct one).
				WF_PRINT_ERROR_TOKEN(&p, &token, "Expected 3 real numbers when reading normal data. Found: \"%.*s\".", (int)token.size, p.data + token.start);
				goto error_cleanup_mtl;
			}

			wf_skip_to_next_line(&p);

			curr_material->Ke = color;
		}
		else if(wf_string_equal(&token.string, "Ks"))
		{
			// A color is specified by 3 real numbers (x y z)
			v3 color;
			bool error = false;

			if(!error)
			{
				token = wf_parse_next(&p);
				if(token.type == WF_REAL)
					color.r = token.real;
				else
					error = true;
			}

			if(!error)
			{
				token = wf_parse_next(&p);
				if(token.type == WF_REAL)
					color.g = token.real;
				else
					error = true;
			}

			if(!error)
			{
				token = wf_parse_next(&p);
				if(token.type == WF_REAL)
					color.b = token.real;
				else
					error = true;
			}

			if(error)
			{
				// @Correctness: This prints the line after the one that has the actual error (instead of the correct one).
				WF_PRINT_ERROR_TOKEN(&p, &token, "Expected 3 real numbers when reading normal data. Found: \"%.*s\".", (int)token.size, p.data + token.start);
				goto error_cleanup_mtl;
			}

			wf_skip_to_next_line(&p);

			curr_material->Ks = color;
		}
		else if(wf_string_equal(&token.string, "Ns"))
		{
			token = wf_parse_next(&p);
			if(token.type != WF_REAL)
			{
				WF_PRINT_ERROR_TOKEN(&p, &token, "Expected real number after \"Ks\". Found: \"%.*s\".", (int)token.size, p.data + token.start);
				goto error_cleanup_mtl;
			}

			curr_material->Ns = token.real;

			wf_skip_to_next_line(&p);
		}
		else if(wf_string_equal(&token.string, "d"))
		{
			token = wf_parse_next(&p);
			if(token.type != WF_REAL)
			{
				WF_PRINT_ERROR_TOKEN(&p, &token, "Expected real number after \"d\". Found: \"%.*s\".", (int)token.size, p.data + token.start);
				goto error_cleanup_mtl;
			}

			curr_material->d = token.real;

			wf_skip_to_next_line(&p);
		}
		else if(wf_string_equal(&token.string, "Ni"))
		{
			token = wf_parse_next(&p);
			if(token.type != WF_REAL)
			{
				WF_PRINT_ERROR_TOKEN(&p, &token, "Expected real number after \"Ni\". Found: \"%.*s\".", (int)token.size, p.data + token.start);
				goto error_cleanup_mtl;
			}

			curr_material->Ni = token.real;

			wf_skip_to_next_line(&p);
		}
		else if(wf_string_equal(&token.string, "illum"))
		{
			token = wf_parse_next(&p);
			if(token.type != WF_INTEGER)
			{
				WF_PRINT_ERROR_TOKEN(&p, &token, "Expected integer number after \"illum\". Found: \"%.*s\".", (int)token.size, p.data + token.start);
				goto error_cleanup_mtl;
			}

			curr_material->illum = token.integer;

			wf_skip_to_next_line(&p);
		}
		else if(wf_string_equal(&token.string, "map_Ka"))
		{
			token = wf_parse_next(&p, WF_PARSE_FORCE_STRING);
			if(token.type != WF_STRING)
			{
				WF_PRINT_ERROR_TOKEN(&p, &token, "Expected texture name after \"map_Ka\". Found: \"%.*s\".", (int)token.size, p.data + token.start);
				goto error_cleanup_mtl;
			}
			curr_material->map_Ka = (char*)wf_alloc(token.string.size + 1);
			memcpy(curr_material->map_Ka, token.string.data, token.string.size);
			curr_material->map_Ka[token.string.size] = 0; // zero-terminated string

			wf_skip_to_next_line(&p);
		}
		else if(wf_string_equal(&token.string, "map_Kd"))
		{
			token = wf_parse_next(&p, WF_PARSE_FORCE_STRING);
			if(token.type != WF_STRING)
			{
				WF_PRINT_ERROR_TOKEN(&p, &token, "Expected texture name after \"map_Kd\". Found: \"%.*s\".", (int)token.size, p.data + token.start);
				goto error_cleanup_mtl;
			}
			curr_material->map_Kd = (char*)wf_alloc(token.string.size + 1);
			memcpy(curr_material->map_Kd, token.string.data, token.string.size);
			curr_material->map_Kd[token.string.size] = 0; // zero-terminated string

			wf_skip_to_next_line(&p);
		}
		else if(wf_string_equal(&token.string, "map_Ke"))
		{
			token = wf_parse_next(&p, WF_PARSE_FORCE_STRING);
			if(token.type != WF_STRING)
			{
				WF_PRINT_ERROR_TOKEN(&p, &token, "Expected texture name after \"map_Ke\". Found: \"%.*s\".", (int)token.size, p.data + token.start);
				goto error_cleanup_mtl;
			}
			curr_material->map_Ke = (char*)wf_alloc(token.string.size + 1);
			memcpy(curr_material->map_Ke, token.string.data, token.string.size);
			curr_material->map_Ke[token.string.size] = 0; // zero-terminated string

			wf_skip_to_next_line(&p);
		}
		else if(wf_string_equal(&token.string, "map_Ks"))
		{
			token = wf_parse_next(&p, WF_PARSE_FORCE_STRING);
			if(token.type != WF_STRING)
			{
				WF_PRINT_ERROR_TOKEN(&p, &token, "Expected texture name after \"map_Ks\". Found: \"%.*s\".", (int)token.size, p.data + token.start);
				goto error_cleanup_mtl;
			}
			curr_material->map_Ks = (char*)wf_alloc(token.string.size + 1);
			memcpy(curr_material->map_Ks, token.string.data, token.string.size);
			curr_material->map_Ks[token.string.size] = 0; // zero-terminated string

			wf_skip_to_next_line(&p);
		}
		else if(wf_string_equal(&token.string, "map_Ns"))
		{
			token = wf_parse_next(&p, WF_PARSE_FORCE_STRING);
			if(token.type != WF_STRING)
			{
				WF_PRINT_ERROR_TOKEN(&p, &token, "Expected texture name after \"map_Ns\". Found: \"%.*s\".", (int)token.size, p.data + token.start);
				goto error_cleanup_mtl;
			}
			curr_material->map_Ns = (char*)wf_alloc(token.string.size + 1);
			memcpy(curr_material->map_Ns, token.string.data, token.string.size);
			curr_material->map_Ns[token.string.size] = 0; // zero-terminated string

			wf_skip_to_next_line(&p);
		}
		else if(wf_string_equal(&token.string, "map_d"))
		{
			token = wf_parse_next(&p, WF_PARSE_FORCE_STRING);
			if(token.type != WF_STRING)
			{
				WF_PRINT_ERROR_TOKEN(&p, &token, "Expected texture name after \"map_d\". Found: \"%.*s\".", (int)token.size, p.data + token.start);
				goto error_cleanup_mtl;
			}
			curr_material->map_d = (char*)wf_alloc(token.string.size + 1);
			memcpy(curr_material->map_d, token.string.data, token.string.size);
			curr_material->map_d[token.string.size] = 0; // zero-terminated string

			wf_skip_to_next_line(&p);
		}
		else if(wf_string_equal(&token.string, "bump"))
		{
			token = wf_parse_next(&p, WF_PARSE_FORCE_STRING);
			if(token.type != WF_STRING)
			{
				WF_PRINT_ERROR_TOKEN(&p, &token, "Expected texture name after \"bump\". Found: \"%.*s\".", (int)token.size, p.data + token.start);
				goto error_cleanup_mtl;
			}
			curr_material->bump = (char*)wf_alloc(token.string.size + 1);
			memcpy(curr_material->bump, token.string.data, token.string.size);
			curr_material->bump[token.string.size] = 0; // zero-terminated string

			wf_skip_to_next_line(&p);
		}
		else if(wf_string_equal(&token.string, "disp"))
		{
			token = wf_parse_next(&p, WF_PARSE_FORCE_STRING);
			if(token.type != WF_STRING)
			{
				WF_PRINT_ERROR_TOKEN(&p, &token, "Expected texture name after \"disp\". Found: \"%.*s\".", (int)token.size, p.data + token.start);
				goto error_cleanup_mtl;
			}
			curr_material->disp = (char*)wf_alloc(token.string.size + 1);
			memcpy(curr_material->disp, token.string.data, token.string.size);
			curr_material->disp[token.string.size] = 0; // zero-terminated string

			wf_skip_to_next_line(&p);
		}
		else if(wf_string_equal(&token.string, "decal"))
		{
			token = wf_parse_next(&p, WF_PARSE_FORCE_STRING);
			if(token.type != WF_STRING)
			{
				WF_PRINT_ERROR_TOKEN(&p, &token, "Expected texture name after \"decal\". Found: \"%.*s\".", (int)token.size, p.data + token.start);
				goto error_cleanup_mtl;
			}
			curr_material->decal = (char*)wf_alloc(token.string.size + 1);
			memcpy(curr_material->decal, token.string.data, token.string.size);
			curr_material->decal[token.string.size] = 0; // zero-terminated string

			wf_skip_to_next_line(&p);
		}
		else
		{
			WF_PRINT_ERROR_TOKEN(&p, &token, "Unrecognized command \"%.*s\". Skipping to next line.", (int)token.string.size, token.string.data);
			wf_skip_to_next_line(&p);
		}

		token = wf_parse_next(&p);
	}

	*return_mtl = result;
	return true;

error_cleanup_mtl:
	wf_cleanup_mtl(&result);
	return false;
}



void wf_cleanup_obj(wf_obj_file *obj_file)
{
	// @Performance: Batch allocation for fast alloc/free
	for(wf_size i = 0; i < obj_file->objects_count; i++)
	{
		WF_FREE_OPTIONAL(obj_file->objects[i].name);
		wf_free(obj_file->objects[i].vertices);
		wf_free(obj_file->objects[i].texture_coords);
		wf_free(obj_file->objects[i].normals);
		wf_free(obj_file->objects[i].faces);
		WF_FREE_OPTIONAL(obj_file->objects[i].material_name);
	}

	for(wf_size i = 0; i < obj_file->mtllib_names_count; i++)
		wf_free(obj_file->mtllib_names[i]);
	WF_FREE_OPTIONAL(obj_file->mtllib_names);
	wf_free(obj_file->name);
	wf_free(obj_file->objects);
}

void wf_cleanup_mtl(wf_mtl_file *mtl_file)
{
	// @Performance: Batch allocation for fast alloc/free
	for(wf_size i = 0; i < mtl_file->materials_count; i++)
	{
		WF_FREE_OPTIONAL(mtl_file->materials[i].name);
		WF_FREE_OPTIONAL(mtl_file->materials[i].map_Ka);
		WF_FREE_OPTIONAL(mtl_file->materials[i].map_Kd);
		WF_FREE_OPTIONAL(mtl_file->materials[i].map_Ke);
		WF_FREE_OPTIONAL(mtl_file->materials[i].map_Ks);
		WF_FREE_OPTIONAL(mtl_file->materials[i].map_Ns);
		WF_FREE_OPTIONAL(mtl_file->materials[i].map_d);
		WF_FREE_OPTIONAL(mtl_file->materials[i].bump);
		WF_FREE_OPTIONAL(mtl_file->materials[i].disp);
		WF_FREE_OPTIONAL(mtl_file->materials[i].decal);
	}

	wf_free(mtl_file->name);
	wf_free(mtl_file->materials);
}
