#include <gtk/gtk.h>
#include <epoxy/gl.h>
#include <signal.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include <Core/gb.h>

#include "macros.h"
#include "settings.h"
#include "shader.h"

typedef struct UserData {
	bool fullscreen;
	GFile *file;
	gchar *boot_rom_path;
	gchar *config_path;
	GB_model_t model;
} UserData;

typedef struct{
	int16_t x, y;
	uint16_t w, h;
} Rect;

static void run(GApplication *app, UserData *user_data);

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

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

static shader_t shader;

static GB_gameboy_t gb;
static uint32_t *image_buffers[3];
static unsigned char current_buffer;

static bool paused = false;
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 rect;
static bool vram_viewer_visible = false;
static bool running = true;

static const size_t tileset_buffer_length = 256 * 192 * 4;
static uint32_t tileset_buffer[tileset_buffer_length] = {0};

static const size_t tilemap_buffer_length = 256 * 256 * 4;
static uint32_t tilemap_buffer[tilemap_buffer_length] = {0};

static unsigned char number_of_buffers(void) {
	bool should_blend = true;

	return should_blend? 3 : 2;
}

static void flip(void) {
	current_buffer = (current_buffer + 1) % number_of_buffers();
}

static uint32_t *get_pixels(void) {
	return image_buffers[(current_buffer + 1) % number_of_buffers()];
}

static uint32_t *get_current_buffer(void) {
	return image_buffers[current_buffer];
}

static uint32_t *get_previous_buffer(void) {
	return image_buffers[(current_buffer + 2) % number_of_buffers()];
}

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);
	
	render_bitmap_with_shader(&shader, _pixels, previous, GB_get_screen_width(&gb), GB_get_screen_height(&gb), rect.x, rect.y, rect.w, rect.h);
}

static void vblank(GB_gameboy_t *gb) {
	flip();
	GB_set_pixels_output(gb, get_pixels());

	// Queue drawing of the current frame
	gtk_gl_area_queue_render(gl_area);
	
	if (vram_viewer_visible) {
		// TODO: Only update what is needed
		GB_draw_tileset(gb, tileset_buffer, GB_PALETTE_NONE, 0);
		GB_draw_tilemap(gb, tilemap_buffer, GB_PALETTE_AUTO, 0, GB_MAP_AUTO, GB_TILESET_AUTO);

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

	while (gtk_events_pending()) {
		gtk_main_iteration();
	}
}

static void update_viewport(void) {
	int win_width  = gtk_widget_get_allocated_width(GTK_WIDGET(gl_area));
	int win_height = gtk_widget_get_allocated_height(GTK_WIDGET(gl_area));

	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);

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

	glViewport(rect.x, rect.y, rect.w, rect.h);
}

// 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;

	return result;
}

// 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 *list = gtk_container_get_children(container);

	while (list) {
		if (GTK_IS_COMBO_BOX(list->data)) {
			gtk_combo_box_set_row_separator_func(GTK_COMBO_BOX(list->data), is_separator, NULL, NULL);
		}

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

		list = list->next;
	}

	g_list_free_full(list, NULL);
}

// Returns true if the application should show a menubar
static gboolean show_menubar(void) {
	switch (get_show_menubar()) {
		case MENUBAR_AUTO: {
			GtkSettings *settings = gtk_settings_get_default();
			gboolean result;

			g_object_get(settings, "gtk-shell-shows-menubar", &result, NULL);

			return result;
		}
		case MENUBAR_SHOW: return true;
		case MENUBAR_HIDE: return false;
	}
}

// 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;
}

static void quit(GApplication *app) {
	// Tell our own main loop to quit.
	// This will allow our run() and therefore our activate() methods to end.
	running = false;
}

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

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

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

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

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

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

// List of GActions for the `app` prefix
static GActionEntry app_entries[] = {
	{ "quit", activate_quit, NULL, NULL, NULL },
	{ "about", activate_about, NULL, NULL, NULL },
	{ "open_gtk_debugger", activate_open_gtk_debugger, NULL, NULL, NULL },
	{ "preferences", activate_preferences, NULL, NULL, NULL },
	{ "open_vram_viewer", activate_open_vram_viewer, NULL, NULL, NULL },
	{ "open_memory_viewer", activate_open_memory_viewer, NULL, NULL, NULL },
};

