#include "main.h"

#ifndef _WIN32
#define DEFAULT_AUDIO_SAMPLE_RATE 96000
#else
/* Windows (well, at least my VM) can't handle 96KHz sound well :( */

/* felsqualle says: For SDL 2.0.6+ using the WASAPI driver, the highest freq.
   we can get is 48000. 96000 also works, but always has some faint crackling in
   the audio, no matter how high or low I set the buffer length...
   Not quite satisfied with that solution, because acc. to SDL2 docs,
   96k + WASAPI *should* work. */

#define DEFAULT_AUDIO_SAMPLE_RATE 48000
#endif

/* Compatibility with older SDL versions */
#ifndef SDL_AUDIO_ALLOW_SAMPLES_CHANGE
#define SDL_AUDIO_ALLOW_SAMPLES_CHANGE 0
#endif

static SDL_GameController *controller = NULL;
static SDL_AudioSpec want_aspec, have_aspec;
static SDL_AudioDeviceID device_id;

static const GThread *main_thread;

static GtkApplication *main_application;
static GtkBuilder *builder;
static GtkGLArea *gl_area;
static GtkDrawingArea *fallback_canvas;

static GtkApplicationWindow *main_window;
static GtkBox *main_window_container;
static GtkWindow *preferences;
static GtkWindow *vram_viewer;
static GtkWindow *memory_viewer;
static GtkWindow *console;
static GtkWindow *printer;

static shader_t shader;

static GuiData gui_data = { { NULL }, NULL };
static GB_gameboy_t gb;
static uint32_t *image_buffers[3];
static unsigned char current_buffer;

static bool supports_gl;
static bool is_fullscreen;

static bool underclock_down = false, rewind_down = false, do_rewind = false, rewind_paused = false, turbo_down = false;
static double clock_mutliplier = 1.0;
static char *battery_save_path_ptr;
static Rect viewport = {0};
static Rect scrollRect = {0};
static bool vram_viewer_visible = false;
static bool vram_viewer_updating = false;
static gchar *vram_viewer_active_tab = "";
static gboolean vram_viewer_is_cgb = false;
static uint8_t vram_viewer_palette_data[16][0x40];
static bool border_mode_changed = false;

static volatile bool running = false;
static volatile bool stopping = false;

#define tileset_buffer_length 256 * 192 * 4
static uint32_t tileset_buffer[tileset_buffer_length] = {0};
static GMutex tileset_buffer_mutex;

#define tilemap_buffer_length 256 * 256 * 4
static uint32_t tilemap_buffer[tilemap_buffer_length] = {0};
static GMutex tilemap_buffer_mutex;

static GB_oam_info_t oamInfo[40];
static uint16_t oamCount;
static uint8_t oamHeight;

static uint8_t pressed_buttons;

static GtkTextBuffer *pending_console_output = NULL;
static gboolean in_sync_input = false;
static gchar *last_console_input = NULL;
static gboolean log_to_sidebar = false;
static gboolean should_clear_sidebar = false;

static GMutex debugger_input_mutex;
static GCond debugger_input_cond;
static GRecMutex console_output_lock;
static GPtrArray *debugger_input_queue;

static const GActionEntry file_entries[] = {
	{ "open", activate_open, NULL, NULL, NULL },
	{ "close", activate_close, NULL, NULL, NULL },
};

static const GActionEntry edit_entries[] = {
	
};

static const GActionEntry emulation_entries[] = {
	{ "reset", activate_reset, NULL, NULL, NULL },
	{ "pause", NULL, NULL, "false", on_pause_changed },
	{ "save_state", NULL, NULL, NULL, NULL },
	{ "load_state", NULL, NULL, NULL, NULL },
};

static const GActionEntry developer_entries[] = {
	{ "show_console", activate_show_console, NULL, NULL, NULL },
	{ "open_gtk_debugger", activate_open_gtk_debugger, NULL, NULL, NULL },
	{ "open_memory_viewer", activate_open_memory_viewer, NULL, NULL, NULL },
	{ "open_vram_viewer", activate_open_vram_viewer, NULL, NULL, NULL },
	{ "toggle_developer_mode", NULL, NULL, "false", NULL },
	{ "clear_console", activate_clear_console, NULL, NULL, NULL },
};

static const GActionEntry app_entries[] = {
	{ "quit", activate_quit, NULL, NULL, NULL },
	{ "about", activate_about, NULL, NULL, NULL },
	{ "preferences", activate_preferences, NULL, NULL, NULL },
	{ "toggle_mute", NULL, NULL, "false", on_mute_changed },
	{ "change_model", NULL, "s", "@s 'CGB'", on_model_changed },
};

int main(int argc, char *argv[]) {
	main_thread = g_thread_self();

	// initialize GB_model_t to invalid value
	gui_data.cli_options.model = -1;
	gui_data.prev_model = -1;

	// Create our GApplication and tell GTK that we are able to handle files
	main_application = gtk_application_new(APP_ID, G_APPLICATION_NON_UNIQUE | G_APPLICATION_HANDLES_OPEN);

	// Define our command line parameters
	GOptionEntry entries[] = {
		{ "version",    'v', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, NULL, "Show the application version", NULL },
		{ "fullscreen", 'f', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, NULL, "Start in fullscreen mode", NULL },
		{ "bootrom",    'b', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &gui_data.cli_options.boot_rom_path, "Path to the boot ROM to use", "<file path>" },
		{ "model",      'm', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, NULL, "Override the model type to emulate", "<model type>" },
		{ "config",     'c', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &gui_data.cli_options.config_path, "Override the path of the configuration file", "<file path>" },
		{ NULL }
	};
	// Setup our command line information
	g_application_add_main_option_entries(G_APPLICATION(main_application), entries);
	g_application_set_option_context_parameter_string(G_APPLICATION(main_application), "[FILE…]");
	g_application_set_option_context_summary(G_APPLICATION(main_application), "SameBoy is an open source Game Boy (DMG) and Game Boy Color (CGB) emulator.");

	// Add signal handlers
	g_signal_connect(main_application, "handle-local-options", G_CALLBACK(handle_local_options), &gui_data);
	g_signal_connect(main_application, "startup", G_CALLBACK(startup), &gui_data);
	g_signal_connect(main_application, "activate", G_CALLBACK(activate), &gui_data);
	g_signal_connect(main_application, "open", G_CALLBACK(open), &gui_data);
	g_signal_connect(main_application, "shutdown", G_CALLBACK(shutdown), &gui_data);

	// Start our GApplication main loop
	int status = g_application_run(G_APPLICATION(main_application), argc, argv);
	g_object_unref(main_application);

	return status;
}

// This function gets called after the parsing of the commandline options has occurred.
static gint handle_local_options(GApplication *app, GVariantDict *options, gpointer gui_data_gptr) {
	GuiData *gui_data = gui_data_gptr;
	guint32 count;

	if (g_variant_dict_lookup(options, "version", "b", &count)) {
		g_message("SameBoy v" xstr(VERSION));
		return EXIT_SUCCESS;
	}

	if (g_variant_dict_lookup(options, "fullscreen", "b", &count)) {
		gui_data->cli_options.fullscreen = true;
	}

	// Handle model override
	GVariant *model_name_var = g_variant_dict_lookup_value(options, "model", G_VARIANT_TYPE_STRING);
	if (model_name_var != NULL) {
		const gchar *model_name = g_variant_get_string(model_name_var, NULL);

		// TODO: Synchronize with GB_model_t (Core/gb.h)
		if (g_str_has_prefix(model_name, "DMG")) {
			gui_data->cli_options.prefix = "DMG";

			if (g_str_has_suffix(model_name, "-B") || g_strcmp0(model_name, "DMG") == 0) {
				gui_data->cli_options.model = GB_MODEL_DMG_B;
			}
			else {
				gui_data->cli_options.model = GB_MODEL_DMG_B;
				g_warning("Unsupported revision: %s\nFalling back to DMG-B", model_name);
			}
		}
		else if (g_str_has_prefix(model_name, "SGB")) {
			gui_data->cli_options.prefix = "SGB";

			if (g_str_has_suffix(model_name, "-NTSC") || g_strcmp0(model_name, "SGB") == 0) {
				gui_data->cli_options.model = GB_MODEL_SGB;
			}
			else if (g_str_has_suffix(model_name, "-PAL")) {
				gui_data->cli_options.model = GB_MODEL_SGB | GB_MODEL_PAL_BIT;
			}
			else if (g_str_has_suffix(model_name, "2")) {
				gui_data->cli_options.model = GB_MODEL_SGB2;
			}
			else {
				gui_data->cli_options.model = GB_MODEL_SGB2;
				g_warning("Unsupported revision: %s\nFalling back to SGB2", model_name);
			}
		}
		else if (g_str_has_prefix(model_name, "CGB")) {
			gui_data->cli_options.prefix = "CGB";

			if (g_str_has_suffix(model_name, "-C")) {
				gui_data->cli_options.model = GB_MODEL_CGB_C;
			}
			else if (g_str_has_suffix(model_name, "-E") || g_strcmp0(model_name, "CGB") == 0) {
				gui_data->cli_options.model = GB_MODEL_CGB_E;
			}
			else {
				gui_data->cli_options.model = GB_MODEL_CGB_E;
				g_warning("Unsupported revision: %s\nFalling back to CGB-E", model_name);
			}
		}
		else if (g_str_has_prefix(model_name, "AGB")) {
			gui_data->cli_options.prefix = "AGB";

			gui_data->cli_options.model = GB_MODEL_AGB;
		}
		else {
			gui_data->cli_options.prefix = NULL;

			g_warning("Unknown model: %s", model_name);
			exit(EXIT_FAILURE);
		}
	}

	return -1;
}

// Workaround to figure out if we have proper OpenGL support.
// Otherwise the application would crash after our GtkGlArea is realized
// and the context it uses is a legacy OpenGL 1.4 context because
// GTK3 calls OpenGL 2.0+ functions on it.
gboolean test_gl_support(void) {
	gboolean result = false;

	GtkWidget *window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
	g_signal_connect(window, "realize", G_CALLBACK(gl_check_realize), &result);
	gtk_widget_realize(window);
	gtk_widget_destroy(window);
	window = NULL;

	return result;
}

// The main function for the OpenGL version check workaround
void gl_check_realize(GtkWidget *w, gpointer user_data_gptr) {
	gboolean *result = (gboolean *) user_data_gptr;

	GError *error = NULL;
	GdkWindow *gdk_window = gtk_widget_get_window(w);
	GdkGLContext *context = gdk_window_create_gl_context(gdk_window, &error);

	if (error != NULL) {
		g_warning("Failed to create context: %s", error->message);
		g_error_free(error);
		*result = false;
	}
	else {
		gdk_gl_context_make_current(context);
		int version = epoxy_gl_version();

		g_object_run_dispose(G_OBJECT(context));
		g_object_unref(context);
		context = NULL;

		gdk_gl_context_clear_current();

		g_debug("OpenGL version: %d", version);

		*result = version >= 32;
	}
}

static gboolean init_controllers() {
	if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) < 0) {
		g_warning("Failed to initialize game controller support: %s", SDL_GetError());
		return false;
	}

	SDL_QuitSubSystem(SDL_INIT_EVENTS);
	SDL_GameControllerEventState(SDL_IGNORE);

	GError *error = NULL;
	GBytes *db_f = g_resources_lookup_data(RESOURCE_PREFIX "gamecontrollerdb.txt", G_RESOURCE_LOOKUP_FLAGS_NONE, &error);

	if (db_f != NULL) {
		gsize db_data_size = 0;
		const guchar *db_data = g_bytes_get_data(db_f, &db_data_size);
		const gint val = SDL_GameControllerAddMappingsFromRW(SDL_RWFromMem((void *)db_data, db_data_size), 1);

		if (val < 0) {
			g_warning("Failed to load controller mappings: %s", SDL_GetError());
		}

		g_bytes_unref(db_f);
	}

	if (error != NULL) g_clear_error(&error);

	// Open the first available controller
	for (int i = 0; i < SDL_NumJoysticks(); ++i) {
		if (SDL_IsGameController(i)) {
			controller = SDL_GameControllerOpen(i);
			if (controller) {
				break;
			}
			else {
				g_warning("Could not open gamecontroller %i: %s", i, SDL_GetError());
			}
		}
	}

	return true;
}

