#include "gb_screen.h"
#include "config.h"
#include "util.h"
#include <stdint.h>

struct _GbScreen {
	GtkBin parent;

	GtkGLArea *gl_area;

	bool use_gl;
	shader_t shader;

	uint32_t *image_buffers[3];
	unsigned char current_buffer;

	unsigned screen_width;
	unsigned screen_height;

	GB_frame_blending_mode_t blending_mode;
};

G_DEFINE_TYPE(GbScreen, gb_screen, GTK_TYPE_BIN);

typedef enum {
	PROP_USE_GL = 1,
	N_PROPERTIES
} GbScreenProperty;

static GParamSpec *obj_properties[N_PROPERTIES] = { NULL, };

static void gb_screen_finalize(GObject *object) {
	GbScreen *self = (GbScreen *) object;

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

	G_OBJECT_CLASS(gb_screen_parent_class)->finalize(object);
}

static void gb_screen_get_natural_size(GbScreen *self, gint *natural_width, gint *natural_height, double *scale_x_ptr, double *scale_y_ptr) {
	int width = gtk_widget_get_allocated_width(GTK_WIDGET(self));
	int height = gtk_widget_get_allocated_height(GTK_WIDGET(self));

	double scale_x = width  / (double)self->screen_width;
	double scale_y = height / (double)self->screen_height;

	if (config.video.use_integer_scaling) {
		scale_x = (unsigned)(scale_x);
		scale_y = (unsigned)(scale_y);
	}

	if (config.video.keep_aspect_ratio) {
		if (scale_x > scale_y) {
			scale_x = scale_y;
		}
		else {
			scale_y = scale_x;
		}
	}

	scale_x = max_double(1.0, scale_x);
	scale_y = max_double(1.0, scale_y);

	if (natural_width)  *natural_width  = self->screen_width  * scale_x;
	if (natural_height) *natural_height = self->screen_height * scale_y;
	if (scale_x_ptr) *scale_x_ptr = scale_x;
	if (scale_y_ptr) *scale_y_ptr = scale_y;
}

static void gb_screen_calculate_viewport(GbScreen *self, Rect *viewport_ptr, gint *scaled_width_ptr, gint *scaled_height_ptr, double *scale_x_ptr, double *scale_y_ptr) {
	gint scaled_width, scaled_height;
	double scale_x, scale_y;

	GtkWidget *widget = GTK_WIDGET(self);
	int width = gtk_widget_get_allocated_width(widget);
	int height = gtk_widget_get_allocated_height(widget);
	gb_screen_get_natural_size(self, &scaled_width, &scaled_height, &scale_x, &scale_y);

	if (viewport_ptr) {
		*viewport_ptr = (Rect){
			(width  - scaled_width)  / 2,
			(height - scaled_height) / 2,
			scaled_width,
			scaled_height
		};
	}
	if (scaled_width_ptr)  *scaled_width_ptr  = scaled_width;
	if (scaled_height_ptr) *scaled_height_ptr = scaled_height;
	if (scale_x_ptr) *scale_x_ptr = scale_x;
	if (scale_y_ptr) *scale_y_ptr = scale_y;
}

static gboolean gb_screen_draw(GtkWidget *widget, cairo_t *cr) {
	GbScreen *self = (GbScreen *)widget;

	if (!self->use_gl) {
		int width = gtk_widget_get_allocated_width(widget);
		int height = gtk_widget_get_allocated_height(widget);

		GtkStyleContext *context = gtk_widget_get_style_context(widget);
		gtk_render_background(context, cr, 0, 0, width, height);
		gtk_render_frame(context, cr, 0, 0, width, height);

		Rect viewport;
		double scale_x, scale_y;
		gb_screen_calculate_viewport(self, &viewport, NULL, NULL, &scale_x, &scale_y);

		cairo_surface_t *surface = cairo_image_surface_create_for_data(
			(unsigned char *) gb_screen_get_current_buffer(self),
			CAIRO_FORMAT_RGB24,
			self->screen_width,
			self->screen_height,
			cairo_format_stride_for_width(CAIRO_FORMAT_RGB24, self->screen_width)
		);

		cairo_translate(cr, viewport.x, viewport.y);
		cairo_scale(cr, scale_x, scale_y);
		cairo_set_source_surface(cr, surface, 0, 0);
		cairo_pattern_set_filter(cairo_get_source(cr), CAIRO_FILTER_NEAREST);
		cairo_paint(cr);

		cairo_surface_destroy(surface);
	}
	else {
		GTK_WIDGET_CLASS(gb_screen_parent_class)->draw(widget, cr);
	}

	return false;
}

