#include <stdlib.h> // malloc
#include <stdio.h> // fprintf, stderr
#include <string.h> // memcmp
#include <zlib.h>
#include <stdbool.h>

#include <SDL2/SDL.h>

#include "libplum.h"
#include "zip.h"
#include "loader.h"
#include "util.h"
#include "main.h"
#include "collision.h"

static void *inflateWrapped(void *const restrict data, uint32_t const outsize);

typedef enum {
	FILE_EXT_UNKNOWN,
	FILE_EXT_TEXTURE,
	FILE_EXT_MAP,
	FILE_EXT_COLLISION,
} ext_T;

ext_T get_extension_type(char *extension, int length);

struct chaos { // why "chaos"? idk, naming variables is hard
	name_T name;
	ext_T extension;
	void *data;
	size_t size;
};

struct blob_collection {
	size_t count;
	struct blob_item {
		struct blob blob;
		name_T name;
	} *items;
} textures = {0, NULL}, maps = {0, NULL}, collisions = {0, NULL};

struct fun_collection {
	size_t count;
	struct fun_item {
		struct funs funs;
		name_T name;
	} *items;
} funs = {0, NULL};

static int name_compare(name_T a, name_T b) {
	return strcmp(a, b);
	// return (a > b) - (a < b);
}

// like binary search, but its made for finding where to insert a new member
void *bsearchinsertposition(const void *key, void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *), int *found) {
	int L = 0, R = nmemb - 1, result, member;
	if (nmemb == 0) {
		if (found != NULL) {
			*found = 0;
		}
		return base;
	}
	while (L != R) {
		member = (L + R + 1) / 2;
		result = compar(((char *) base) + member * size, key);
		if (result > 0) {
			R = member - 1;
		} else {
			L = member;
		}
	}
	result = compar(((char *) base) + L * size, key);
	if (found != NULL) {
		*found = !result;
	}
	if (result < 0) {
		return ((char *) base) + (L + 1) * size;
	} else {
		return ((char *) base) + L * size;
	}
}

void res_free_texture(void) {
	for (size_t i = 0; i < textures.count; i++) {
		SDL_DestroyTexture(textures.items[i].blob.data);
		free(textures.items[i].name);
	}
	free(textures.items);
	textures.count = 0, textures.items = NULL;
}

static void res_free_blobs(struct blob_collection *collection) {
	for (size_t i = 0; i < collection->count; i++) {
		free(collection->items[i].blob.data);
		free(collection->items[i].name);
	}
	free(collection->items);
	collection->count = 0, collection->items = NULL;
}

void res_free_map(void) {
	res_free_blobs(&maps);
}

void res_free_collision(void) {
	res_free_blobs(&collisions);
}

void res_free_fun(void) {
	for (size_t i = 0; i < funs.count; i++) {
		free(funs.items[i].name);
	}
	free(funs.items);
	funs.count = 0;
}

static int res_compare_blobs(void const *a, void const *b) {
	struct blob_item const *aa = a, *bb = b;
	return name_compare(aa->name, bb->name);
}

static int res_compare_funs(void const *a, void const *b) {
	struct fun_item const *aa = a, *bb = b;
	return name_compare(aa->name, bb->name);
}

static struct blob res_get_blob(name_T const name, struct blob_collection *collection) {
	struct blob_item new = {.name = name};
	struct blob_item *dst = bsearch(&new, collection->items, collection->count, sizeof (struct blob_item), res_compare_blobs);
	if (dst == NULL) {
		return (struct blob) {.data = NULL, .size = 0};
	}
	return dst->blob;
}

struct blob res_get_texture(name_T const name) {
	return res_get_blob(name, &textures);
}

struct blob res_get_map(name_T const name) {
	return res_get_blob(name, &maps);
}

struct blob res_get_collision(name_T const name) {
	return res_get_blob(name, &collisions);
}

struct funs res_get_fun(name_T const name) {
	struct fun_item new = {.name = name};
	struct fun_item *dst = bsearch(&new, funs.items, funs.count, sizeof (struct fun_item), res_compare_funs);
	if (dst == NULL) {
		return (struct funs) {.newfun = NULL, .setfun = NULL};
	}
	return dst->funs;
}