static gboolean init_audio() {
	bool audio_playing = SDL_GetAudioDeviceStatus(device_id) == SDL_AUDIO_PLAYING;
	SDL_PauseAudioDevice(device_id, 1);
	SDL_ClearQueuedAudio(device_id);
	SDL_QuitSubSystem(SDL_INIT_AUDIO);

	if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) {
		g_warning("Failed to initialize audio: %s", SDL_GetError());
		return false;
	}

	memset(&want_aspec, 0, sizeof(want_aspec));
	want_aspec.freq = gui_data.sample_rate;
	want_aspec.format = AUDIO_S16SYS;
	want_aspec.channels = 2;
	want_aspec.samples = 512;

	SDL_version _sdl_version;
	SDL_GetVersion(&_sdl_version);
	unsigned sdl_version = _sdl_version.major * 1000 + _sdl_version.minor * 100 + _sdl_version.patch;

#ifndef _WIN32
	/* SDL 2.0.5 on macOS and Linux introduced a bug where certain combinations of buffer lengths and frequencies
	fail to produce audio correctly. */
	if (sdl_version >= 2005) {
		want_aspec.samples = 2048;
	}
#else
	if (sdl_version < 2006) {
		/* Since WASAPI audio was introduced in SDL 2.0.6, we have to lower the audio frequency
		to 44100 because otherwise we would get garbled audio output.*/
		want_aspec.freq = 44100;
	}
#endif

	device_id = SDL_OpenAudioDevice(0, 0, &want_aspec, &have_aspec, SDL_AUDIO_ALLOW_FREQUENCY_CHANGE | SDL_AUDIO_ALLOW_SAMPLES_CHANGE);

	g_debug("Requested Sample Rate: %d Hz\nUsed Sample Rate: %d Hz", want_aspec.freq, have_aspec.freq);

	SDL_PauseAudioDevice(device_id, audio_playing? 0 : 1);
	GB_set_sample_rate(&gb, have_aspec.freq);

	return true;
}

static GB_model_t get_model() {
	if (gui_data.cli_options.model != -1) {
		return gui_data.cli_options.model;
	}

	GAction *action = g_action_map_lookup_action(G_ACTION_MAP(main_application), "change_model");
	GVariant *value = g_action_get_state(action);
	const gchar *family = g_variant_get_string(value, NULL);

	if (g_strcmp0(family, "DMG") == 0) {
		return get_dmg_model();
	}
	else if (g_strcmp0(family, "AGB") == 0) {
		return GB_MODEL_AGB;
	}
	else if (g_strcmp0(family, "SGB") == 0) {
		return get_sgb_model();
	}

	return get_cgb_model();
}

static void gb_audio_callback(GB_gameboy_t *gb, GB_sample_t *sample) {
	if (turbo_down) {
		static unsigned skip = 0;
		skip++;
		if (skip == have_aspec.freq / 8) {
			skip = 0;
		}
		if (skip > have_aspec.freq / 16) {
			return;
		}
	}

	if (SDL_GetQueuedAudioSize(device_id) / sizeof(*sample) > have_aspec.freq / 4) {
		return;
	}

	SDL_QueueAudio(device_id, sample, sizeof(*sample));
}

// Console TODO:
// TODO: clear sidebar when switching to async mode
// TODO: Command history (up / down arrow in input)
// TODO: reverse search of commands
// TODO: search in output
static char *sync_console_input(GB_gameboy_t *gb) {
	update_debugger_sidebar(gb);
	console_log(gb, "> ", 0);
	in_sync_input = true;

	g_mutex_lock(&debugger_input_mutex);
	g_cond_wait(&debugger_input_cond, &debugger_input_mutex);

	gchar *input = NULL;
	const gchar *_input = g_ptr_array_index(debugger_input_queue, 0);
	input = g_strdup(_input);
	gpointer ptr = g_ptr_array_remove_index(debugger_input_queue, 0);
	if (ptr) g_free(ptr);

	g_mutex_unlock(&debugger_input_mutex);

	in_sync_input = false;

	return input;
}

static void clear_sidebar() {
	GtkTextView *sidebar_output = builder_get(GTK_TEXT_VIEW, "console_sidebar_output");
	GtkTextBuffer *sidebar_output_text_buf = gtk_text_view_get_buffer(sidebar_output);
	gtk_text_buffer_set_text(sidebar_output_text_buf, "", -1);
}

static char *async_console_input(GB_gameboy_t *gb) {
	// TODO: This is rather ugly
	g_idle_add((GSourceFunc) clear_sidebar, NULL);

	if (debugger_input_queue->len == 0) return NULL;

	g_mutex_lock(&debugger_input_mutex);

	gchar *input = NULL;
	const gchar *_input = g_ptr_array_index(debugger_input_queue, 0);
	if (_input) {
		input = g_strdup(_input);
		gpointer ptr = g_ptr_array_remove_index(debugger_input_queue, 0);
		if (ptr) g_free(ptr);
	}

	g_mutex_unlock(&debugger_input_mutex);

	return input;
}

static void update_debugger_sidebar(GB_gameboy_t *gb) {
	if (!GB_debugger_is_stopped(gb)) {
		return;
	}

	if (main_thread != g_thread_self()) {
		g_idle_add((GSourceFunc) update_debugger_sidebar, gb);
		return;
	}

	g_rec_mutex_lock(&console_output_lock);
	should_clear_sidebar = true;
	append_pending_output();
	log_to_sidebar = true;
	g_rec_mutex_unlock(&console_output_lock);

	GtkTextView *sidebar_input = builder_get(GTK_TEXT_VIEW, "console_sidebar_input");
	GtkTextBuffer *sidebar_input_text_buf = gtk_text_view_get_buffer(sidebar_input);

	gint line_count = gtk_text_buffer_get_line_count(sidebar_input_text_buf);

	for (unsigned line = 0; line < line_count; ++line) {
		GtkTextIter start_iter;
		GtkTextIter end_iter;
		gunichar ch;

		gtk_text_buffer_get_iter_at_line(sidebar_input_text_buf, &start_iter, line);
		end_iter = start_iter;

		do {
			ch = gtk_text_iter_get_char(&end_iter);
			if (!gtk_text_iter_forward_char(&end_iter)) {
				break;
			}
		}
		while (ch != '\n');

		gchar *cmd = gtk_text_buffer_get_text(sidebar_input_text_buf, &start_iter, &end_iter, false);
		g_strchug(cmd); // trim leading whitespace
		g_strchomp(cmd); // trim trailing whitespace

		if (g_strcmp0("", cmd) != 0) {
			char *duped = g_strdup(cmd);
			GB_attributed_log(gb, GB_LOG_BOLD, "%s:\n", duped);
			GB_debugger_execute_command(gb, duped);
			GB_log(gb, "\n");
			g_free(duped);
		}

		g_free(cmd);
	}

	g_rec_mutex_lock(&console_output_lock);
	append_pending_output();
	log_to_sidebar = false;
	g_rec_mutex_unlock(&console_output_lock);
}

static gboolean scroll_to_bottom(GtkTextView *textview, GtkTextMark *mark) {
	GtkTextBuffer *buffer = gtk_text_view_get_buffer(textview);
	GtkTextIter iter;

	gtk_text_buffer_get_end_iter(buffer, &iter);
	gtk_text_iter_set_line_offset(&iter, 0);

	gtk_text_buffer_move_mark(buffer, mark, &iter);
	gtk_text_view_scroll_to_mark(textview, mark, 0.0, true, 0.0, 0.10);

	gtk_text_buffer_delete_mark(buffer, mark);

	return true;
}

static void append_pending_output(void) {
	g_rec_mutex_lock(&console_output_lock);

	if (should_clear_sidebar) {
		clear_sidebar();
		should_clear_sidebar = false;
	}

	if (gtk_text_buffer_get_char_count(pending_console_output) > 0) {
		GtkTextView *text_view = builder_get(GTK_TEXT_VIEW, log_to_sidebar? "console_sidebar_output" : "console_screen");
		GtkTextBuffer *text_buf = gtk_text_view_get_buffer(text_view);
		GtkTextIter start;
		GtkTextIter end;
		gtk_text_buffer_get_start_iter(pending_console_output, &start);
		gtk_text_buffer_get_end_iter(pending_console_output, &end);

		GtkTextIter iter;
		gtk_text_buffer_get_end_iter(text_buf, &iter);
		gtk_text_buffer_insert_range(text_buf, &iter, &start, &end);

		scroll_to_bottom(text_view, gtk_text_buffer_create_mark(text_buf, NULL, &iter, true));

		gtk_text_buffer_set_text(pending_console_output, "", -1);
	}

	g_rec_mutex_unlock(&console_output_lock);
}

static void console_log(GB_gameboy_t *gb, const char *string, GB_log_attributes attributes) {
	g_rec_mutex_lock(&console_output_lock);

	if (string != NULL && !g_str_equal("", string)) {
		GtkTextIter iter;
		GtkTextIter start;

		// Append attributed text to "pending_console_output" GtkTextBuffer
		gtk_text_buffer_get_end_iter(pending_console_output, &iter);
		GtkTextMark *start_mark = gtk_text_buffer_create_mark(pending_console_output, NULL, &iter, true);
		gtk_text_buffer_insert(pending_console_output, &iter, g_strdup(string), -1);
		gtk_text_buffer_get_iter_at_mark(pending_console_output, &start, start_mark);

		if (attributes & GB_LOG_BOLD) {
			gtk_text_buffer_apply_tag_by_name(pending_console_output, "bold", &start, &iter);
		}

		if (attributes & GB_LOG_DASHED_UNDERLINE) {
			gtk_text_buffer_apply_tag_by_name(pending_console_output, "dashed_underline", &start, &iter);
		}

		if (attributes & GB_LOG_UNDERLINE) {
			gtk_text_buffer_apply_tag_by_name(pending_console_output, "underline", &start, &iter);
		}

		gtk_text_buffer_delete_mark(pending_console_output, start_mark);

		g_idle_add((GSourceFunc) append_pending_output, NULL);
	}

	g_rec_mutex_unlock(&console_output_lock);
}

// Returns a `GApplication`s `GMenuModel` by ID
// GApplication menus are loaded from `gtk/menus.ui`, `gtk/menus-traditional.ui` and `gtk/menus-common.ui`.
static GMenuModel *get_menu_model(GApplication *app, const char *id) {
	GMenu *menu;

	menu = gtk_application_get_menu_by_id(GTK_APPLICATION(app), id);

	return menu ? G_MENU_MODEL(g_object_ref_sink(menu)) : NULL;
}

// Create a GtkDrawingArea as a fallback in case we can’t use OpenGL
static void create_fallback_canvas(void) {
	fallback_canvas = GTK_DRAWING_AREA(gtk_drawing_area_new());
	g_signal_connect(fallback_canvas, "draw", G_CALLBACK(on_draw_fallback), NULL);
	g_signal_connect(fallback_canvas, "size-allocate", G_CALLBACK(resize), NULL);
	gtk_box_pack_end(GTK_BOX(main_window_container), GTK_WIDGET(fallback_canvas), true, true, 0);
}

