#define SDL_MAIN_HANDLED
#include <SDL2/SDL.h>

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

#include "input.h"
#include "loader.h"
#include "util.h"
#include "save.h"

#include "tilemap.h"

#include "incbin.h"
#include "libplum.h"

#include "main.h"

#include <getopt.h>

SDL_Window *window = NULL;
SDL_Renderer *renderer = NULL;
enum game_state game_state = STATE_PLAYING;
char *game_level, *game_next_level;
char *save_file_name;

static void *particle_tex = NULL;

static SDL_Texture *framebuffer;

#if defined(__EMSCRIPTEN__)
#include <emscripten.h>
void main_loop(void);
#endif

#define WINDOW_WIDTH 160
#define WINDOW_HEIGHT 90
#define INIT_SUBSYSTEMS SDL_INIT_VIDEO

unsigned input_pressed = 0, input_held = 0;
SDL_Scancode keybinds[] = {
	SDL_SCANCODE_UP,
	SDL_SCANCODE_DOWN,
	SDL_SCANCODE_LEFT,
	SDL_SCANCODE_RIGHT,
	SDL_SCANCODE_A,
	SDL_SCANCODE_S,
};
unsigned input_keyboard = 0;

struct touch {
	size_t touches;
	size_t allocated;
	struct touch_vec {
		float x, y;
		int active;
	} *positions;
	unsigned input_touch;
} touch = {0, 0, NULL, 0};

struct entities entities = {{{0}}, {{0}}, 0, {{0}}, 0}, next_entities = {{{0}}, {{0}}, 0, {{0}}, 0};

void *player_new(struct entities *entities);
int player_property(void *const restrict entity, char const *const restrict property, char const *const restrict value);

void funs_init(void);

void entities_free(struct entities *entities) {
	for (int i = 0, e = 0; i < 64 && e < entities->enemies; i++) {
		if (entities->enemy[i].state) {
			e++;
			if (entities->enemy[i].free != NULL) {
				entities->enemy[i].free(entities->enemy + i);
			}
		}
	}
	entities->enemies = 0;
	#if 0
	for (int i = 0; i < entities.warps; i++) {
		entities.warp[i].update(entities.warp + i);
	}
	#endif
	entities->warps = 0;
}

int entities_load(struct entities *entities, char *data, size_t size, size_t input_bytes) {
	entities->enemies = 0;
	entities->warps = 0;
	entities->projectiles = 0;
	entities->particles = 0;
	data += input_bytes;
	size -= input_bytes;
	while (size) {
		size_t len;
		if ((len = strnlen(data, size)) == size) {
			return 1;
		}
		char *name = data;
		struct funs fun = res_get_fun(name);
		data += len + 1;
		size -= len + 1;
		if (fun.newfun) {
			struct entity *entity = fun.newfun(&next_entities);
			while (1) {
				if ((len = strnlen(data, size)) == size) {
					return 1;
				} else if (len == 0) {
					data++;
					size--;
					break;
				}
				char *key = data;
				data += len + 1;
				size -= len + 1;
				if ((len = strnlen(data, size)) == size) {
					return 1;
				}
				char *value = data;
				data += len + 1;
				size -= len + 1;
				if (fun.setfun(entity, key, value)) {
					puts(key); puts(value);
					return 1;
				}
			}
		} else {
			fprintf(stderr, "warn: unknown entity %s\n", name);
			while (1) {
				if ((len = strnlen(data, size)) == size) {
					return 1;
				} else if (len == 0) {
					data++;
					size--;
					break;
				}
				data += len + 1;
				size -= len + 1;
				if ((len = strnlen(data, size)) == size) {
					return 1;
				}
				data += len + 1;
				size -= len + 1;
			}
		}
	}
	return 0;
}