G_MODULE_EXPORT void on_quit(GtkWidget *w, gpointer app) {
	quit(G_APPLICATION(app));
}

G_MODULE_EXPORT void on_show_window(GtkWidget *w, gpointer window) {
	gtk_widget_show_all(GTK_WIDGET(window));
}

G_MODULE_EXPORT void gl_init() {
	const char *renderer;

	gtk_gl_area_make_current(gl_area);
	if (gtk_gl_area_get_error(gl_area) != NULL) {
		return;
	}

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

	if (config.shader == NULL || !init_shader_with_name(&shader, config.shader)) {
		init_shader_with_name(&shader, "NearestNeighbor");
	}
}

G_MODULE_EXPORT void gl_resize() {
	update_viewport();
}

G_MODULE_EXPORT void gl_draw() {
	render_texture(get_current_buffer(), get_previous_buffer());
}

G_MODULE_EXPORT void gl_finish() { }

G_MODULE_EXPORT void on_vram_viewer_realize() {
	vram_viewer_visible = true;
}

G_MODULE_EXPORT void on_vram_viewer_unrealize() {
	vram_viewer_visible = false;
}

G_MODULE_EXPORT gboolean on_draw_vram_viewer_tileset(GtkWidget *widget, cairo_t *cr, gpointer data) {
	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_set_source_surface(cr, surface, 0, 0);
	cairo_paint(cr);

	return FALSE;
}

G_MODULE_EXPORT gboolean on_vram_viewer_tilemap(GtkWidget *widget, cairo_t *cr, gpointer data) {
	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_set_source_surface(cr, surface, 0, 0);
	cairo_paint(cr);

	return FALSE;
}

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_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 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_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_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);

	init_shader_with_name(&shader, config.shader);
}

G_MODULE_EXPORT void on_cgb_model_changed(GtkWidget *w, gpointer user_data_gptr) {
	GtkComboBox *box = GTK_COMBO_BOX(w);
	g_print("New value: %s\n", gtk_combo_box_get_active_id(box));
}

G_MODULE_EXPORT void on_sgb_model_changed(GtkWidget *w, gpointer user_data_gptr) {
	GtkComboBox *box = GTK_COMBO_BOX(w);
	g_print("New value: %s\n", gtk_combo_box_get_active_id(box));
}

G_MODULE_EXPORT void on_dmg_model_changed(GtkWidget *w, gpointer user_data_gptr) {
	GtkComboBox *box = GTK_COMBO_BOX(w);
	g_print("New value: %s\n", gtk_combo_box_get_active_id(box));
}

G_MODULE_EXPORT void on_rewind_duration_changed(GtkWidget *w, gpointer user_data_gptr) {
	GtkComboBox *box = GTK_COMBO_BOX(w);
	g_print("New value: %s\n", gtk_combo_box_get_active_id(box));
}

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_color_menubar_override_changed(GtkWidget *w, gpointer user_data_gptr) {
	config.menubar_override = (gchar *)gtk_combo_box_get_active_id(GTK_COMBO_BOX(w));
}