// Create our application’s menu.
//
// This function tries to stick to the desktop environment’s conventions.
// For the GNOME Shell it uses a hamburger menu, otherwise it either lets
// the desktop environment shell handle the menu if it signals support for it
// or uses a standard menubar inside the window.
static void setup_menu(GApplication *app) {
	GMenuModel *menubar_model = get_menu_model(app, "menubar");
	enum menubar_type_t menubar_type = get_show_menubar();

	// Try to use a sane default
	if (menubar_type == MENUBAR_AUTO) {
		GtkSettings *settings = gtk_settings_get_default();
		gboolean show_in_shell;
		g_object_get(settings, "gtk-shell-shows-menubar", &show_in_shell, NULL);

		const gchar *xdg_current_desktop = g_getenv("XDG_CURRENT_DESKTOP");
		const gchar *gdm_session = g_getenv("GDMSESSION");
		const gchar *desktop_session = g_getenv("DESKTOP_SESSION");

		gchar *desktop = (gchar *)xdg_current_desktop;
		if (desktop == NULL || g_str_equal(desktop, "")) desktop = (gchar *)gdm_session;
		if (desktop == NULL || g_str_equal(desktop, "")) desktop = (gchar *)desktop_session;

		g_debug("XDG_CURRENT_DESKTOP: %s\nGDMSESSION: %s\nDESKTOP_SESSION: %s\nChosen value: %s\nShow menu in shell: %d", xdg_current_desktop, gdm_session, desktop_session, desktop, show_in_shell);

		if (desktop != NULL && show_in_shell) {
			menubar_type = MENUBAR_SHOW_IN_SHELL;
		}
		else if (desktop != NULL && g_str_match_string("GNOME", desktop, false)) {
			if (g_str_match_string("GNOME-Flashback", desktop, false) || g_str_match_string("GNOME-Classic", desktop, false)) {
				menubar_type = MENUBAR_SHOW_IN_WINDOW;
			}
			else if (gdm_session != NULL && (g_str_match_string("gnome-classic", gdm_session, false) || g_str_match_string("gnome-flashback", gdm_session, false))) {
				menubar_type = MENUBAR_SHOW_IN_WINDOW;
			}
			else {
				menubar_type = MENUBAR_SHOW_HAMBURGER;
			}
		}
		else {
			menubar_type = MENUBAR_SHOW_IN_WINDOW;
		}
	}

	switch (menubar_type) {
		case MENUBAR_AUTO:
			g_warning("Unreachable");
			break;

		case MENUBAR_SHOW_IN_SHELL:
			g_debug("Showing menu in the shell");
			gtk_application_set_menubar(GTK_APPLICATION(app), menubar_model);
			break;

		case MENUBAR_SHOW_IN_WINDOW: {
			g_debug("Showing menu in the window");
			GtkMenuBar *menubar = GTK_MENU_BAR(gtk_menu_bar_new_from_model(menubar_model));
			gtk_box_pack_start(GTK_BOX(main_window_container), GTK_WIDGET(menubar), false, false, 0);
			break;
		}

		case MENUBAR_SHOW_HAMBURGER: {
			g_debug("Showing hamburger");
			// Attach a custom title bar
			GtkWidget *titlebar = builder_get(GTK_WIDGET, "main_header_bar");
			gtk_header_bar_set_title(GTK_HEADER_BAR(titlebar), gtk_window_get_title(GTK_WINDOW(main_window)));
			gtk_window_set_titlebar(GTK_WINDOW(main_window), titlebar);

			// Disable menubar
			gtk_application_set_menubar(GTK_APPLICATION(app), NULL);

			// Hook menubar up to the hamburger button
			GtkMenuButton *hamburger_button = GTK_MENU_BUTTON(get_object("hamburger_button"));
			gtk_menu_button_set_menu_model(hamburger_button, menubar_model);
			break;
		}
	}
}

// Recursively goes through all children of the given container and sets
// our `is_separator` function to all children of type`GtkComboBox`
static void set_combo_box_row_separator_func(GtkContainer *container) {
	GList *children = gtk_container_get_children(container);

	for (GList *l = children; l; l = l->next) {
		if (GTK_IS_COMBO_BOX(l->data)) {
			gtk_combo_box_set_row_separator_func(GTK_COMBO_BOX(l->data), is_separator, NULL, NULL);
		}

		if (GTK_IS_CONTAINER(l->data)) {
			set_combo_box_row_separator_func(GTK_CONTAINER(l->data));
		}
	}

	g_list_free(children);
}

// Determines if a ComboBox entry should be converted into a separator.
// Each element with a text value of `<separator>` will be converted into a separator element.
static gboolean is_separator(GtkTreeModel *model, GtkTreeIter *iter, gpointer data) {
	gchar *text = NULL;

	gtk_tree_model_get(model, iter, 0, &text, -1);
	gboolean result = g_strcmp0("<separator>", text) == 0;
	g_free(text);

	return result;
}

// Determines how many frame buffers to use
static unsigned char number_of_buffers(void) {
	if (fallback_canvas) return 2;

	bool should_blend = get_frame_blending_mode() != GB_FRAME_BLENDING_MODE_DISABLED;

	return should_blend? 3 : 2;
}

// Returns the buffer that should be used by the Core to render a new frame to
static uint32_t *get_pixels(void) {
	return image_buffers[(current_buffer + 1) % number_of_buffers()];
}

// Returns the current finished frame
static uint32_t *get_current_buffer(void) {
	return image_buffers[current_buffer];
}

// Returns the previous finished frame
static uint32_t *get_previous_buffer(void) {
	return image_buffers[(current_buffer + 2) % number_of_buffers()];
}

// Cycles the buffers
static void flip(void) {
	current_buffer = (current_buffer + 1) % number_of_buffers();
}

static void quit_interrupt(int ignored) {
	quit();
}

// WHY DO WE NEED SUCH AN UGLY METHOD, GTK?!
static void action_entries_set_enabled(const GActionEntry *entries, unsigned n_entries, bool value) {
	// Assumes null-terminated if n_entries == -1
	for (unsigned i = 0; n_entries == -1 ? entries[i].name != NULL : i < n_entries; i++) {
		const GActionEntry *entry = &entries[i];
		if (entry->name == NULL) continue;

		action_set_enabled(main_application, entry->name, value);
	}
}

static void create_action_groups(GApplication *app) {
	g_action_map_add_action_entries(G_ACTION_MAP(app), emulation_entries, G_N_ELEMENTS(emulation_entries), NULL);
	g_action_map_add_action_entries(G_ACTION_MAP(app), developer_entries, G_N_ELEMENTS(developer_entries), NULL);
	g_action_map_add_action_entries(G_ACTION_MAP(app), app_entries,       G_N_ELEMENTS(app_entries),       NULL);
	g_action_map_add_action_entries(G_ACTION_MAP(app), file_entries,      G_N_ELEMENTS(file_entries),      NULL);
	g_action_map_add_action_entries(G_ACTION_MAP(app), edit_entries,      G_N_ELEMENTS(edit_entries),      NULL);

	action_set_enabled(app, "close", false);
	action_entries_set_enabled(emulation_entries, G_N_ELEMENTS(emulation_entries), false);
}

// This functions gets called immediately after registration of the GApplication
static void startup(GApplication *app, gpointer gui_data_gptr) {
	GuiData *gui_data = gui_data_gptr;

	signal(SIGINT, quit_interrupt);

	// Very ugly workaround for GtkGlArea!
	// When a GtkGlArea is realized and it creates a legacy GL 1.4 context
	// it tries to use GL 2.0 functions to render the window which leads to the application crashing.
	// So we initialize GTK, create a dummy GtkWindow object, attach a `realize` callback and
	// in this callback create a GdkGLContext on this window. But instead of running the GTK main loop
	// we just realize and destroy the dummy window and compare the context’s version in the realize callback.
	supports_gl = test_gl_support();
	g_debug("OpenGL supported: %s", supports_gl? "Yes" : "No");

	builder = gtk_builder_new_from_resource(RESOURCE_PREFIX "ui/window.ui");
	gtk_builder_connect_signals(builder, NULL);

	create_action_groups(app);

	if (gui_data->cli_options.prefix != NULL) {
		GAction *action = g_action_map_lookup_action(G_ACTION_MAP(main_application), "change_model");
		g_action_change_state(action, g_variant_new_string(gui_data->cli_options.prefix));
	}

#if NDEBUG
	// Disable when not compiled in debug mode
	action_set_enabled(app, "open_gtk_debugger", false);

	// Remove the menubar override
	gtk_widget_destroy(builder_get(GTK_WIDGET, "menubar_override_selector_label"));
	gtk_widget_destroy(builder_get(GTK_WIDGET, "menubar_override_selector"));
#endif

	preferences = GTK_WINDOW(get_object("preferences"));

	g_signal_connect(preferences, "realize", G_CALLBACK(on_preferences_realize), (gpointer) builder);
	init_settings(gui_data->cli_options.config_path, preferences);

	vram_viewer = GTK_WINDOW(get_object("vram_viewer"));
	memory_viewer = GTK_WINDOW(get_object("memory_viewer"));

	console = GTK_WINDOW(get_object("console"));
	printer = GTK_WINDOW(get_object("printer"));

	if (config.sample_rate == -1) {
		gui_data->sample_rate = DEFAULT_AUDIO_SAMPLE_RATE;
	}
	else {
		gui_data->sample_rate = config.sample_rate;
	}

	// setup main window
	main_window = GTK_APPLICATION_WINDOW(gtk_application_window_new(GTK_APPLICATION(app)));
	main_window_container = GTK_BOX(gtk_box_new(GTK_ORIENTATION_VERTICAL, 0));

	gtk_window_set_title(GTK_WINDOW(main_window), "SameBoy");
	gtk_application_window_set_show_menubar(main_window, false);
	gtk_container_add(GTK_CONTAINER(main_window), GTK_WIDGET(main_window_container));

	setup_menu(app);

	// Insert separators into `GtkComboBox`es
	set_combo_box_row_separator_func(GTK_CONTAINER(preferences));
	set_combo_box_row_separator_func(GTK_CONTAINER(vram_viewer));
	set_combo_box_row_separator_func(GTK_CONTAINER(memory_viewer));

	// Define a set of window icons
	GList *icon_list = NULL;
	static char* icons[] = {
		RESOURCE_PREFIX "logo_256.png",
		RESOURCE_PREFIX "logo_128.png",
		RESOURCE_PREFIX "logo_64.png",
		RESOURCE_PREFIX "logo_48.png",
		RESOURCE_PREFIX "logo_32.png",
		RESOURCE_PREFIX "logo_16.png"
	};

	// Create list of GdkPixbufs
	for (int i = 0; i < (sizeof(icons) / sizeof(const char*)); ++i) {
		GdkPixbuf *icon = gdk_pixbuf_new_from_resource(icons[i], NULL);
		if (!icon) continue;

		icon_list = g_list_prepend(icon_list, icon);
	}

	// Let GTK choose the proper icon
	gtk_window_set_icon_list(GTK_WINDOW(main_window), icon_list);

	// Add missing information to the about dialog
	GtkAboutDialog *about_dialog = GTK_ABOUT_DIALOG(get_object("about_dialog"));
	gtk_about_dialog_set_logo(about_dialog, gdk_pixbuf_new_from_resource(icons[2], NULL)); // reuse the 64x64 icon
	gtk_about_dialog_set_version(about_dialog, "v" xstr(VERSION));
	g_list_free_full(icon_list, g_object_unref);
}