static void res_push_blob(struct blob *blob, name_T name, struct blob_collection *collection, void (*freeFunc)(void *)) {
	int found;
	struct blob_item new = {.blob = *blob, .name = name};
	struct blob_item *dst = bsearchinsertposition(&new, collection->items, collection->count, sizeof (struct blob_item), res_compare_blobs, &found);
	if (found) {
		fprintf(stderr, "warn: name collision: %s\n", new.name);
		freeFunc(dst);
		memcpy(dst, &new, sizeof (struct blob_item));
	} else {
		size_t index = dst - collection->items; // hack
		collection->count++;
		collection->items = realloc(collection->items, sizeof (struct blob_item) * collection->count);
		dst = collection->items + index; // hack for the struct having been reallocated
		#if 0
		fprintf(stderr, "info: no collision: %s\n", new.name);
		#endif
		memmove(dst + 1, dst, sizeof (struct blob_item) * (collection->count - 1 - (dst - collection->items)));
		memcpy(dst, &new, sizeof (struct blob_item));
	}
}

static void texture_free_func(void *ptr) {
	struct blob_item *a = ptr;
	SDL_DestroyTexture(a->blob.data);
	free(a->name);
}

void res_push_texture(struct blob *blob, name_T name) {
	res_push_blob(blob, name, &textures, texture_free_func);
}

static void blob_free_func(void *ptr) {
	struct blob_item *a = ptr;
	free(a->blob.data);
	free(a->name);
}

void res_push_map(struct blob *blob, name_T name) {
	res_push_blob(blob, name, &maps, blob_free_func);
}

void res_push_collision(struct blob *blob, name_T name) {
	res_push_blob(blob, name, &collisions, blob_free_func);
}

void res_push_fun(void *(*newfun)(struct entities *entities), int (*setfun)(void *const restrict self, char const *const restrict key, char const *const restrict value), name_T name) {
	int found;
	struct fun_item new = {.funs = {.newfun = newfun, .setfun = setfun}, .name = name};
	struct fun_item *dst = bsearchinsertposition(&new, funs.items, funs.count, sizeof (struct fun_item), res_compare_funs, &found);
	if (found) {
		fprintf(stderr, "warn: name collision: %s\n", new.name);
		free(dst->name);
		memcpy(dst, &new, sizeof (struct fun_item));
	} else {
		size_t index = dst - funs.items; // hack
		funs.count++;
		funs.items = realloc(funs.items, sizeof (struct fun_item) * funs.count);
		dst = funs.items + index; // hack for the struct having been reallocated
		memmove(dst + 1, dst, sizeof (struct fun_item) * (funs.count - 1 - (dst - funs.items)));
		memcpy(dst, &new, sizeof (struct fun_item));
	}
}