static void gb_screen_get_preferred_width(GtkWidget *widget, gint *minimum_width, gint *natural_width) {
	GbScreen *self = (GbScreen *)widget;

	*minimum_width = self->screen_width;
	gb_screen_get_natural_size(self, natural_width, NULL, NULL, NULL);
}

static void gb_screen_get_preferred_height(GtkWidget *widget, gint *minimum_height, gint *natural_height) {
	GbScreen *self = (GbScreen *)widget;

	*minimum_height = self->screen_height;
	gb_screen_get_natural_size(self, NULL, natural_height, NULL, NULL);
}

static void gb_screen_set_property(GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) {
	GbScreen *self = (GbScreen *) object;

	switch ((GbScreenProperty) property_id) {
		case PROP_USE_GL:
			self->use_gl = g_value_get_boolean(value);
		break;

		default:
			G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
	}
}

static void gb_screen_get_property(GObject *object, guint property_id, GValue *value, GParamSpec *pspec) {
	GbScreen *self = (GbScreen *) object;

	switch ((GbScreenProperty) property_id) {
		case PROP_USE_GL:
			g_value_set_boolean(value, self->use_gl);
		break;

		default:
			G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
	}
}

static void gb_screen_gl_area_render(GtkGLArea *gl_area, GdkGLContext *context, GObject *object) {
	GbScreen *self = (GbScreen *)object;

	Rect viewport;
	gint scaled_width, scaled_height;
	gb_screen_calculate_viewport(self, &viewport, &scaled_width, &scaled_height, NULL, NULL);

	glViewport(viewport.x, viewport.y, scaled_width, scaled_height);

	uint32_t *pixels = gb_screen_get_current_buffer(self);
	uint32_t *previous = gb_screen_get_previous_buffer(self);

	static void *_pixels = NULL;

	if (pixels) {
		_pixels = pixels;
	}

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

	render_bitmap_with_shader(
		&self->shader, _pixels, previous,
		self->screen_width, self->screen_height,
		viewport.x, viewport.y, viewport.width, viewport.height,
		self->blending_mode
	);
}