static void connect_signal_handlers(GApplication *app) {
	// Connect signal handlers
	gtk_widget_add_events(GTK_WIDGET(main_window), GDK_KEY_PRESS_MASK);
	gtk_widget_add_events(GTK_WIDGET(main_window), GDK_KEY_RELEASE_MASK);

	g_signal_connect(main_window, "destroy", G_CALLBACK(on_quit), app);
	g_signal_connect(main_window, "key_press_event", G_CALLBACK(on_key_press), NULL);
	g_signal_connect(main_window, "key_release_event", G_CALLBACK(on_key_press), NULL);
	g_signal_connect(main_window, "window-state-event", G_CALLBACK(on_window_state_change), NULL);

	g_signal_connect(vram_viewer, "realize",   G_CALLBACK(on_vram_viewer_realize), NULL);
	g_signal_connect(vram_viewer, "unrealize", G_CALLBACK(on_vram_viewer_unrealize), NULL);

	// Just hide our sub-windows when closing them
	g_signal_connect(preferences,   "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), NULL);
	g_signal_connect(vram_viewer,   "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), NULL);
	g_signal_connect(memory_viewer, "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), NULL);
	g_signal_connect(console,       "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), NULL);
	g_signal_connect(printer,       "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), NULL);

	g_signal_connect(get_object("vram_viewer_tileset_canvas"), "draw", G_CALLBACK(on_draw_vram_viewer_tileset), NULL);
	g_signal_connect(get_object("vram_viewer_tilemap_canvas"), "draw", G_CALLBACK(on_draw_vram_viewer_tilemap), NULL);

	gtk_widget_add_events(builder_get(GTK_WIDGET, "vram_viewer_tileset_canvas"), GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK);
	gtk_widget_add_events(builder_get(GTK_WIDGET, "vram_viewer_tilemap_canvas"), GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK);

	g_signal_connect(get_object("vram_viewer_tileset_canvas"), "motion_notify_event", G_CALLBACK(on_motion_vram_viewer_tileset), NULL);
	g_signal_connect(get_object("vram_viewer_tilemap_canvas"), "motion_notify_event", G_CALLBACK(on_motion_vram_viewer_tilemap), NULL);

	g_signal_connect(get_object("vram_viewer_stack"), "notify::visible-child", G_CALLBACK(on_vram_tab_change), NULL);
}

static void create_canvas() {
	// create our renderer area
	if (supports_gl) {
		gl_area = GTK_GL_AREA(gtk_gl_area_new());
		gtk_gl_area_set_required_version(gl_area, 3, 2);
		gtk_gl_area_set_auto_render(gl_area, false);
		gtk_gl_area_set_has_alpha(gl_area, false);
		gtk_gl_area_set_has_depth_buffer(gl_area, false);
		gtk_gl_area_set_has_stencil_buffer(gl_area, false);
		g_signal_connect(gl_area, "realize", G_CALLBACK(gl_init), NULL);
		gtk_box_pack_end(GTK_BOX(main_window_container), GTK_WIDGET(gl_area), true, true, 0);
	}
	else {
		create_fallback_canvas();
	}

	GdkScreen *screen = gdk_screen_get_default();
	GtkCssProvider *provider = gtk_css_provider_new();
	gtk_css_provider_load_from_resource(provider, RESOURCE_PREFIX "css/main.css");
	gtk_style_context_add_provider_for_screen(screen, GTK_STYLE_PROVIDER(provider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
}

static void setup_console() {
	GtkTextView *text_view = builder_get(GTK_TEXT_VIEW, "console_screen");
	GtkTextBuffer *text_buf = gtk_text_view_get_buffer(text_view);

	gtk_text_view_set_buffer(
		builder_get(GTK_TEXT_VIEW, "console_sidebar_output"),
		gtk_text_buffer_new(gtk_text_buffer_get_tag_table(text_buf))
	);

	gtk_text_buffer_create_tag(text_buf, "bold",             "weight",    PANGO_WEIGHT_BOLD, NULL);
	gtk_text_buffer_create_tag(text_buf, "underline",        "underline", PANGO_UNDERLINE_SINGLE, "underline-set", true, NULL);
	gtk_text_buffer_create_tag(text_buf, "dashed_underline", "underline", PANGO_UNDERLINE_DOUBLE, "underline-set", true, NULL);

	g_mutex_init(&debugger_input_mutex);
	g_cond_init(&debugger_input_cond);
	g_rec_mutex_init(&console_output_lock);

	if (!debugger_input_queue) {
		debugger_input_queue = g_ptr_array_sized_new(4);
	}

	if (!pending_console_output) {
		pending_console_output = gtk_text_buffer_new(gtk_text_buffer_get_tag_table(text_buf));
	}
}

// This function gets called when the GApplication gets activated, i.e. it is ready to show widgets.
static void activate(GApplication *app, gpointer gui_data_gptr) {
	GuiData *gui_data = gui_data_gptr;

	// initialize SameBoy core
	init(gui_data);

	init_audio();
	init_controllers();

	connect_signal_handlers(app);
	create_canvas();
	setup_console();

	if (gui_data->cli_options.fullscreen) {
		gtk_window_fullscreen(GTK_WINDOW(main_window));
	}

	gtk_application_add_window(GTK_APPLICATION(app), GTK_WINDOW(main_window));
	gtk_widget_show_all(GTK_WIDGET(main_window));

	// Start the emulation thread
	run(gui_data);
}

// This function gets called when the application is closed.
static void shutdown(GApplication *app, GFile **files, gint n_files, const gchar *hint, gpointer gui_data_gptr) {
	g_debug("SHUTDOWN");

	stop(&gui_data);
	while (stopping);

	g_object_unref(builder);

	save_settings();
	free_settings();
	SDL_Quit();

	if (image_buffers[0]) g_free(image_buffers[0]);
	if (image_buffers[1]) g_free(image_buffers[1]);
	if (image_buffers[2]) g_free(image_buffers[2]);
	free_shader(&shader);
	free_master_shader();

	GB_free(&gb);
}

// This function gets called when there are files to open.
// Note: When `open` gets called `activate` won’t fire unless we call it ourselves.
static void open(GApplication *app, GFile **files, gint n_files, const gchar *hint, gpointer gui_data_gptr) {
	GuiData *gui_data = gui_data_gptr;

	if (n_files > 1) {
		g_warning("More than one file specified");
		exit(EXIT_FAILURE);
	}

	gui_data->file = g_file_dup(files[0]);

	// We have handled the files, now activate the application
	activate(app, gui_data_gptr);
}

// Tell our application to quit.
// After this functions has been called the `shutdown` signal will be issued.
//
// TODO: Make sure we have a way to quit our emulation loop before `shutdown` gets called
static void quit() {
	stop(&gui_data);

	// Quit our application properly.
	// This fires the “shutdown” signal.
	g_application_quit(G_APPLICATION(main_application));
}

static gboolean on_key_press(GtkWidget *w, GdkEventKey *event, gpointer data) {
	uint8_t mask;

	// TODO: Allow control remapping in the GUI
	switch (event->keyval) {
		case GDK_KEY_w: mask = BUTTON_MASK_UP; break;
		case GDK_KEY_a: mask = BUTTON_MASK_LEFT; break;
		case GDK_KEY_s: mask = BUTTON_MASK_DOWN; break;
		case GDK_KEY_d: mask = BUTTON_MASK_RIGHT; break;

		case GDK_KEY_g: mask = BUTTON_MASK_SELECT; break;
		case GDK_KEY_h: mask = BUTTON_MASK_START; break;

		case GDK_KEY_k: mask = BUTTON_MASK_B; break;
		case GDK_KEY_l: mask = BUTTON_MASK_A; break;

		case GDK_KEY_Tab: {
			rewind_down = event->type == GDK_KEY_PRESS;
			GB_set_turbo_mode(&gb, turbo_down, turbo_down && rewind_down);

			if (event->type == GDK_KEY_RELEASE) {
				rewind_paused = false;
			}
		break; }

		case GDK_KEY_space: {
			turbo_down = event->type == GDK_KEY_PRESS;
			SDL_ClearQueuedAudio(device_id);
			GB_set_turbo_mode(&gb, turbo_down, turbo_down && rewind_down);
		break; }

		case GDK_KEY_dead_acute: // fall through
		case GDK_KEY_acute: // fall through
		case GDK_KEY_apostrophe:
			underclock_down = event->type == GDK_KEY_PRESS;
		break;

		case GDK_KEY_F11: {
			if (event->type == GDK_KEY_RELEASE) {
				if (is_fullscreen) {
					gtk_window_unfullscreen(GTK_WINDOW(main_window));
				}
				else {
					gtk_window_fullscreen(GTK_WINDOW(main_window));
				}
			}
		break; }
	}

	if (event->type == GDK_KEY_PRESS) {
		pressed_buttons |= mask;
	}
	else if (event->type == GDK_KEY_RELEASE) {
		pressed_buttons &= ~mask;
	}

	return false;
}

static void on_window_state_change(GtkWidget *w, GdkEventWindowState *event, gpointer data) {
	is_fullscreen = event->new_window_state & GDK_WINDOW_STATE_FULLSCREEN;
}

// app.about GAction
// Opens the about dialog
static void activate_about(GSimpleAction *action, GVariant *parameter, gpointer app) {
	GObject *dialog = get_object("about_dialog");
	gtk_dialog_run(GTK_DIALOG(dialog));
	gtk_widget_hide(GTK_WIDGET(dialog));
}

// app.show_console GAction
// Opens the console
static void activate_show_console(GSimpleAction *action, GVariant *parameter, gpointer app) {
	if (debugger_input_queue) {
		while (debugger_input_queue->len) {
			g_ptr_array_remove_index_fast(debugger_input_queue, debugger_input_queue->len - 1);
		}
	}

	gtk_widget_show_all(builder_get(GTK_WIDGET, "console"));
}

// app.open_gtk_debugger GAction
// Opens the GTK debugger
static void activate_open_gtk_debugger(GSimpleAction *action, GVariant *parameter, gpointer app) {
	gtk_window_set_interactive_debugging(true);
}

// app.open_memory_viewer GAction
// Opens the memory viewer window
static void activate_open_memory_viewer(GSimpleAction *action, GVariant *parameter, gpointer app) {
	gtk_widget_show_all(GTK_WIDGET(memory_viewer));
}

// app.open_vram_viewer GAction
// Opens the VRAM viewer window
static void activate_open_vram_viewer(GSimpleAction *action, GVariant *parameter, gpointer app) {
	gtk_widget_show_all(GTK_WIDGET(vram_viewer));
}

// app.open GAction
// Opens a ROM file
static void activate_open(GSimpleAction *action, GVariant *parameter, gpointer app) {
	stop(&gui_data);

	GtkFileChooserNative *native = gtk_file_chooser_native_new("Open File", GTK_WINDOW(main_window), GTK_FILE_CHOOSER_ACTION_OPEN, "_Open", "_Cancel");
	gint res = gtk_native_dialog_run(GTK_NATIVE_DIALOG(native));

	if (res == GTK_RESPONSE_ACCEPT) {
		const char* path = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(native));
		gui_data.file = g_file_new_for_path(path);

		activate_reset(action, parameter, app);
	}
	else {
		run(&gui_data);
	}

	g_object_unref(native);
}

// app.close GAction
// Closes a ROM
static void activate_close(GSimpleAction *action, GVariant *parameter, gpointer app) {
	stop(&gui_data);
	GB_free(&gb);
	
	// Clear the screen as side effect
	update_window_geometry();
	gtk_widget_queue_draw(fallback_canvas ? GTK_WIDGET(fallback_canvas) : GTK_WIDGET(gl_area));

	// Clear the VRAM viewer
	g_mutex_lock(&tileset_buffer_mutex);
	memset(tileset_buffer, 0, sizeof tileset_buffer);
	g_mutex_unlock(&tileset_buffer_mutex);
	
	g_mutex_lock(&tilemap_buffer_mutex);
	memset(tilemap_buffer, 0, sizeof tilemap_buffer);
	g_mutex_unlock(&tilemap_buffer_mutex);

	gtk_stack_set_visible_child_name(builder_get(GTK_STACK, "vram_viewer_stack"), "vram_viewer_tileset");
	gtk_tree_view_set_model(builder_get(GTK_TREE_VIEW, "vram_viewer_sprites"), NULL);
	gtk_tree_view_set_model(builder_get(GTK_TREE_VIEW, "vram_viewer_palettes"), NULL);

	// Redraw the VRAM viewer
	gtk_widget_queue_draw(GTK_WIDGET(vram_viewer));

	// Update menu action states
	action_set_enabled(main_application, "close", false);
	action_entries_set_enabled(emulation_entries, G_N_ELEMENTS(emulation_entries), false);
}

// app.preferences GAction
// Opens the preferences window
static void activate_preferences(GSimpleAction *action, GVariant *parameter, gpointer app) {
	gtk_widget_show_all(GTK_WIDGET(get_object("preferences")));
}

// app.quit GAction
// Exits the application
static void activate_quit(GSimpleAction *action, GVariant *parameter, gpointer app) {
	quit();
}

// app.reset GAction
// Resets the emulation
static void activate_reset(GSimpleAction *action, GVariant *parameter, gpointer app) {
	if (!GB_is_inited(&gb)) {
		init(&gui_data);
	}

	stop(&gui_data);
	reset(&gui_data);
	run(&gui_data);
}

// app.clear_console GAction
// Clears the debugger console
static void activate_clear_console(GSimpleAction *action, GVariant *parameter, gpointer app) {
	g_rec_mutex_lock(&console_output_lock);

	GtkTextView *text_view = builder_get(GTK_TEXT_VIEW, "console_screen");
	GtkTextBuffer *text_buf = gtk_text_view_get_buffer(text_view);
	gtk_text_buffer_set_text(text_buf, "", -1);

	g_rec_mutex_unlock(&console_output_lock);
}

static void on_model_changed(GSimpleAction *action, GVariant *value, gpointer user_data) {
	if (!GB_is_inited(&gb)) {
		g_simple_action_set_state(action, value);
		return;
	}

	const gchar *model_str = g_variant_get_string(value, NULL);

	GtkMessageDialog *dialog = GTK_MESSAGE_DIALOG(gtk_message_dialog_new(
		GTK_WINDOW(main_window),
		GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT,
		GTK_MESSAGE_QUESTION,
		GTK_BUTTONS_YES_NO,
		"Changing the emulated model requires a reset.\nChange model and reset game?"
	));

	stop(&gui_data);
	gint result = gtk_dialog_run(GTK_DIALOG(dialog));

	switch (result) {
		case GTK_RESPONSE_YES:
			// Reset the CLI model override
			gui_data.cli_options.model = -1;

			g_simple_action_set_state(action, value);
			reset(&gui_data);
			break;
		default:
			// Action has been canceled
			break;
	}

	run(&gui_data);
	gtk_widget_destroy(GTK_WIDGET(dialog));
}

static void on_mute_changed(GSimpleAction *action, GVariant *value, gpointer user_data) {
	gboolean do_mute = g_variant_get_boolean(value);

	if (do_mute) {
		SDL_PauseAudioDevice(device_id, 1);
	}
	else {
		SDL_ClearQueuedAudio(device_id);
		SDL_PauseAudioDevice(device_id, 0);
	}

	g_simple_action_set_state(action, value);
}

static void on_pause_changed(GSimpleAction *action, GVariant *value, gpointer user_data) {
	if (g_variant_get_boolean(value)) {
		stop(&gui_data);
	}
	else {
		run(&gui_data);
	}

	g_simple_action_set_state(action, value);
}

// `destroy` signal GCallback
// Exits the application
static void on_quit(GtkWidget *w, gpointer app) {
	quit();
}

// TODO: Comment
static void gl_init(GtkWidget *w) {
	GtkGLArea *gl_area = GTK_GL_AREA(w);

	g_debug("GL_INIT");
	const char *renderer;

	g_debug("GL Context: %p", gtk_gl_area_get_context(gl_area));

	gtk_gl_area_make_current(gl_area);

	if (gtk_gl_area_get_error(gl_area) != NULL) {
		goto error;
	}

	renderer = (char *)glGetString(GL_RENDERER);
	g_debug("GtkGLArea on %s", renderer ? renderer : "Unknown");

	if (config.shader == NULL || (!init_shader_with_name(&shader, config.shader) && !init_shader_with_name(&shader, "NearestNeighbor"))) {
		GError *error = g_error_new_literal(g_quark_from_string("sameboy-gl-error"), 1, "Failed to initialize shaders");
		gtk_gl_area_set_error(gl_area, error);
	}
	else {
		g_signal_connect(gl_area, "render",    G_CALLBACK(gl_draw), NULL);
		g_signal_connect(gl_area, "resize",    G_CALLBACK(resize), NULL);
		g_signal_connect(gl_area, "unrealize", G_CALLBACK(gl_finish), NULL);
		return;
	}

	error:
		if (gtk_gl_area_get_error(gl_area) != NULL) {
			g_warning("GtkGLArea: %s", gtk_gl_area_get_error(gl_area)->message);
		}

		create_fallback_canvas();
}

// TODO: Comment
static void gl_draw() {
	render_texture(get_current_buffer(), get_previous_buffer());
}

// TODO: Comment
static void gl_finish() { }

// TODO: Comment
static gboolean on_draw_fallback(GtkWidget *widget, cairo_t *cr, gpointer data) {
	GtkStyleContext *context = gtk_widget_get_style_context(widget);
	guint width = gtk_widget_get_allocated_width(widget);
	guint height = gtk_widget_get_allocated_height(widget);

	guint screen_width = GB_get_screen_width(&gb);
	guint screen_height = GB_get_screen_height(&gb);

	gtk_render_background(context, cr, 0, 0, width, height);

	cairo_surface_t *surface = cairo_image_surface_create_for_data(
		(unsigned char *) get_current_buffer(),
		CAIRO_FORMAT_RGB24,
		screen_width,
		screen_height,
		cairo_format_stride_for_width(CAIRO_FORMAT_RGB24, screen_width)
	);

	cairo_translate(cr, viewport.x, viewport.y);
	cairo_scale(cr, viewport.w / screen_width, viewport.h / screen_height);
	cairo_set_source_surface(cr, surface, 0, 0);
	cairo_pattern_set_filter(cairo_get_source(cr), CAIRO_FILTER_NEAREST);
	cairo_paint(cr);

	return false;
}

// TODO: Comment
static void resize() {
	update_viewport();
}

// Gets called when the VRAM viewer gets realized
static void on_vram_viewer_realize() {
	vram_viewer_visible = true;
	vram_viewer_active_tab = (gchar *)gtk_stack_get_visible_child_name(builder_get(GTK_STACK, "vram_viewer_stack"));
}

// Gets called when the VRAM viewer gets unrealized
static void on_vram_viewer_unrealize() {
	vram_viewer_visible = false;
}

// Gets called when the tileset viewer should be redrawn
static gboolean on_draw_vram_viewer_tileset(GtkWidget *widget, cairo_t *cr, gpointer data) {
	g_mutex_lock(&tileset_buffer_mutex);

	guint width, height;
	GtkStyleContext *context;

	context = gtk_widget_get_style_context(widget);
	width = gtk_widget_get_allocated_width(widget);
	height = gtk_widget_get_allocated_height(widget);

	gtk_render_background(context, cr, 0, 0, width, height);

	cairo_surface_t *surface = cairo_image_surface_create_for_data(
		(unsigned char *) tileset_buffer,
		CAIRO_FORMAT_RGB24,
		256,
		192,
		cairo_format_stride_for_width(CAIRO_FORMAT_RGB24, 256)
	);

	cairo_scale(cr, 2.0, 2.0);
	cairo_set_source_surface(cr, surface, 0, 0);
	cairo_pattern_set_filter(cairo_get_source(cr), CAIRO_FILTER_NEAREST);
	cairo_paint(cr);

	if (gtk_toggle_button_get_active(builder_get(GTK_TOGGLE_BUTTON, "vram_viewer_tileset_toggle_grid_button"))) {
		cairo_set_source_rgba(cr, 0.0, 0.0, 0.0, 0.25);
		cairo_set_line_width(cr, 1);

		const int divisions_x = 256 / 8;
		const int divisions_y = 192 / 8;

		for (int i = 0; i < divisions_x; i++) {
			const int j = 256 * i;

			cairo_move_to(cr, j / divisions_x, 0);
			cairo_line_to(cr, j / divisions_x, 192);
		}

		for (int i = 0; i < divisions_y; i++) {
			const int j = 192 * i;

			cairo_move_to(cr, 0, j / divisions_y);
			cairo_line_to(cr, 256, j / divisions_y);
		}

		cairo_stroke(cr);
	}

	g_mutex_unlock(&tileset_buffer_mutex);
	return false;
}

// Gets called when the tilemap viewer should be redrawn
static gboolean on_draw_vram_viewer_tilemap(GtkWidget *widget, cairo_t *cr, gpointer data) {
	g_mutex_lock(&tilemap_buffer_mutex);

	guint width, height;
	GtkStyleContext *context;

	context = gtk_widget_get_style_context(widget);
	width = gtk_widget_get_allocated_width(widget);
	height = gtk_widget_get_allocated_height(widget);

	gtk_render_background(context, cr, 0, 0, width, height);

	cairo_surface_t *surface = cairo_image_surface_create_for_data(
		(unsigned char *) tilemap_buffer,
		CAIRO_FORMAT_RGB24,
		256,
		256,
		cairo_format_stride_for_width(CAIRO_FORMAT_RGB24, 256)
	);

	cairo_scale(cr, 2.0, 2.0);
	cairo_set_source_surface(cr, surface, 0, 0);
	cairo_pattern_set_filter(cairo_get_source(cr), CAIRO_FILTER_NEAREST);
	cairo_paint(cr);

	if (gtk_toggle_button_get_active(builder_get(GTK_TOGGLE_BUTTON, "vram_viewer_tilemap_toggle_grid_button"))) {
		cairo_set_source_rgba(cr, 0.0, 0.0, 0.0, 0.25);
		cairo_set_line_width(cr, 1);

		const int divisions = 256 / 8;

		for (int i = 0; i < divisions; i++) {
			const int j = 256 * i;

			cairo_move_to(cr, j / divisions, 0);
			cairo_line_to(cr, j / divisions, 256);
			cairo_move_to(cr, 0, j / divisions);
			cairo_line_to(cr, 256, j / divisions);
		}

		cairo_stroke(cr);
	}

	if (gtk_toggle_button_get_active(builder_get(GTK_TOGGLE_BUTTON, "vram_viewer_tilemap_toggle_scrolling_button"))) {
		cairo_rectangle(cr, -2, -2, width + 2, height + 2);

		for (unsigned x = 0; x < 2; x++) {
			for (unsigned y = 0; y < 2; y++) {
				Rect rect = scrollRect;
				rect.x -= 256 * x;
				rect.y += 256 * y;

				cairo_rectangle(cr, rect.x, rect.y, rect.w, rect.h);
			}
		}

		cairo_set_fill_rule(cr, CAIRO_FILL_RULE_EVEN_ODD);
		cairo_set_line_width(cr, 2);
		cairo_set_line_join(cr, CAIRO_LINE_JOIN_ROUND);
		cairo_set_source_rgba(cr, 0.2, 0.2, 0.2, 0.5);
		cairo_fill_preserve(cr);
		cairo_clip_preserve(cr);
		cairo_set_source_rgba(cr, 0.0, 0.0, 0.0, 0.6);
		cairo_stroke(cr);
	}

	g_mutex_unlock(&tilemap_buffer_mutex);
	return false;
}

static gboolean on_motion_vram_viewer_tileset(GtkWidget *widget, GdkEventMotion *event) {
	int x, y;

	if (event->is_hint) {
		gdk_window_get_pointer (event->window, &x, &y, NULL);
	}
	else {
		x = event->x;
		y = event->y;
	}

	// Compensate for our canvas scale
	x /= 2;
	y /= 2;

	uint8_t bank = x >= 128? 1 : 0;
	x &= 127;
	uint16_t tile = x / 8 + y / 8 * 16;

	GtkLabel *status = builder_get(GTK_LABEL, "vram_viewer_status");
	gtk_label_set_text(status, g_strdup_printf("Tile number $%02x at %d:$%04x", tile & 0xFF, bank, 0x8000 + tile * 0x10));

	return true;
}

static gboolean on_motion_vram_viewer_tilemap(GtkWidget *widget, GdkEventMotion *event) {
	int x, y;

	if (event->is_hint) {
		gdk_window_get_pointer (event->window, &x, &y, NULL);
	}
	else {
		x = event->x;
		y = event->y;
	}

	// Compensate for our canvas scale
	x /= 2;
	y /= 2;

	GtkLabel *status = builder_get(GTK_LABEL, "vram_viewer_status");

	uint16_t map_offset = x / 8 + y / 8 * 32;
	uint16_t map_base = 0x1800;

	GB_map_type_t map_type = GB_MAP_AUTO;
	const gchar *map_type_id = gtk_combo_box_get_active_id(builder_get(GTK_COMBO_BOX, "vram_viewer_tilemap_tilemap_selector"));
	if (g_strcmp0("auto", map_type_id) != 0) {
		map_type = (g_strcmp0("9800", map_type_id) == 0)? GB_MAP_9800 : GB_MAP_9C00;
	}

	GB_tileset_type_t tileset_type = GB_TILESET_AUTO;
	const gchar *tileset_type_id = gtk_combo_box_get_active_id(builder_get(GTK_COMBO_BOX, "vram_viewer_tilemap_tileset_selector"));
	if (g_strcmp0("auto", tileset_type_id) != 0) {
		tileset_type = (g_strcmp0("8800", tileset_type_id) == 0)? GB_TILESET_8800 : GB_TILESET_8000;
	}

	uint8_t lcdc = ((uint8_t *)GB_get_direct_access(&gb, GB_DIRECT_ACCESS_IO, NULL, NULL))[GB_IO_LCDC];
	uint8_t *vram = GB_get_direct_access(&gb, GB_DIRECT_ACCESS_VRAM, NULL, NULL);

	if (map_type == GB_MAP_9C00 || (map_type == GB_MAP_AUTO && lcdc & 0x08)) {
		map_base = 0x1c00;
	}

	if (tileset_type == GB_TILESET_AUTO) {
		tileset_type = (lcdc & 0x10)? GB_TILESET_8800 : GB_TILESET_8000;
	}

	uint8_t tile = vram[map_base + map_offset];
	uint16_t tile_address = 0;
	if (tileset_type == GB_TILESET_8000) {
		tile_address = 0x8000 + tile * 0x10;
	}
	else {
		tile_address = 0x9000 + (int8_t)tile * 0x10;
	}

	if (GB_is_cgb(&gb)) {
		uint8_t attributes = vram[map_base + map_offset + 0x2000];
		gtk_label_set_text(status, g_strdup_printf("Tile number $%02x (%d:$%04x) at map address $%04x (Attributes: %c%c%c%d%d)",
			tile,
			attributes & 0x8? 1 : 0,
			tile_address,
			0x8000 + map_base + map_offset,
			(attributes & 0x80) ? 'P' : '-',
			(attributes & 0x40) ? 'V' : '-',
			(attributes & 0x20) ? 'H' : '-',
			attributes & 0x8? 1 : 0,
			attributes & 0x7
		));
	}
	else {
		gtk_label_set_text(status, g_strdup_printf("Tile number $%02x ($%04x) at map address $%04x",
			tile,
			tile_address,
			0x8000 + map_base + map_offset
		));
	}

	return true;
}

static void on_vram_tab_change(GtkWidget *widget, GParamSpec *pspec, GtkStackSwitcher *self) {
	gtk_label_set_text(builder_get(GTK_LABEL, "vram_viewer_status"), "");
	vram_viewer_active_tab = (gchar *)gtk_stack_get_visible_child_name(builder_get(GTK_STACK, "vram_viewer_stack"));
}

G_MODULE_EXPORT void on_boot_rom_location_changed(GtkWidget *w, gpointer user_data_gptr) {
	GtkComboBox *box = GTK_COMBO_BOX(w);
	const gchar *id = gtk_combo_box_get_active_id(box);
	if (id == NULL) return;

	if (g_strcmp0(id, "other") == 0) {
		GtkFileChooserNative *native = gtk_file_chooser_native_new("Select Folder", preferences, GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER, "_Select", "_Cancel");
		gint res = gtk_native_dialog_run(GTK_NATIVE_DIALOG(native));

		if (res == GTK_RESPONSE_ACCEPT) {
			config.boot_rom_path = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(native));
			update_boot_rom_selector(builder);
		}

		g_object_unref(native);
	}
	else {
		config.boot_rom_path = (gchar *)id;
	}
}

G_MODULE_EXPORT void on_cgb_model_changed(GtkWidget *w, gpointer user_data_gptr) {
	GtkComboBox *box = GTK_COMBO_BOX(w);
	config.cgb_revision_name = (gchar *)gtk_combo_box_get_active_id(box);
}

G_MODULE_EXPORT void on_color_correction_changed(GtkWidget *w, gpointer user_data_gptr) {
	GtkComboBox *box = GTK_COMBO_BOX(w);
	config.color_correction_id = (gchar *)gtk_combo_box_get_active_id(box);

	if (GB_is_inited(&gb)) {
		GB_set_color_correction_mode(&gb, get_color_correction_mode());
	}
}

G_MODULE_EXPORT void on_frame_blending_changed(GtkWidget *w, gpointer user_data_gptr) {
	GtkComboBox *box = GTK_COMBO_BOX(w);
	config.frame_blending_mode = (gchar *)gtk_combo_box_get_active_id(box);
}

G_MODULE_EXPORT void on_display_border_changed(GtkWidget *w, gpointer user_data_gptr) {
	GtkComboBox *box = GTK_COMBO_BOX(w);
	config.display_border_mode = (gchar *)gtk_combo_box_get_active_id(box);

	border_mode_changed = true;
}

G_MODULE_EXPORT void on_monochrome_palette_changed(GtkWidget *w, gpointer user_data_gptr) {
	GtkComboBox *box = GTK_COMBO_BOX(w);
	config.monochrome_palette_id = (gchar *)gtk_combo_box_get_active_id(box);

	GB_set_palette(&gb, get_monochrome_palette());
}

G_MODULE_EXPORT void on_color_menubar_override_changed(GtkWidget *w, gpointer user_data_gptr) {
	config.menubar_override = (gchar *)gtk_combo_box_get_active_id(GTK_COMBO_BOX(w));
}

G_MODULE_EXPORT void on_dmg_model_changed(GtkWidget *w, gpointer user_data_gptr) {
	GtkComboBox *box = GTK_COMBO_BOX(w);
	config.dmg_revision_name = (gchar *)gtk_combo_box_get_active_id(box);
}

G_MODULE_EXPORT void on_graphic_filter_changed(GtkWidget *w, gpointer user_data_gptr) {
	GtkComboBox *box = GTK_COMBO_BOX(w);
	config.shader = (gchar *)gtk_combo_box_get_active_id(box);

	free_shader(&shader);
	init_shader_with_name(&shader, config.shader);
}

G_MODULE_EXPORT void on_highpass_filter_changed(GtkWidget *w, gpointer user_data_gptr) {
	config.high_pass_filter_id = (gchar *)gtk_combo_box_get_active_id(GTK_COMBO_BOX(w));

	if (GB_is_inited(&gb)) {
		GB_set_highpass_filter_mode(&gb, get_highpass_mode());
	}
}

G_MODULE_EXPORT void on_keep_aspect_ratio_changed(GtkWidget *w, gpointer user_data_gptr) {
	GtkCheckButton *button = GTK_CHECK_BUTTON(w);
	gboolean value = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button));
	config.keep_aspect_ratio = value;
	update_viewport();
}

G_MODULE_EXPORT void on_rewind_duration_changed(GtkWidget *w, gpointer user_data_gptr) {
	GtkComboBox *box = GTK_COMBO_BOX(w);
	config.rewind_duration = g_ascii_strtoll(gtk_combo_box_get_active_id(box), NULL, 10);
	GB_set_rewind_length(&gb, config.rewind_duration);
}

G_MODULE_EXPORT void on_sample_rate_changed(GtkWidget *w, gpointer user_data_gptr) {
	GtkComboBox *box = GTK_COMBO_BOX(w);
	config.sample_rate = g_ascii_strtoll(gtk_combo_box_get_active_id(box), NULL, 10);

	if (config.sample_rate == -1) {
		gui_data.sample_rate = DEFAULT_AUDIO_SAMPLE_RATE;
	}
	else {
		gui_data.sample_rate = config.sample_rate;
	}

	init_audio();
}

G_MODULE_EXPORT void on_sgb_model_changed(GtkWidget *w, gpointer user_data_gptr) {
	GtkComboBox *box = GTK_COMBO_BOX(w);
	config.sgb_revision_name = (gchar *)gtk_combo_box_get_active_id(box);
}

G_MODULE_EXPORT void on_use_integer_scaling_changed(GtkWidget *w, gpointer user_data_gptr) {
	GtkCheckButton *button = GTK_CHECK_BUTTON(w);
	gboolean value = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button));
	config.use_integer_scaling = value;
	update_viewport();
}

G_MODULE_EXPORT void console_on_enter(GtkWidget *w, gpointer user_data_gptr) {
	GtkEntry *input = GTK_ENTRY(w);
	const gchar *_text = gtk_entry_get_text(input);
	gchar *text = g_strdup(_text);

	if (g_strcmp0("", text) == 0 && g_strcmp0("", last_console_input) < 0) {
		text = g_strdup(last_console_input);
	}
	else if (text) {
		if (last_console_input != NULL) g_free(last_console_input);
		last_console_input = g_strdup(text);
	}

	if (!in_sync_input) {
		console_log(&gb, "> ", 0);
	}

	console_log(&gb, text, 0);
	console_log(&gb, "\n", 0);

	g_mutex_lock(&debugger_input_mutex);
	g_ptr_array_add(debugger_input_queue, (gpointer)text);
	g_cond_signal(&debugger_input_cond);
	g_mutex_unlock(&debugger_input_mutex);

	// clear input
	gtk_entry_set_text(input, "");
}

static uint32_t rgb_encode(GB_gameboy_t *gb, uint8_t r, uint8_t g, uint8_t b) {
	return 0xFF000000 | (r << 16) | (g << 8) | b;
}

static void render_texture(void *pixels,  void *previous) {
	static void *_pixels = NULL;
	if (pixels) {
		_pixels = pixels;
	}

	glClearColor(0, 0, 0, 1);
	glClear(GL_COLOR_BUFFER_BIT);

	GB_frame_blending_mode_t mode = get_frame_blending_mode();
	if (!previous) {
		mode = GB_FRAME_BLENDING_MODE_DISABLED;
	}
	else if (mode == GB_FRAME_BLENDING_MODE_ACCURATE) {
		if (GB_is_sgb(&gb)) {
			mode = GB_FRAME_BLENDING_MODE_SIMPLE;
		}
		else {
			mode = GB_is_odd_frame(&gb)? GB_FRAME_BLENDING_MODE_ACCURATE_ODD : GB_FRAME_BLENDING_MODE_ACCURATE_EVEN;
		}
	}

	render_bitmap_with_shader(
		&shader, _pixels, previous,
		GB_get_screen_width(&gb), GB_get_screen_height(&gb),
		viewport.x, viewport.y, viewport.w, viewport.h,
		mode
	);
}

static void update_viewport(void) {
	GtkWidget *w = fallback_canvas ? GTK_WIDGET(fallback_canvas) : GTK_WIDGET(gl_area);

	int win_width  = gtk_widget_get_allocated_width(w);
	int win_height = gtk_widget_get_allocated_height(w);

	double x_factor = win_width  / (double) GB_get_screen_width(&gb);
	double y_factor = win_height / (double) GB_get_screen_height(&gb);

	if (config.use_integer_scaling) {
		x_factor = (int)(x_factor);
		y_factor = (int)(y_factor);
	}

	if (config.keep_aspect_ratio) {
		if (x_factor > y_factor) {
			x_factor = y_factor;
		}
		else {
			y_factor = x_factor;
		}
	}

	unsigned new_width  = x_factor * GB_get_screen_width(&gb);
	unsigned new_height = y_factor * GB_get_screen_height(&gb);

	viewport = (Rect){
		(win_width - new_width) / 2,
		(win_height - new_height) / 2,
		new_width,
		new_height
	};

	if (!fallback_canvas) glViewport(viewport.x, viewport.y, viewport.w, viewport.h);
}

static void update_window_geometry() {
	// Set size hints
	GdkGeometry hints;
	hints.min_width  = GB_get_screen_width(&gb);
	hints.min_height = GB_get_screen_height(&gb);

	gtk_window_set_geometry_hints(
		GTK_WINDOW(main_window),
		NULL,
		&hints,
		(GdkWindowHints)(GDK_HINT_MIN_SIZE)
	);

	gtk_window_resize(GTK_WINDOW(main_window),
		GB_get_screen_width(&gb)  * 2,
		GB_get_screen_height(&gb) * 2
	);

	// Setup our image buffers
	if (image_buffers[0]) g_free(image_buffers[0]);
	if (image_buffers[1]) g_free(image_buffers[1]);
	if (image_buffers[2]) g_free(image_buffers[2]);

	size_t buffer_size = sizeof(image_buffers[0][0]) * GB_get_screen_width(&gb) * GB_get_screen_height(&gb);

	image_buffers[0] = g_malloc0(buffer_size);
	image_buffers[1] = g_malloc0(buffer_size);
	image_buffers[2] = g_malloc0(buffer_size);

	if (GB_is_inited(&gb)) {
		GB_set_pixels_output(&gb, get_pixels());
	}
}

static void handle_events(GB_gameboy_t *gb) {
	SDL_GameControllerUpdate();

	uint8_t controller_state = 0;

	if (controller) {
		int16_t x_axis = SDL_GameControllerGetAxis(controller, SDL_CONTROLLER_AXIS_LEFTX);
		int16_t y_axis = SDL_GameControllerGetAxis(controller, SDL_CONTROLLER_AXIS_LEFTY);

		if (x_axis >= JOYSTICK_HIGH) {
			controller_state |= BUTTON_MASK_RIGHT;
		}
		else if (x_axis <= -JOYSTICK_HIGH) {
			controller_state |= BUTTON_MASK_LEFT;
		}

		if (y_axis >= JOYSTICK_HIGH) {
			controller_state |= BUTTON_MASK_DOWN;
		}
		else if (y_axis <= -JOYSTICK_HIGH) {
			controller_state |= BUTTON_MASK_UP;
		}

		if (SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_DPAD_RIGHT)) controller_state |= BUTTON_MASK_RIGHT;
		if (SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_DPAD_LEFT))  controller_state |= BUTTON_MASK_LEFT;
		if (SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_DPAD_UP))    controller_state |= BUTTON_MASK_UP;
		if (SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_DPAD_DOWN))  controller_state |= BUTTON_MASK_DOWN;
		if (SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_A))          controller_state |= BUTTON_MASK_A;
		if (SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_B))          controller_state |= BUTTON_MASK_B;
		if (SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_BACK))       controller_state |= BUTTON_MASK_SELECT;
		if (SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_START))      controller_state |= BUTTON_MASK_START;
	}

	GB_set_key_state(gb, GB_KEY_RIGHT,  (pressed_buttons & BUTTON_MASK_RIGHT)  | (controller_state & BUTTON_MASK_RIGHT));
	GB_set_key_state(gb, GB_KEY_LEFT,   (pressed_buttons & BUTTON_MASK_LEFT)   | (controller_state & BUTTON_MASK_LEFT));
	GB_set_key_state(gb, GB_KEY_UP,     (pressed_buttons & BUTTON_MASK_UP)     | (controller_state & BUTTON_MASK_UP));
	GB_set_key_state(gb, GB_KEY_DOWN,   (pressed_buttons & BUTTON_MASK_DOWN)   | (controller_state & BUTTON_MASK_DOWN));
	GB_set_key_state(gb, GB_KEY_A,      (pressed_buttons & BUTTON_MASK_A)      | (controller_state & BUTTON_MASK_A));
	GB_set_key_state(gb, GB_KEY_B,      (pressed_buttons & BUTTON_MASK_B)      | (controller_state & BUTTON_MASK_B));
	GB_set_key_state(gb, GB_KEY_SELECT, (pressed_buttons & BUTTON_MASK_SELECT) | (controller_state & BUTTON_MASK_SELECT));
	GB_set_key_state(gb, GB_KEY_START,  (pressed_buttons & BUTTON_MASK_START)  | (controller_state & BUTTON_MASK_START));
}