int game_update(void) {
	if (entities.player[0].update(entities.player)) {
		return 1;
	}
	
	for (int i = 0, e = 0; i < 64 && e < entities.enemies; i++) {
		if (entities.enemy[i].state) {
			e++;
			entities.enemy[i].update(entities.enemy + i);
		}
	}
	
	for (int i = 0; i < entities.warps; i++) {
		entities.warp[i].update(entities.warp + i);
	}
	
	for (int i = 0, e = 0; i < 64 && e < entities.projectiles; i++) {
		if (entities.projectile[i].state) {
			e++;
			if (entities.projectile[i].update(entities.projectile + i)) {
				entities.projectiles--;
				if (entities.projectile + i != entities.projectile + entities.projectiles) {
					memcpy(entities.projectile + i, entities.projectile + entities.projectiles, sizeof (struct projectile));
				}
				continue;
			}
		}
	}

	for (int i = 0; i < entities.particles; i++) {
		if (entities.particle[i].hp-- == 0) {
			entities.particles--;
			if (entities.particle + i != entities.particle + entities.particles) {
				memcpy(entities.particle + i, entities.particle + entities.particles, sizeof (struct particle));
			}
			continue;
		}
		entities.particle[i].x += (entities.particle[i].velocity.x += entities.particle[i].acceleration.x);
		entities.particle[i].y += (entities.particle[i].velocity.y += entities.particle[i].acceleration.y);
	}
	
	return 0;
}

void game_render(SDL_Texture *framebuffer, int x, int y) {
	SDL_SetRenderTarget(renderer, framebuffer);
	tilemap_background(tilemap, x, y, WINDOW_WIDTH, WINDOW_HEIGHT);
	for (int i = 0; i < entities.particles; i++) {
		struct particle *self = entities.particle + i;
		SDL_RenderCopy(renderer, particle_tex, &self->rect, &(SDL_Rect) {from_fixed(self->x) - x, from_fixed(self->y) - y, self->rect.w, self->rect.h});
	}
	for (int i = 0, e = 0; i < 64 && e < entities.projectiles; i++) {
		if (entities.projectile[i].state) {
			e++;
			entities.projectile[i].draw(entities.projectile + i, x, y);
		}
	}
	for (int i = 0, e = 0; i < 64 && e < entities.enemies; i++) {
		if (entities.enemy[i].state) {
			e++;
			entities.enemy[i].draw(entities.enemy + i, x, y);
		}
	}
	entities.player[0].draw(entities.player, x, y);
	tilemap_foreground(tilemap, x, y, WINDOW_WIDTH, WINDOW_HEIGHT);
	// hacky hp render
	if (entities.player[0].hp) {
		SDL_SetRenderDrawColor(renderer, 255, 0, 0, 255);
		for (int i = 0; i < entities.player[0].hp; i++) {
			SDL_RenderFillRect(renderer, &(SDL_Rect) {1, 1 + i * 5, 4, 4});
		}
	}
}

int fade = 255;

int game_load_level(char *level) {
	struct blob blob = res_get_map(level);
	next_tilemap = tilemap_load(blob.data, blob.size);
	if (next_tilemap != NULL) {
		if (entities_load(&next_entities, blob.data, blob.size, next_tilemap->input_bytes)) {
			tilemap_free(next_tilemap);
		} else {
			tilemap_free(tilemap);
			tilemap = next_tilemap;
			game_level = level;
			entities_free(&entities);
			memcpy(&entities, &next_entities, sizeof (entities));

			game_state = STATE_FADE_OUT;
			fade = 255;
			int x = (entities.player[0].x / 16) - (WINDOW_WIDTH / 2);
			int y = (entities.player[0].y / 16) - (WINDOW_HEIGHT / 2);
			if (x < 0) {x = 0;} else if (x + WINDOW_WIDTH > tilemap->width * 8) {x = tilemap->width * 8 - WINDOW_WIDTH;}
			if (y < 0) {y = 0;} else if (y + WINDOW_HEIGHT > tilemap->height * 8) {y = tilemap->height * 8 - WINDOW_HEIGHT;}
			
			game_render(framebuffer, x, y);
			SDL_SetRenderTarget(renderer, NULL);
			SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0);
			SDL_RenderClear(renderer);
			return 0;
		}
	}
	return 1;
}