static void gb_screen_gl_area_realized(GtkWidget *widget, GObject *object) {
	GbScreen *self = (GbScreen *)object;
	GtkGLArea *gl_area = GTK_GL_AREA(widget);

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

	gtk_gl_area_make_current(self->gl_area);
	if (gtk_gl_area_get_error(self->gl_area) != NULL) {
		goto error;
	}

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

	if (config.video.shader == NULL || (!init_shader_with_name(&self->shader, config.video.shader) && !init_shader_with_name(&self->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(self->gl_area, error);
	}
	else {
		g_info("Using OpenGL for rendering");
		G_OBJECT_CLASS(gb_screen_parent_class)->constructed(object);
		return;
	}

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

	g_info("Using Cairo for rendering");
	G_OBJECT_CLASS(gb_screen_parent_class)->constructed(object);
}

static void gb_screen_constructed(GObject *object) {
	GbScreen *self = (GbScreen *)object;

	// 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.
	self->use_gl = self->use_gl && test_gl_support();

	if (self->use_gl) {
		self->gl_area = GTK_GL_AREA(gtk_gl_area_new());
		g_signal_connect(self->gl_area, "realize", G_CALLBACK(gb_screen_gl_area_realized), object);
		g_signal_connect(self->gl_area, "render", G_CALLBACK(gb_screen_gl_area_render), object);

		gtk_gl_area_set_required_version(self->gl_area, 3, 2);
		gtk_gl_area_set_auto_render(self->gl_area, false);
		gtk_gl_area_set_has_alpha(self->gl_area, false);
		gtk_gl_area_set_has_depth_buffer(self->gl_area, false);
		gtk_gl_area_set_has_stencil_buffer(self->gl_area, false);
		gtk_container_add(GTK_CONTAINER(self), GTK_WIDGET(self->gl_area));
	}
	else {
		g_info("Using Cairo for rendering");
		G_OBJECT_CLASS(gb_screen_parent_class)->constructed(object);
	}
}

static void gb_screen_class_init(GbScreenClass *class) {
	obj_properties[PROP_USE_GL] = g_param_spec_boolean(
		"use-gl", "Use OpenGL", "Whether to use OpenGL for rendering.",
		true,
		G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE
	);

	G_OBJECT_CLASS(class)->finalize = gb_screen_finalize;
	G_OBJECT_CLASS(class)->set_property = gb_screen_set_property;
	G_OBJECT_CLASS(class)->get_property = gb_screen_get_property;
	G_OBJECT_CLASS(class)->constructed = gb_screen_constructed;

	GTK_WIDGET_CLASS(class)->draw = gb_screen_draw;
	GTK_WIDGET_CLASS(class)->get_preferred_width  = gb_screen_get_preferred_width;
	GTK_WIDGET_CLASS(class)->get_preferred_height = gb_screen_get_preferred_height;
	
	g_object_class_install_properties(G_OBJECT_CLASS(class), N_PROPERTIES, obj_properties);
}

static void gb_screen_init(GbScreen *self) {
	gb_screen_set_resolution(self, 160, 144);
	gb_screen_clear(self);
}

GbScreen *gb_screen_new(bool force_fallback) {
	return g_object_new(GB_SCREEN_TYPE, "use-gl", !force_fallback, NULL);
}

void gb_screen_clear(GbScreen *self) {
	for (unsigned i = 0; i < 3; i++) {
		memset(self->image_buffers[i], 0, self->screen_width * self->screen_height * sizeof(uint32_t));
	}
}

bool gb_screen_uses_fallback(GbScreen *self) {
	return !self->use_gl;
}

// Determines how many frame buffers to use
static uint8_t number_of_buffers(GbScreen *self) {
	if (!self->use_gl) return 2;

	bool should_blend = config_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
uint32_t *gb_screen_get_pixels(GbScreen *self) {
	return self->image_buffers[(self->current_buffer + 1) % number_of_buffers(self)];
}

// Returns the current finished frame
uint32_t *gb_screen_get_current_buffer(GbScreen *self) {
	return self->image_buffers[self->current_buffer];
}

// Returns the previous finished frame
uint32_t *gb_screen_get_previous_buffer(GbScreen *self) {
	return self->image_buffers[(self->current_buffer + 2) % number_of_buffers(self)];
}

// Cycles the buffers
void gb_screen_flip(GbScreen *self) {
	self->current_buffer = (self->current_buffer + 1) % number_of_buffers(self);
}

void gb_screen_set_resolution(GbScreen *self, unsigned width, unsigned height) {
	self->screen_width = width;
	self->screen_height = height;

	for (unsigned i = 0; i < 3; i++) {
		self->image_buffers[i] = g_realloc_n(
			self->image_buffers[i],
			self->screen_width * self->screen_height, sizeof(uint32_t)
		);
	}

	gtk_widget_queue_resize(GTK_WIDGET(self));
}

void gb_screen_set_blending_mode(GbScreen *self, GB_frame_blending_mode_t mode) {
	self->blending_mode = mode;
}

void gb_screen_set_shader(GbScreen *self, const char *shader_name) {
	if (!self->use_gl) return;

	free_shader(&self->shader);
	init_shader_with_name(&self->shader, shader_name);
}

void gb_screen_queue_render(GbScreen *self) {
	if (self->use_gl) {
		gtk_gl_area_queue_render(self->gl_area);
	}
	
	gtk_widget_queue_draw(GTK_WIDGET(self));
}