static uint32_t convert_color(uint16_t color) {
	const uint8_t r = ((uint16_t)(color & 0x1F) * 255) / 31;
	const uint8_t g = ((uint16_t)((color >> 5) & 0x1F) * 255) / 31;
	const uint8_t b = ((uint16_t)((color >> 10) & 0x1F) * 255) / 31;

	return (r << 16) | (g << 8) | b;
}

static void palette_color_data_func(GtkTreeViewColumn *col, GtkCellRenderer *renderer, GtkTreeModel *model, GtkTreeIter *iter, gpointer user_data) {
	const gchar *title = gtk_tree_view_column_get_title(col);
	const uint8_t color_index = g_ascii_strtoll(&title[6], NULL, 10);
	const uint8_t column_index = 2 + (2 * color_index);

	GValue color_val = G_VALUE_INIT;
	gtk_tree_model_get_value(model, iter, column_index, &color_val);
	gint color = g_value_get_int(&color_val);
	gchar *color_string = g_strdup_printf("#%06x", color);

	gint lightness = 0.299 * ((color >> 16) & 0xFF) + 0.587 * ((color >> 8) & 0xFF) + 0.114 * (color & 0xFF);

	GValue color_str = G_VALUE_INIT;
	g_value_init(&color_str, G_TYPE_STRING);
	g_value_set_string(&color_str, color_string);
	g_object_set_property(G_OBJECT(renderer), "background", &color_str);

	GValue fg_color_str = G_VALUE_INIT;
	g_value_init(&fg_color_str, G_TYPE_STRING);
	g_value_set_static_string(&fg_color_str, (lightness > 0x7F)? "#000000" : "#FFFFFF");
	g_object_set_property(G_OBJECT(renderer), "foreground", &fg_color_str);

	g_value_unset(&color_val);
	g_value_unset(&color_str);
	g_value_unset(&fg_color_str);
	g_free(color_string);
}