void game_render_flush(SDL_Texture *framebuffer) {
	SDL_SetRenderTarget(renderer, NULL);
	SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0);
	SDL_RenderClear(renderer);
	SDL_RenderCopy(renderer, framebuffer, &(SDL_Rect) {0, 0, WINDOW_WIDTH, WINDOW_HEIGHT}, &(SDL_Rect) {0, 0, WINDOW_WIDTH, WINDOW_HEIGHT});
	// then we wait for the next video frame 
	SDL_RenderPresent(renderer);
}

int main(int const argc, char *const *const argv) {
	save_file_name = SDL_GetPrefPath("sylvie", "game");
	save_file_name = SDL_realloc(save_file_name, strlen(save_file_name) + strlen("save.sav") + 1);
	strcat(save_file_name, "save.sav");
	struct option opts[] = {
		{"help",       0, NULL, 'h'},
		{"scale",      1, NULL, 's'},
		{"resizable",  2, NULL, 'r'},
		{"fullscreen", 2, NULL, 'f'},
		{"maximize",   2, NULL, 'm'},
		{"border",     2, NULL, 'b'},
		{NULL,         0, NULL,  0 },
	};
	int opt, li, scale = 0;
	unsigned flags = 0, error = 0;
	while ((opt = getopt_long(argc, argv, "h", opts, &li)) != -1) {
		switch (opt) {
			case 'h':
				printf("usage:\n\t%s\n\t%s [mod.zip]\n", argv[0], argv[0]);
				return 0;
			
			case 's':
				scale = atoi(optarg);
				break;
			
			case 'r':
				if (optarg == NULL || strcmp(optarg, "true") == 0) {
					flags |= SDL_WINDOW_RESIZABLE;
				} else if (strcmp(optarg, "false") == 0) {
					flags &= ~SDL_WINDOW_RESIZABLE;
				} else {
					fprintf(stderr, "%s=%s -- expected 'true' or 'false'\n", opts[li].name, optarg);
					error = 1;
				}
				break;
			
			case 'f':
				if (optarg == NULL || strcmp(optarg, "true") == 0) {
					flags |= SDL_WINDOW_FULLSCREEN_DESKTOP;
				} else if (strcmp(optarg, "false") == 0) {
					flags &= ~SDL_WINDOW_FULLSCREEN_DESKTOP;
				} else {
					fprintf(stderr, "%s=%s -- expected 'true' or 'false'\n", opts[li].name, optarg);
					error = 1;
				}
				break;
			
			case 'm':
				if (optarg == NULL || strcmp(optarg, "true") == 0) {
					flags |= SDL_WINDOW_MAXIMIZED;
				} else if (strcmp(optarg, "false") == 0) {
					flags &= ~SDL_WINDOW_MAXIMIZED;
				} else {
					fprintf(stderr, "%s=%s -- expected 'true' or 'false'\n", opts[li].name, optarg);
					error = 1;
				}
				break;
			
			case 'b':
				if (optarg == NULL || strcmp(optarg, "true") == 0) {
					flags &= ~SDL_WINDOW_BORDERLESS;
				} else if (strcmp(optarg, "false") == 0) {
					flags |= SDL_WINDOW_BORDERLESS;
				} else {
					fprintf(stderr, "%s=%s -- expected 'true' or 'false'\n", opts[li].name, optarg);
					error = 1;
				}
				break;
			
			default:
				error = 1;
		}
	}
	if (error) {
		return EXIT_FAILURE;
	}
	
	if (SDL_Init(INIT_SUBSYSTEMS)) {
		fprintf(stderr, "failed to initialize SDL2: %s\n", SDL_GetError());
		return EXIT_FAILURE;
	}
	SDL_StopTextInput();
	#if defined(__EMSCRIPTEN__)
	scale = 1;
	#else
	if (scale <= 0) { // this looks very wrong
		SDL_DisplayMode dm;
		if (SDL_GetDesktopDisplayMode(0, &dm) != 0) {
			SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "couldnt get desktop size", SDL_GetError(), NULL);
			fprintf(stderr, "info: couldnt get desktop size %s\n", SDL_GetError());
			flags |= SDL_WINDOW_RESIZABLE;
		} else {
			int x = dm.w / 2 / WINDOW_WIDTH;
			int y = dm.h / 2 / WINDOW_HEIGHT;
			if (x < y) {
				scale = x;
			} else {
				scale = y;
			}
			if (scale == 0) {
				scale = 1;
			}
			if (dm.refresh_rate != 60) {
				SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_WARNING, "refresh rate", "this game currently only runs well on 60Hz displays", NULL);
			}
		}
	}
	#endif
	
	window = SDL_CreateWindow(":3", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, WINDOW_WIDTH * scale, WINDOW_HEIGHT * scale, flags | SDL_WINDOW_HIDDEN);
	if (window == NULL) {
		goto end;
	}

	SDL_SetHint(SDL_HINT_RENDER_DRIVER, "opengl"); // hack, i dont wanna deal with windows discarding render textures
	renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_TARGETTEXTURE | SDL_RENDERER_PRESENTVSYNC);
	if (renderer == NULL) {
		goto end;
	}
	SDL_RenderSetLogicalSize(renderer, WINDOW_WIDTH, WINDOW_HEIGHT);
	SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);

	framebuffer = SDL_CreateTexture(renderer, SDL_PIXELTYPE_UNKNOWN, SDL_TEXTUREACCESS_TARGET, WINDOW_WIDTH, WINDOW_HEIGHT);
	
	{
		funs_init();
		if (optind == argc) {
			void *a = util_executableRelativePath("assets.res", *argv, 0);
			puts(a);
			if (loadResources(a)) {
				fputs("loading resources failed\n", stderr);
				return 1;
			}
			free(a);
		} else {
			while (optind < argc) {
			if (loadResources(argv[optind])) {
				fprintf(stderr, "loading %s failed\n", argv[optind]);
				return 1;
			}
				optind++;
			}
		}
		
		particle_tex = res_get_texture("particles").data;

		FILE *file = fopen(save_file_name, "rb");
		if (file == NULL) {
			if (errno != ENOENT) {
				perror(save_file_name);
				goto end;
			}
			player_property(next_entities.player, "x", "40");
			player_property(next_entities.player, "y", "64");
			game_load_level("untitled");
		} else {
			if (game_load(file)) {
				fputs("the save file is corrupt!\n", stderr);
				player_property(next_entities.player, "x", "40");
				player_property(next_entities.player, "y", "64");
				game_load_level("untitled");
			}
		}
		#if 0
		struct blob blob = res_get_map("untitled");
		next_tilemap = tilemap_load(blob.data, blob.size);
		if (next_tilemap == NULL) {
			fputs("failed to load initial tilemap\n", stderr);
			goto end;
		}
		if (entities_load(&entities, blob.data, blob.size, next_tilemap->input_bytes)) {
			tilemap_free(next_tilemap);
			fputs("failed to load initial tilemap\n", stderr);
			goto end;
		}
		tilemap = next_tilemap;
		#endif
	}

	/* unsigned error; // identical variable is declared higher up */
	struct plum_image *image = plum_load_image(game_icon, game_icon_end - game_icon, PLUM_COLOR_32 | PLUM_ALPHA_INVERT, &error);
	if (image == NULL) {
		fprintf(stderr, "error: libplum: %s\n", plum_get_error_text(error));
		goto icon_done;
	}
	SDL_Surface *surface = SDL_CreateRGBSurfaceFrom(image->data, image->width, image->height, 32, image->width * 4, 0x000000ff, 0x0000ff00, 0x00ff0000, 0xff000000);
	if (surface == NULL) {
		plum_destroy_image(image);
		fprintf(stderr, "error: SDL2: %s\n", SDL_GetError());
		goto icon_done;
	}
	SDL_SetWindowIcon(window, surface);
	SDL_FreeSurface(surface);
	plum_destroy_image(image);
	icon_done:

	SDL_ShowWindow(window);
	