// This functions gets called immediately after registration of the GApplication
static void startup(GApplication *app, gpointer user_data_gptr) {
	UserData *user_data = user_data_gptr;

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

	// Setup application actions
	g_action_map_add_action_entries(G_ACTION_MAP(app), app_entries, G_N_ELEMENTS(app_entries), app);

#if NDEBUG
	// Disable when not compiled in debug mode
	g_simple_action_set_enabled(G_SIMPLE_ACTION(g_action_map_lookup_action(G_ACTION_MAP(app), "open_gtk_debugger")), false);

	// Remove the menubar override
	gtk_widget_destroy(gtkget(GTK_WIDGET, "menubar_override_selector_label"));
	gtk_widget_destroy(gtkget(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(user_data->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"));

	// setup main window
	main_window = GTK_APPLICATION_WINDOW(gtk_application_window_new(GTK_APPLICATION(app)));
	gtk_application_window_set_show_menubar(main_window, true);

	// create our renderer area
	gl_area = GTK_GL_AREA(gtk_gl_area_new());
	gtk_gl_area_set_auto_render(gl_area, false);

	// 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));

	// Connect signal handlers
	g_signal_connect(gl_area, "realize",   G_CALLBACK(gl_init), NULL);
	g_signal_connect(gl_area, "render",    G_CALLBACK(gl_draw), NULL);
	g_signal_connect(gl_area, "resize",    G_CALLBACK(gl_resize), NULL);
	g_signal_connect(gl_area, "unrealize", G_CALLBACK(gl_finish), 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_vram_viewer_tilemap), NULL);
	
	gtk_container_add(GTK_CONTAINER(main_window), GTK_WIDGET(gl_area));

	// Handle the whole menubar situation …
	GMenuModel *menubar = get_menu_model(app, "menubar");

	if (show_menubar()) {
		// Show a classic menubar
		gtk_application_set_menubar(GTK_APPLICATION(app), menubar);
	}
	else {
		// Attach a custom title bar
		GtkWidget *titlebar = GTK_WIDGET(gtk_builder_get_object(builder, "main_header_bar"));
		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);
	}

	gtk_window_set_title(GTK_WINDOW(main_window), "SameBoy");

	// 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, g_list_nth_data(icon_list, 3)); // reuse the 64x64 icon
	gtk_about_dialog_set_version(about_dialog, "v" xstr(VERSION));
	g_list_free(icon_list);
}

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

	if (user_data->fullscreen) {
		gtk_window_fullscreen(GTK_WINDOW(main_window));
	}

	g_signal_connect(main_window, "destroy", G_CALLBACK(on_quit), app);

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

	// Start the emulation loop.
	// This loop takes care of the GTK main loop.
	run(app, user_data);
}

// 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 user_data_gptr) {
	UserData *user_data = user_data_gptr;

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

	user_data->file = files[0];

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

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

	save_settings();
	free_settings();
}

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

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

	if (g_variant_dict_lookup(options, "fullscreen", "b", &count)) {
		user_data->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")) {
			if (g_str_has_suffix(model_name, "-B") || g_strcmp0(model_name, "DMG") == 0) {
				user_data->model = GB_MODEL_DMG_B;
			}
			else {
				user_data->model = GB_MODEL_DMG_B;
				g_printerr("Unsupported revision: %s\nFalling back to DMG-B", model_name);
			}
		}
		else if (g_str_has_prefix(model_name, "SGB")) {
			if (g_str_has_suffix(model_name, "-NTSC") || g_strcmp0(model_name, "SGB") == 0) {
				user_data->model = GB_MODEL_SGB;
			}
			else if (g_str_has_suffix(model_name, "-PAL")) {
				user_data->model = GB_MODEL_SGB | GB_MODEL_PAL_BIT;
			}
			else if (g_str_has_suffix(model_name, "2")) {
				user_data->model = GB_MODEL_SGB2;
			}
			else {
				user_data->model = GB_MODEL_SGB2;
				g_printerr("Unsupported revision: %s\nFalling back to SGB2", model_name);
			}
		}
		else if (g_str_has_prefix(model_name, "CGB")) {
			if (g_str_has_suffix(model_name, "-C")) {
				user_data->model = GB_MODEL_CGB_C;
			}
			else if (g_str_has_suffix(model_name, "-E") || g_strcmp0(model_name, "CGB") == 0) {
				user_data->model = GB_MODEL_CGB_E;
			}
			else {
				user_data->model = GB_MODEL_CGB_E;
				g_printerr("Unsupported revision: %s\nFalling back to CGB-E", model_name);
			}
		}
		else if (g_str_has_prefix(model_name, "AGB")) {
			user_data->model = GB_MODEL_AGB;
		}
		else {
			g_printerr("Unknown model: %s\n", model_name);
			exit(EXIT_FAILURE);
		}
	}

	return -1;
}