static void on_vblank(gpointer data) {
	if (!vram_viewer_updating && vram_viewer_visible) {
		vram_viewer_updating = true;

		if (g_strcmp0("vram_viewer_sprites", vram_viewer_active_tab) == 0) {
			GtkTreeIter iter;
			GtkTreeView *tree_view = builder_get(GTK_TREE_VIEW, "vram_viewer_sprites");
			// gtk_tree_view_set_model(tree_view, NULL); // Do we need this?

			GtkListStore *store = gtk_list_store_new(7,
				GDK_TYPE_PIXBUF, // Preview image
				G_TYPE_STRING,   // X position
				G_TYPE_STRING,   // Y position
				G_TYPE_STRING,   // Tile
				G_TYPE_STRING,   // Tile Address
				G_TYPE_STRING,   // OAM Address
				G_TYPE_STRING    // Attributes
			);

			gtk_tree_model_get_iter_first(GTK_TREE_MODEL(store), &iter);

			for (unsigned row = 0; row < oamCount; ++row) {
				GdkPixbuf *pixbuf = gdk_pixbuf_new_from_bytes(
					g_bytes_new(oamInfo[row].image, 128 * sizeof(uint32_t)),
					GDK_COLORSPACE_RGB, true, 8, 8, oamHeight, 8 * sizeof(uint32_t)
				);

				GdkPixbuf *dest = gdk_pixbuf_new(GDK_COLORSPACE_RGB, true, 8, 8 * 2, oamHeight * 2);

				gdk_pixbuf_scale(pixbuf, dest,
					0, 0, 8 * 2, oamHeight * 2,
					0, 0, 2.0, 2.0,
					GDK_INTERP_NEAREST
				);

				gtk_list_store_insert_with_values(store, &iter, -1,
					0, dest,
					1, itoa(oamInfo[row].x - 8),
					2, itoa(oamInfo[row].y - 16),
					3, g_strdup_printf("$%02x", oamInfo[row].tile),
					4, g_strdup_printf("$%04x", 0x8000 + oamInfo[row].tile * 0x10),
					5, g_strdup_printf("$%04x", oamInfo[row].oam_addr),
					6, vram_viewer_is_cgb
						? g_strdup_printf("%c%c%c%d%d",
							oamInfo[row].flags & 0x80? 'P' : '-',
							oamInfo[row].flags & 0x40? 'Y' : '-',
							oamInfo[row].flags & 0x20? 'X' : '-',
							oamInfo[row].flags & 0x08? 1 : 0,
							oamInfo[row].flags & 0x07)
						: g_strdup_printf("%c%c%c%d",
							oamInfo[row].flags & 0x80? 'P' : '-',
							oamInfo[row].flags & 0x40? 'Y' : '-',
							oamInfo[row].flags & 0x20? 'X' : '-',
							oamInfo[row].flags & 0x10? 1 : 0),
					-1
				);

				g_object_unref(pixbuf);
				g_object_unref(dest);
			}

			gtk_tree_view_set_model(tree_view, GTK_TREE_MODEL(store));
			g_object_unref(store);
		}
		else if (g_strcmp0("vram_viewer_palettes", vram_viewer_active_tab) == 0) {
			GtkTreeIter iter;
			GtkTreeView *tree_view = builder_get(GTK_TREE_VIEW, "vram_viewer_palettes");
			// gtk_tree_view_set_model(tree_view, NULL); // Do we need this?

			GtkListStore *store = gtk_list_store_new(9,
				G_TYPE_STRING, // Name

				G_TYPE_STRING, // Color 0 string
				G_TYPE_INT,    // Color 0 integer

				G_TYPE_STRING, // Color 1 string
				G_TYPE_INT,    // Color 1 integer

				G_TYPE_STRING, // Color 2 string
				G_TYPE_INT,    // Color 2 integer

				G_TYPE_STRING, // Color 3 string
				G_TYPE_INT     // Color 3 integer
			);

			gtk_tree_model_get_iter_first(GTK_TREE_MODEL(store), &iter);

			for (unsigned row = 0; row < 16; ++row) {
				uint8_t offset = (row & 7) * 4;

				uint16_t color_0 = (vram_viewer_palette_data[row][((0 + offset) << 1) + 1] << 8) | vram_viewer_palette_data[row][((0 + offset) << 1)];
				uint16_t color_1 = (vram_viewer_palette_data[row][((1 + offset) << 1) + 1] << 8) | vram_viewer_palette_data[row][((1 + offset) << 1)];
				uint16_t color_2 = (vram_viewer_palette_data[row][((2 + offset) << 1) + 1] << 8) | vram_viewer_palette_data[row][((2 + offset) << 1)];
				uint16_t color_3 = (vram_viewer_palette_data[row][((3 + offset) << 1) + 1] << 8) | vram_viewer_palette_data[row][((3 + offset) << 1)];

				gtk_list_store_insert_with_values(store, &iter, -1,
					0, g_strdup_printf("%s %d", row >=8 ? "Object" : "Background", row & 7),
					1, g_strdup_printf("$%04x", color_0 & 0x7FFF),
					2, convert_color(color_0),
					3, g_strdup_printf("$%04x", color_1 & 0x7FFF),
					4, convert_color(color_1),
					5, g_strdup_printf("$%04x", color_2 & 0x7FFF),
					6, convert_color(color_2),
					7, g_strdup_printf("$%04x", color_3 & 0x7FFF),
					8, convert_color(color_3),
					-1
				);
			}

			GtkTreeViewColumn *column_0 = gtk_tree_view_get_column(tree_view, 1);
			GtkTreeViewColumn *column_1 = gtk_tree_view_get_column(tree_view, 2);
			GtkTreeViewColumn *column_2 = gtk_tree_view_get_column(tree_view, 3);
			GtkTreeViewColumn *column_3 = gtk_tree_view_get_column(tree_view, 4);

			GtkCellRendererText *cell_renderer_0 = builder_get(GTK_CELL_RENDERER_TEXT, "vram_viewer_palette_cell_renderer_0");
			GtkCellRendererText *cell_renderer_1 = builder_get(GTK_CELL_RENDERER_TEXT, "vram_viewer_palette_cell_renderer_1");
			GtkCellRendererText *cell_renderer_2 = builder_get(GTK_CELL_RENDERER_TEXT, "vram_viewer_palette_cell_renderer_2");
			GtkCellRendererText *cell_renderer_3 = builder_get(GTK_CELL_RENDERER_TEXT, "vram_viewer_palette_cell_renderer_3");

			gtk_tree_view_column_set_cell_data_func(column_0, GTK_CELL_RENDERER(cell_renderer_0), palette_color_data_func, NULL, NULL);
			gtk_tree_view_column_set_cell_data_func(column_1, GTK_CELL_RENDERER(cell_renderer_1), palette_color_data_func, NULL, NULL);
			gtk_tree_view_column_set_cell_data_func(column_2, GTK_CELL_RENDERER(cell_renderer_2), palette_color_data_func, NULL, NULL);
			gtk_tree_view_column_set_cell_data_func(column_3, GTK_CELL_RENDERER(cell_renderer_3), palette_color_data_func, NULL, NULL);

			gtk_tree_view_set_model(tree_view, GTK_TREE_MODEL(store));
			g_object_unref(store);
		}

		// Queue a redraw of the VRAM viewer
		gtk_widget_queue_draw(GTK_WIDGET(vram_viewer));

		vram_viewer_updating = false;
	}

	// Queue drawing of the current frame
	if (fallback_canvas) {
		gtk_widget_queue_draw(GTK_WIDGET(main_window));
	}
	else if (gl_area) {
		gtk_gl_area_queue_render(gl_area);
	}
}