#if defined(__EMSCRIPTEN__)
	emscripten_set_main_loop(main_loop, 60, 0); // TODO: how do i query the framerate if i set it to 0?
	return 0;
	
	end:
	tilemap_free(tilemap), tilemap = NULL;
	SDL_DestroyRenderer(renderer);
	SDL_DestroyWindow(window);
	SDL_QuitSubSystem(INIT_SUBSYSTEMS);
	return 0;
}

int x = 0, y = 0;

void main_loop(void) {
#else
	int x = 0, y = 0;
	while (1) {
#endif
		input_pressed = input_held;
		SDL_Event evt;
		while (SDL_PollEvent(&evt)) {
			switch (evt.type) {
				case SDL_QUIT: // the last window is closed, or the app is asked to terminate
					fputs("quitting...\n", stderr);
					goto end;
				case SDL_KEYDOWN:
				case SDL_KEYUP: // a key on is pressed or released
					if (evt.key.repeat)
						break;
					
					#if !defined(__EMSCRIPTEN__)
					// check for ^Q and exit if pressed
					if (evt.key.state == SDL_PRESSED && evt.key.keysym.sym == SDLK_q && evt.key.keysym.mod & KMOD_CTRL) {
						goto end;
					}
					#endif
					
					//static_assert(INPUT_LENGTH <= sizeof(input_held) * CHAR_BIT); // if this trips up, scope creep happened
					for (unsigned key = 0, bit = 1; key < INPUT_LENGTH; key++, bit <<= 1) {
						if (evt.key.keysym.scancode == keybinds[key]) {
							if (evt.key.state == SDL_PRESSED)
								input_keyboard |= bit;
							else
								input_keyboard &= ~bit;
						}
					}
					//fprintf(stderr, "input: %0*b\n", INPUT_LENGTH, input_held);
					break;
				case SDL_MOUSEMOTION: // the cursor moves
					//fprintf(stderr, "mouse at %4i %4i\n", evt.motion.x, evt.motion.y);
					break;
				case SDL_MOUSEBUTTONDOWN:
				case SDL_MOUSEBUTTONUP: // a left click or release
					if (evt.button.state) {
						//fprintf(stderr, "click %i\n", evt.button.button);
					}
					if (!evt.button.state) {
						//fprintf(stderr, "unclick %i\n", evt.button.button);
					}
					break;
				case SDL_MOUSEWHEEL: // the scroll wheel gets rolled
					//fprintf(stderr, "scrolled by %2i %2i %4.1f %4.1f\n", evt.wheel.x, evt.wheel.y, evt.wheel.preciseX, evt.wheel.preciseY);
					break;
				case SDL_CLIPBOARDUPDATE: // annoying
					break;

				case SDL_FINGERUP:;
					size_t i = evt.tfinger.fingerId;
					touch.positions[i].active = 0;
					goto reset_touch;
				case SDL_FINGERDOWN:
					i = evt.tfinger.fingerId;
					if (i >= touch.allocated) {
						size_t const start = touch.allocated;
						touch.allocated = i + 1;
						touch.positions = realloc(touch.positions, sizeof (struct touch_vec) * touch.allocated);
						if (i) { // hack for 0 - 1 = absurd-high number; all of RAM gets wiped
							for (size_t index = start; index < i - 1; index++) {
								touch.positions[index].active = 0;
							}
						}
					}
					touch.positions[i].active = 1;
				case SDL_FINGERMOTION:
					i = evt.tfinger.fingerId;
					touch.positions[i].x = evt.tfinger.x;
					touch.positions[i].y = evt.tfinger.y;
					printf("%" PRIu64 " %" PRIu64 " %f %f\n", evt.tfinger.touchId, evt.tfinger.fingerId, evt.tfinger.x, evt.tfinger.y);
					reset_touch:
					touch.input_touch = 0;
					for (size_t i = 0; i < touch.allocated; i++) {
						if (touch.positions[i].active) {
							switch ((int) (touch.positions[i].x * 5)) {
								case 0:
									touch.input_touch |= 1 << INPUT_LEFT;
									break;
								case 1:
									touch.input_touch |= 1 << INPUT_RIGHT;
									break;
								case 3:
									touch.input_touch |= 1 << INPUT_S;
									break;
								case 4:
									touch.input_touch |= 1 << INPUT_A;
									break;
								default:;
							}
						}
					}
					break;
				case SDL_MULTIGESTURE:
					break;
				
				case SDL_WINDOWEVENT: // window related events
					switch (evt.window.event) {
						case SDL_WINDOWEVENT_SHOWN:
							//fprintf(stderr, "window %u shown\n", evt.window.windowID);
							break;
						case SDL_WINDOWEVENT_HIDDEN:
							//fprintf(stderr, "window %u hidden\n", evt.window.windowID);
							break;
						case SDL_WINDOWEVENT_EXPOSED:
							//fprintf(stderr, "window %u exposed\n", evt.window.windowID);
							break;
						case SDL_WINDOWEVENT_MOVED: // a window is moved
							//fprintf(stderr, "window %u moved %4i %4i\n", evt.window.windowID, evt.window.data1, evt.window.data2);
							break;
						case SDL_WINDOWEVENT_RESIZED: // a window is resized
							//fprintf(stderr, "window %u resized %4i %4i\n", evt.window.windowID, evt.window.data1, evt.window.data2);
							break;
						case SDL_WINDOWEVENT_SIZE_CHANGED: // ?
							break;
						case SDL_WINDOWEVENT_MINIMIZED:
							//fprintf(stderr, "window %u minimized\n", evt.window.windowID);
							break;
						case SDL_WINDOWEVENT_MAXIMIZED:
							//fprintf(stderr, "window %u maximized\n", evt.window.windowID);
							break;
						case SDL_WINDOWEVENT_RESTORED:
							//fprintf(stderr, "window %u restored\n", evt.window.windowID);
							break;
						case SDL_WINDOWEVENT_ENTER:
						case SDL_WINDOWEVENT_LEAVE: // cursor enters or leaves a window
						case SDL_WINDOWEVENT_FOCUS_GAINED:
						case SDL_WINDOWEVENT_FOCUS_LOST: // keyboard

							break;
						case SDL_WINDOWEVENT_CLOSE: // a window is asked to close
							//fprintf(stderr, "window %u closed\n", evt.window.windowID);
							break;
						case SDL_WINDOWEVENT_TAKE_FOCUS:
							//fprintf(stderr, "window %u offered focus\n", evt.window.windowID);
							break;
						case SDL_WINDOWEVENT_HIT_TEST:
							//fprintf(stderr, "window %u hit test\n", evt.window.windowID);
							break;
						default:
							fprintf(stderr, "window %u event %i\n", evt.window.windowID, evt.window.event);
					}
					break;
				default:
					fprintf(stderr, "unknown event type %i\n", evt.type);
			}
		}
		input_held = input_keyboard | touch.input_touch;
		input_pressed = ~input_pressed & input_held;
		//fprintf(stderr, "input: %0*b\n", INPUT_LENGTH, input_pressed);

		switch (game_state) {
			case STATE_PLAYING:
				//SDL_RenderCopy(renderer, tilemap->wang_tileset, &(SDL_Rect) {0, 0, 128, 90}, &(SDL_Rect) {0, 0, 128, 90});
				if (game_update()) {
					//goto end; // TODO: game over state
					FILE *file = fopen(save_file_name, "rb");
					if (file == NULL) {
						if (errno != ENOENT) {
							perror(save_file_name);
							goto end;
						}
						player_property(next_entities.player, "x", "40");
						player_property(next_entities.player, "y", "64");
						game_load_level("untitled");
					} else {
						if (game_load(file)) {
							fputs("the save file somehow corrupted itself!\n", stderr);
							goto end; // better than freezing if the save file magically corrupts
						}
					}
				}
				
				x = (entities.player[0].x / 16) - (WINDOW_WIDTH / 2);
				y = (entities.player[0].y / 16) - (WINDOW_HEIGHT / 2);
				if (x < 0) {x = 0;} else if (x + WINDOW_WIDTH > tilemap->width * 8) {x = tilemap->width * 8 - WINDOW_WIDTH;}
				if (y < 0) {y = 0;} else if (y + WINDOW_HEIGHT > tilemap->height * 8) {y = tilemap->height * 8 - WINDOW_HEIGHT;}
				
				game_render(framebuffer, x, y);
				game_render_flush(framebuffer);
				break;

			case STATE_FADE_IN:
				fade += 15;
				SDL_SetRenderTarget(renderer, NULL);
				SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0);
				SDL_RenderClear(renderer);
				SDL_RenderCopy(renderer, framebuffer, &(SDL_Rect) {0, 0, WINDOW_WIDTH, WINDOW_HEIGHT}, &(SDL_Rect) {0, 0, WINDOW_WIDTH, WINDOW_HEIGHT});
				SDL_SetRenderDrawColor(renderer, 0, 0, 0, fade);
				SDL_RenderFillRect(renderer, &(SDL_Rect) {0, 0, WINDOW_WIDTH, WINDOW_HEIGHT});
				SDL_RenderPresent(renderer);
				if (fade == 255) {
					if (game_load_level(game_next_level)) {
						game_state = STATE_FADE_OUT;
						x = (entities.player[0].x / 16) - (WINDOW_WIDTH / 2);
						y = (entities.player[0].y / 16) - (WINDOW_HEIGHT / 2);
						if (x < 0) {x = 0;} else if (x + WINDOW_WIDTH > tilemap->width * 8) {x = tilemap->width * 8 - WINDOW_WIDTH;}
						if (y < 0) {y = 0;} else if (y + WINDOW_HEIGHT > tilemap->height * 8) {y = tilemap->height * 8 - WINDOW_HEIGHT;}
						
						game_render(framebuffer, x, y);
						SDL_SetRenderTarget(renderer, NULL);
						SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0);
						SDL_RenderClear(renderer);
					}
				}
				break;
			
			case STATE_FADE_OUT:
				fade -= 15;
				SDL_SetRenderTarget(renderer, NULL);
				SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0);
				SDL_RenderClear(renderer);
				SDL_RenderCopy(renderer, framebuffer, &(SDL_Rect) {0, 0, WINDOW_WIDTH, WINDOW_HEIGHT}, &(SDL_Rect) {0, 0, WINDOW_WIDTH, WINDOW_HEIGHT});
				SDL_SetRenderDrawColor(renderer, 0, 0, 0, fade);
				SDL_RenderFillRect(renderer, &(SDL_Rect) {0, 0, WINDOW_WIDTH, WINDOW_HEIGHT});
				SDL_RenderPresent(renderer);
				if (fade == 0) {
					game_state = STATE_PLAYING;
				}
				break;
		}
#if defined(__EMSCRIPTEN__)
	return;
#else
	}
#endif
	
	end:
	tilemap_free(tilemap), tilemap = NULL;
	SDL_DestroyRenderer(renderer);
	SDL_DestroyWindow(window);
	SDL_QuitSubSystem(INIT_SUBSYSTEMS);
#if defined(__EMSCRIPTEN__)
	emscripten_cancel_main_loop();
#else
	return 0;
#endif
}