int loadResources(char *filename) {
	// open the file
	FILE *zip = fopen(filename, "rb");
	if (zip == NULL) {
		perror("fopen");
		return EXIT_FAILURE;
	}

	// load it into memory
	size_t size;
	char *file = util_loadFile(zip, &size);
	if (file == NULL) {
		perror("reading file"); // "opening file: Succeeded"
		return EXIT_FAILURE; // "process returned with status 1" id love to see that happen
	}
	fclose(zip);

	// index the archive
	int error;
	struct zip_file *headers = zip_index(file, size, &error);
	if (headers == NULL) {
		fprintf(stderr, "%s\n", zip_error(error));
		return EXIT_FAILURE;
	}

	size_t const file_count = headers->file_count;
	struct chaos *files = malloc(sizeof (struct chaos) * file_count);
	
	for (size_t i = 0; i < file_count; i++) {
		struct zip_index const *zip_file = headers->files + i;
		struct chaos *chaos_file = files + i;
		
		size_t j = zip_file->filename_length;
		for (; j > 0 && zip_file->filename[j] != '.'; j--)
			;
		if (j == 0) {
			continue;
		}
		
		chaos_file->name = malloc(j + 1);
		memcpy(chaos_file->name, zip_file->filename, j);
		chaos_file->name[j] = 0;
		
		chaos_file->extension = get_extension_type((char *) zip_file->filename + j + 1, zip_file->filename_length - j - 1);
		
		#if 0
		fprintf(stderr, "%s: %i\n", chaos_file->name, chaos_file->extension);
		#endif
		
		switch (zip_file->method) {
			case 0: // STORE
				chaos_file->data = malloc(zip_file->original_size);
				memcpy(chaos_file->data, zip_file->data, zip_file->original_size);
				chaos_file->size = zip_file->original_size;
				break;
			
			case 8: // DEFLATE
				chaos_file->data = inflateWrapped(zip_file->data, zip_file->original_size);
				if (chaos_file->data == NULL)
					return EXIT_FAILURE;
				chaos_file->size = zip_file->original_size;
				break;
			
			default:
				return EXIT_FAILURE;
		}
		
		switch (chaos_file->extension) {
			struct blob blob;
			
			case FILE_EXT_TEXTURE:;
				unsigned error;
				struct plum_image *image = plum_load_image(chaos_file->data, chaos_file->size, PLUM_COLOR_32 | PLUM_ALPHA_INVERT, &error);
				free(chaos_file->data);
				if (image == NULL) {
					free(chaos_file->name);
					fprintf(stderr, "error: libplum: %s\n", plum_get_error_text(error));
					break;
				}
				SDL_Surface *surface = SDL_CreateRGBSurfaceFrom(image->data, image->width, image->height, 32, image->width * 4, 0x000000ff, 0x0000ff00, 0x00ff0000, 0xff000000);
				if (surface == NULL) {
					free(chaos_file->name);
					plum_destroy_image(image);
					fprintf(stderr, "error: SDL2: %s\n", SDL_GetError());
					break;
				}
				SDL_Texture *texture = SDL_CreateTextureFromSurface(renderer, surface);
				SDL_SetTextureBlendMode(texture, SDL_BLENDMODE_BLEND);
				SDL_FreeSurface(surface);
				plum_destroy_image(image);
				if (texture == NULL) {
					free(chaos_file->name);
					fprintf(stderr, "error: SDL2: %s\n", SDL_GetError());
					break;
				}
				
				blob = (struct blob) {.data = texture, .size = 0};
				res_push_texture(&blob, chaos_file->name);
				break;
			
			case FILE_EXT_MAP:
				blob = (struct blob) {.data = chaos_file->data, .size = chaos_file->size};
				res_push_map(&blob, chaos_file->name);
				break;

			case FILE_EXT_COLLISION:;
				char const *props = chaos_file->data;
				size_t remaining = chaos_file->size;
				if (props[remaining - 1] != '\n') {
					fprintf(stderr, "warn: will not load %s.col (missing trailing newline)\n", chaos_file->name);
					free(chaos_file->name);
					free(chaos_file->data);
					break;
				}
				int tiles = 0;
				for (size_t i = 0; i < remaining; i++) {
					if (props[i] == '\n') {
						tiles++;
					}
				}
				//fprintf(stderr, "allocating %u tiles\n", tiles);
				collision_T *const data = calloc(tiles, sizeof (collision_T));
				for (int tile = 0; tile < tiles; tile++) {
					char const *tmp = memchr(props, '\n', remaining);
					size_t tile_remaining = tmp - props;
					//fprintf(stderr, "tile %u (%zu):", tile, tile_remaining);
					char const *tile_props = props;
					props += tile_remaining + 1;
					while (true) {
						char const *tile_prop = memchr(tile_props, ',', tile_remaining);
						char const *const str = tile_props;
						size_t len;
						if (tile_prop == NULL) {
							len = tile_remaining;
							//fprintf(stderr, " '%.*s'", len, tile_props);
							//fprintf(stderr, " '%u'", len);
						} else {
							len = tile_prop - tile_props;
							//fprintf(stderr, " '%.*s'", len, tile_props);
							//fprintf(stderr, " '%u'", len);
							tile_props += len + 1;
							tile_remaining -= len + 1;
						}
						
						if (len == 0) {
							break;
						}
						
						#define t(a, b)\
							else if (len == sizeof (a) - 1 && memcmp(str, a, sizeof (a) - 1) == 0) {\
								data[tile] |= b;\
							}
						t("solid", 1 << COLLISION_SOLID | 1 << COLLISION_FLOOR)
						t("hazard", 1 << COLLISION_HAZARD)
						t("semisolid", 1 << COLLISION_FLOOR)
						#undef t
						#if 0
						if (len == 5 && memcmp(str, "solid", 5) == 0) {
							data[tile] |= 1 << COLLISION_SOLID | 1 << COLLISION_FLOOR;
						} else if (len == 6 && memcmp(str, "hazard", 6) == 0) {
							data[tile] |= 1 << COLLISION_HAZARD;
						} else if (len == 9 && memcmp(str, "semisolid", 9) == 0) {
							data[tile] |= 1 << COLLISION_FLOOR;
						}
						#endif
						else {
							fprintf(stderr, "warn: %s: unrecognized property '%.*s'\n", chaos_file->name, (int) len, str);
						}
						
						if (tile_prop == NULL) {
							break;
						}
					}
					//fprintf(stderr, " %b\n", data[tile]);
				}
				free(chaos_file->data);
				blob = (struct blob) {.data = data, .size = tiles};
				res_push_collision(&blob, chaos_file->name);
				break;
			
			default:
				free(chaos_file->data), chaos_file->data = NULL;
				free(chaos_file->name), chaos_file->name = NULL;
				break;
		}
	}
	
	free(file);
	free(headers);
	
	free(files);

	return EXIT_SUCCESS;
}