static void vblank(GB_gameboy_t *gb) {
	flip();

	if (border_mode_changed) {
		GB_set_border_mode(gb, get_display_border_mode());
		update_window_geometry();

		border_mode_changed = false;
	}

	GB_set_pixels_output(gb, get_pixels());

	if (underclock_down && clock_mutliplier > 0.5) {
		clock_mutliplier -= 1.0/16;
		GB_set_clock_multiplier(gb, clock_mutliplier);
	}
	else if (!underclock_down && clock_mutliplier < 1.0) {
		clock_mutliplier += 1.0/16;
		GB_set_clock_multiplier(gb, clock_mutliplier);
	}

	if (g_strcmp0("vram_viewer_tileset", vram_viewer_active_tab) == 0) {
		const gchar *palette_id = gtk_combo_box_get_active_id(builder_get(GTK_COMBO_BOX, "vram_viewer_tileset_palette_selector"));

		GB_palette_type_t palette_type = g_str_has_prefix(palette_id, "bg")? GB_PALETTE_BACKGROUND : GB_PALETTE_OAM;
		uint8_t palette_index = g_ascii_digit_value(palette_id[palette_type == GB_PALETTE_OAM ? 3 : 2]);

		GB_draw_tileset(gb, tileset_buffer,
			palette_type,
			palette_index
		);
	}
	else if (g_strcmp0("vram_viewer_tilemap", vram_viewer_active_tab) == 0) {
		const gchar *palette_id = gtk_combo_box_get_active_id(builder_get(GTK_COMBO_BOX, "vram_viewer_tilemap_palette_selector"));
		uint8_t palette_index = 0;
		GB_palette_type_t palette_type = GB_PALETTE_AUTO;

		if (g_strcmp0("auto", palette_id) != 0) {
			palette_type = g_str_has_prefix(palette_id, "bg")? GB_PALETTE_BACKGROUND : GB_PALETTE_OAM;
			palette_index = g_ascii_digit_value(palette_id[palette_type == GB_PALETTE_OAM ? 3 : 2]);
		}

		GB_map_type_t map_type = GB_MAP_AUTO;
		const gchar *map_type_id = gtk_combo_box_get_active_id(builder_get(GTK_COMBO_BOX, "vram_viewer_tilemap_tilemap_selector"));
		if (g_strcmp0("auto", map_type_id) != 0) {
			map_type = (g_strcmp0("9800", map_type_id) == 0)? GB_MAP_9800 : GB_MAP_9C00;
		}

		GB_tileset_type_t tileset_type = GB_TILESET_AUTO;
		const gchar *tileset_type_id = gtk_combo_box_get_active_id(builder_get(GTK_COMBO_BOX, "vram_viewer_tilemap_tileset_selector"));
		if (g_strcmp0("auto", tileset_type_id) != 0) {
			tileset_type = (g_strcmp0("8800", tileset_type_id) == 0)? GB_TILESET_8800 : GB_TILESET_8000;
		}

		GB_draw_tilemap(gb, tilemap_buffer,
			palette_type,
			palette_index,
			map_type,
			tileset_type
		);

		scrollRect = (Rect){
			GB_read_memory(gb, 0xFF00 | GB_IO_SCX),
			GB_read_memory(gb, 0xFF00 | GB_IO_SCY),
			160, 144
		};
	}
	else if (g_strcmp0("vram_viewer_sprites", vram_viewer_active_tab) == 0) {
		oamCount = GB_get_oam_info(gb, oamInfo, &oamHeight);
		vram_viewer_is_cgb = GB_is_cgb(gb);
	}
	else if (g_strcmp0("vram_viewer_palettes", vram_viewer_active_tab) == 0) {
		size_t size;

		for (unsigned row = 0; row < 16; ++row) {
			uint8_t *palette_data = GB_get_direct_access(gb, row >= 8? GB_DIRECT_ACCESS_OBP : GB_DIRECT_ACCESS_BGP, &size, NULL);
			memcpy(vram_viewer_palette_data[row], palette_data, size);
		}
	}

	do_rewind = rewind_down;

	g_idle_add((GSourceFunc) on_vblank, NULL);
}