static uint32_t rgb_encode(GB_gameboy_t *gb, uint8_t r, uint8_t g, uint8_t b) {
	// We use GL_RGBA and GL_UNSIGNED_BYTE for our texture upload,
	// so OpenGL expects pixel data in RGBA order in memory.
#ifdef GB_LITTLE_ENDIAN
	// ABGR
	uint32_t color = 0xFF000000 | (b << 16) | (g << 8) | r;
#else
	// RGBA
	uint32_t color = (r << 24) | (g << 16) | (b << 8) | 0xFF;
#endif

	return color;
}

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]) free(image_buffers[0]);
	if (image_buffers[1]) free(image_buffers[1]);
	if (image_buffers[2]) 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] = malloc(buffer_size);
	image_buffers[1] = malloc(buffer_size);
	image_buffers[2] = malloc(buffer_size);
}

static void run(GApplication *app, UserData *user_data) {
	GB_model_t prev_model = GB_get_model(&gb);
	GB_model_t model = user_data->model? user_data->model : GB_MODEL_CGB_E; // TODO: Model from config

	if (GB_is_inited(&gb)) {
		GB_switch_model_and_reset(&gb, 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();
		}
	}
	else {
		GB_init(&gb, model);
		update_window_geometry();

		GB_set_vblank_callback(&gb, (GB_vblank_callback_t) vblank);
		GB_set_pixels_output(&gb, get_current_buffer());
		GB_set_rgb_encode_callback(&gb, rgb_encode);
		// GB_set_sample_rate(&gb, have_aspec.freq);
		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);
	}

	GError *error;
	char *boot_rom_path;
	char *boot_rom_name;
	GBytes *boot_rom_f;
	const guchar *boot_rom_data;
	gsize boot_rom_size;

	if (user_data->boot_rom_path) {
		g_print("Trying to load boot ROM from %s\n", user_data->boot_rom_path);
		if (GB_load_boot_rom(&gb, user_data->boot_rom_path)) {
			g_printerr("Falling back to boot ROM from config\n");
			goto config_boot_rom;
		}
	}
	else { config_boot_rom:
		switch (model) {
			case GB_MODEL_DMG_B:
				boot_rom_name = "dmg_boot.bin";
				break;

			case GB_MODEL_SGB:
			case GB_MODEL_SGB_PAL:
			case GB_MODEL_SGB_NO_SFC:
				boot_rom_name = "sgb_boot.bin";
				break;

			case GB_MODEL_SGB2:
			case GB_MODEL_SGB2_NO_SFC:
				boot_rom_name = "sgb2_boot.bin";
				break;

			case GB_MODEL_CGB_C:
			case GB_MODEL_CGB_E:
				boot_rom_name = "cgb_boot.bin";
				break;

			case GB_MODEL_AGB:
				boot_rom_name = "agb_boot.bin";
				break;
		}

		if (config.boot_rom_path != NULL) {
			boot_rom_path = g_build_filename(config.boot_rom_path, boot_rom_name, NULL);
			g_print("Trying to load boot ROM from %s\n", boot_rom_path);
			
			if (GB_load_boot_rom(&gb, boot_rom_path)) {
				g_printerr("Falling back to internal boot ROM\n");
				goto internal_boot_rom;
			}
		}
		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);

			if (boot_rom_f == NULL) {
				g_printerr("Failed to load internal boot ROM: %s\n", 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);
		}
	}
	
	if (user_data->file != NULL) {
		GB_load_rom(&gb, g_file_get_path(user_data->file));
	}

	/* Run emulation */
	while (running) {
		if (paused || rewind_paused) {
			while (gtk_events_pending()) {
				gtk_main_iteration();
			}
		}
		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);
		}
	}

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

int main(int argc, char *argv[]) {
	// Create our GApplication and tell GTK that we are able to handle files
	main_application = gtk_application_new(APP_ID, G_APPLICATION_HANDLES_OPEN);

	UserData user_data = { NULL };

	// 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, &user_data.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, &user_data.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), &user_data);
	g_signal_connect(main_application, "startup", G_CALLBACK(startup), &user_data);
	g_signal_connect(main_application, "activate", G_CALLBACK(activate), &user_data);
	g_signal_connect(main_application, "open", G_CALLBACK(open), &user_data);
	g_signal_connect(main_application, "shutdown", G_CALLBACK(shutdown), &user_data);

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

	return status;
}