ext_T get_extension_type(char *extension, int length) {
	#define ext(str, en) \
		if (length == sizeof (str) - 1 && \
		memcmp(extension, str, sizeof (str) - 1) == 0) \
		return en
	ext("png", FILE_EXT_TEXTURE);
	ext("jpg", FILE_EXT_TEXTURE);
	ext("gif", FILE_EXT_TEXTURE);
	ext("map", FILE_EXT_MAP);
	ext("col", FILE_EXT_COLLISION);
	return FILE_EXT_UNKNOWN;
	#undef ext
}

static void *inflateWrapped(void *const restrict data, uint32_t const outsize) {
	z_stream stream = {
		.zalloc = Z_NULL,
		.zfree = Z_NULL,
		.opaque = Z_NULL,
	}; // zlib docs require you to set these 3 to Z_NULL, apparently
	stream.avail_in = outsize; // hope itll work! hehe
	stream.next_in = data;
	int ret = inflateInit2(&stream, -15); // -15 is raw DEFLATE, we dont want zlib + DEFLATE :p
	if (ret != Z_OK) {
		return NULL;
	}
	unsigned char *buf = malloc(outsize);
	if (buf == NULL) {
		perror("malloc");
		goto finally; // would it hurt to change it to error? free(NULL) is defined to be a no-op
	}
	stream.avail_out = outsize;
	stream.next_out = buf;
	
	switch ((ret = inflate(&stream, Z_SYNC_FLUSH))) {
		case Z_NEED_DICT:
		case Z_DATA_ERROR:
			//fputs("inflate: data error\n", stderr);
			goto error;
		case Z_MEM_ERROR:
			//fputs("inflate: memory error\n", stderr);
			goto error;
		case Z_STREAM_ERROR:
			//fputs("inflate: stream error\n", stderr);
			goto error;
		case Z_OK:
			//fputs("inflate: stream didnt end", stderr);
			goto error;
		
		case Z_STREAM_END:
			goto finally;
		
		default:
			//fprintf(stderr, "unrecognised return value %u\n", ret);
			goto error;
	}
	
	error: // all cases share the error handling code
	free(buf);
	buf = NULL; // return value
	
	finally:
	(void) inflateEnd(&stream);
	//fprintf(stderr, "processed %lu (0x%lx) bytes total, exit status %d\n", stream.total_out, stream.total_out, ret);
	return buf;
}