static void run(GuiData *gui_data) {
	if (running) return;
	while (stopping);

	g_thread_new("CoreLoop", run_thread, gui_data);
}

static gpointer run_thread(gpointer gui_data_gptr) {
	GuiData *gui_data = gui_data_gptr;

	if (!gui_data->file) return NULL;

	if (gui_data->stopped) {
		start(gui_data);
	}
	else {
		init(gui_data);
		reset(gui_data);
		start(gui_data);
	}

	return NULL;
}

static void init(GuiData *gui_data) {
	if (GB_is_inited(&gb)) return;

	GB_init(&gb, get_model());

	GB_set_vblank_callback(&gb, vblank);
	GB_set_pixels_output(&gb, get_current_buffer());
	GB_set_rgb_encode_callback(&gb, rgb_encode);
	GB_set_sample_rate(&gb, gui_data->sample_rate);
	GB_set_color_correction_mode(&gb, get_color_correction_mode());
	GB_set_highpass_filter_mode(&gb, get_highpass_mode());
	GB_set_rewind_length(&gb, config.rewind_duration);
	GB_set_update_input_hint_callback(&gb, handle_events);
	GB_apu_set_sample_callback(&gb, gb_audio_callback);
	GB_set_input_callback(&gb, sync_console_input);
	GB_set_async_input_callback(&gb, async_console_input);
	GB_set_log_callback(&gb, console_log);
	GB_set_boot_rom_load_callback(&gb, load_boot_rom);

	if (get_display_border_mode() <= GB_BORDER_ALWAYS) {
		GB_set_border_mode(&gb, get_display_border_mode());
	}

	update_window_geometry();
}

static void load_boot_rom(GB_gameboy_t *gb, GB_boot_rom_t type) {
	GError *error = NULL;
	char *boot_rom_path = NULL;
	GBytes *boot_rom_f = NULL;
	const guchar *boot_rom_data;
	gsize boot_rom_size;

	static const char *const names[] = {
		[GB_BOOT_ROM_DMG0] = "dmg0_boot.bin",
		[GB_BOOT_ROM_DMG]  = "dmg_boot.bin",
		[GB_BOOT_ROM_MGB]  = "mgb_boot.bin",
		[GB_BOOT_ROM_SGB]  = "sgb_boot.bin",
		[GB_BOOT_ROM_SGB2] = "sgb2_boot.bin",
		[GB_BOOT_ROM_CGB0] = "cgb0_boot.bin",
		[GB_BOOT_ROM_CGB]  = "cgb_boot.bin",
		[GB_BOOT_ROM_AGB]  = "agb_boot.bin",
	};

	const char *const boot_rom_name = names[type];

	if (gui_data.cli_options.boot_rom_path != NULL) {
		g_message("[CLI override] Trying to load boot ROM from %s", gui_data.cli_options.boot_rom_path);
		if (GB_load_boot_rom(gb, gui_data.cli_options.boot_rom_path)) {
			g_warning("Falling back to boot ROM from config");
			goto config_boot_rom;
		}
	}
	else { config_boot_rom:
		if (config.boot_rom_path != NULL && g_strcmp0(config.boot_rom_path, "other") != 0 && g_strcmp0(config.boot_rom_path, "auto") != 0) {
			boot_rom_path = g_build_filename(config.boot_rom_path, boot_rom_name, NULL);
			g_message("Trying to load boot ROM from %s", boot_rom_path);

			if (GB_load_boot_rom(gb, boot_rom_path)) {
				g_free(boot_rom_path);
				g_warning("Falling back to internal boot ROM");
				goto internal_boot_rom;
			}

			g_free(boot_rom_path);
		}
		else { internal_boot_rom:
			boot_rom_path = g_build_filename(RESOURCE_PREFIX "bootroms/", boot_rom_name, NULL);
			boot_rom_f = g_resources_lookup_data(boot_rom_path, G_RESOURCE_LOOKUP_FLAGS_NONE, &error);

			g_message("Loading internal boot ROM: %s", boot_rom_path);
			g_free(boot_rom_path);

			if (boot_rom_f == NULL) {
				g_warning("Failed to load internal boot ROM: %s", boot_rom_path);
				g_error_free(error);
				exit(EXIT_FAILURE);
			}

			boot_rom_data = g_bytes_get_data(boot_rom_f, &boot_rom_size);
			GB_load_boot_rom_from_buffer(gb, boot_rom_data, boot_rom_size);

			g_bytes_unref(boot_rom_f);
		}
	}
}

static void stop(GuiData *gui_data) {
	if (!running) return;

	SDL_PauseAudioDevice(device_id, 1);
	GB_debugger_set_disabled(&gb, true);

	if (GB_debugger_is_stopped(&gb)) {
		// [self interruptDebugInputRead];
	}

	stopping = true;
	running = false;
	while (stopping);

	GB_debugger_set_disabled(&gb, false);
	gui_data->stopped = true;
}

static void reset(GuiData *gui_data) {
	g_debug("Reset: %d == %d", get_model(), gui_data->prev_model);
	GB_model_t current_model = get_model();

	if (gui_data->prev_model == -1 || gui_data->prev_model == current_model) {
		GB_reset(&gb);
	}
	else {
		GB_switch_model_and_reset(&gb, current_model);
	}

	GB_set_palette(&gb, get_monochrome_palette());

	gui_data->prev_model = get_model();

	GtkRequisition minimum_size;
	GtkRequisition natural_size;
	gtk_widget_get_preferred_size(GTK_WIDGET(main_window), &minimum_size, &natural_size);

	// Check SGB -> non-SGB and non-SGB to SGB transitions
	if (GB_get_screen_width(&gb) != minimum_size.width || GB_get_screen_height(&gb) != minimum_size.height) {
		update_window_geometry();
	}

	bool success = false;
	if (gui_data->file) {
		char *path = g_file_get_path(gui_data->file);

		if (GB_load_rom(&gb, path) == 0) {
			success = true;
		}
		else {
			g_warning("Failed to load ROM: %s", path);
		}

		g_free(path);
	}

	action_set_enabled(main_application, "close", success);
	action_entries_set_enabled(emulation_entries, G_N_ELEMENTS(emulation_entries), success);
}

static void start(GuiData *gui_data) {
	running = true;
	gui_data->stopped = false;

	SDL_ClearQueuedAudio(device_id);
	SDL_PauseAudioDevice(device_id, 0);

	/* Run emulation */
	while (running) {
		if (rewind_paused) {
			handle_events(&gb);
			g_usleep(G_USEC_PER_SEC / 8);
		}
		else {
			if (do_rewind) {
				GB_rewind_pop(&gb);
				if (turbo_down) {
					GB_rewind_pop(&gb);
				}
				if (!GB_rewind_pop(&gb)) {
					rewind_paused = true;
				}
				do_rewind = false;
			}
			GB_run(&gb);
		}
	}

	stopping = false;
}
