/* gdk-pixbuf image loader test app
 *
 * compile with:
 *  gcc -g -Wall `pkg-config --cflags gtk+-2.0` pixbuf-tester.c -o pixbuf-tester `pkg-config --libs gtk+-2.0`
 *
 * an image file can be given on the command line:
 *  pixbuf-tester /path/to/image/file.png
 *
 * upper left   is result of gdk_pixbuf_new_from_file()
 * upper right  is current pixels contained within pixbuf of GdkPixbufLoader
 * bottom left  is current "area-updated" signal shown in white
 * bottom right is accumulated "area-updated" pixels shown in gray (starts out solid
 *           white, overlapping signals such as in a progressive jpeg will be darker)
 *
 * most controls should be obvious, open will reload current image
 */

#define G_DISABLE_DEPRECATED 1
#define GDK_DISABLE_DEPRECATED 1
#define GTK_DISABLE_DEPRECATED 1

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#include <gtk/gtk.h>
#include <gdk-pixbuf/gdk-pixbuf.h>


typedef struct _PBTest PBTest;
struct _PBTest {
	GtkWidget *window;
	GtkWidget *simple;
	GtkWidget *progressive_norm;
	GtkWidget *progressive_change;
	GtkWidget *progressive_coverage;

	GtkWidget *entry;
	GtkWidget *status;

	GtkWidget *button_manual;

	GdkPixbufLoader *loader;
	gchar *path;

	int fd;
	gint idle_id;

	gboolean step_manual;
	guint step_size;

	gboolean slow_draw;
	gboolean drawing;

	guint bytes;
};

/*
 * Fills region of pixbuf at x,y over w,h
 * with colors red (r), green (g), blue (b)
 * applying alpha (a), use a=255 for solid.
 */
static void pixbuf_draw_rect_fill(GdkPixbuf *pb,
				  gint x, gint y, gint w, gint h,
				  gint r, gint g, gint b, gint a)
{
	gint p_alpha;
	gint pw, ph, prs;
	guchar *p_pix;
	guchar *pp;
	gint i, j;

	if (!pb) return;

	pw = gdk_pixbuf_get_width(pb);
	ph = gdk_pixbuf_get_height(pb);

	if (x < 0 || x + w > pw) return;
	if (y < 0 || y + h > ph) return;

	p_alpha = gdk_pixbuf_get_has_alpha(pb);
	prs = gdk_pixbuf_get_rowstride(pb);
	p_pix = gdk_pixbuf_get_pixels(pb);

        for (i = 0; i < h; i++)
		{
		pp = p_pix + (y + i) * prs + (x * (p_alpha ? 4 : 3));
		for (j = 0; j < w; j++)
			{
			*pp = (r * a + *pp * (256-a)) >> 8;
			pp++;
			*pp = (g * a + *pp * (256-a)) >> 8;
			pp++;
			*pp = (b * a + *pp * (256-a)) >> 8;
			pp++;
			if (p_alpha) pp++;
			}
		}
}

static void window_pixbuf_sync(PBTest *pt, GdkPixbuf *pixbuf)
{
	GdkPixbuf *pb;
	gint w, h;

	w = gdk_pixbuf_get_width(pixbuf);
	h = gdk_pixbuf_get_height(pixbuf);

	pb = gdk_pixbuf_new(GDK_COLORSPACE_RGB, gdk_pixbuf_get_has_alpha(pixbuf), 8, w, h);
	gdk_pixbuf_fill(pb, 0x000000FF);
	gtk_image_set_from_pixbuf(GTK_IMAGE(pt->progressive_change), pb);
	g_object_unref(pb);

	pb = gdk_pixbuf_new(GDK_COLORSPACE_RGB, gdk_pixbuf_get_has_alpha(pixbuf), 8, w, h);
	gdk_pixbuf_fill(pb, 0xFFFFFFFF);
	gtk_image_set_from_pixbuf(GTK_IMAGE(pt->progressive_coverage), pb);
	g_object_unref(pb);
}

static void loader_area_cb(GdkPixbufLoader *loader,
			   guint x, guint y, guint w, guint h,
			   gpointer data)
{
	PBTest *pt = data;
	GdkPixbuf *pixbuf;
	GdkPixbuf *pb;

	pixbuf = gdk_pixbuf_loader_get_pixbuf(loader);
	if (!pixbuf)
		{
		printf("** area %d,%d (%dx%d) with missing pixbuf!\n", x, y, w, h);
		return;
		}

	pb = gtk_image_get_pixbuf(GTK_IMAGE(pt->progressive_norm));
	if (pb != pixbuf)
		{
		window_pixbuf_sync(pt, pixbuf);
		}

	if (x < 0 || y < 0 ||
	    x + w > gdk_pixbuf_get_width(pixbuf) ||
	    y + h > gdk_pixbuf_get_height(pixbuf))
		{
		printf("** area %d,%d (%dx%d) outside of pixbuf!\n", x, y, w, h);
		return;
		}

	printf("area signal: %d,%d (%dx%d)\n", x, y, w, h);
	gtk_image_set_from_pixbuf(GTK_IMAGE(pt->progressive_norm), pixbuf);

	/* change */
	pb = gtk_image_get_pixbuf(GTK_IMAGE(pt->progressive_change));
	g_object_ref(pb);
	pixbuf_draw_rect_fill(pb, 0, 0, gdk_pixbuf_get_width(pb), gdk_pixbuf_get_height(pb),
			      0, 0, 0, 255);
	pixbuf_draw_rect_fill(pb, x, y, w, h,
			      255, 255, 255, 255);
	gtk_image_set_from_pixbuf(GTK_IMAGE(pt->progressive_change), pb);
	g_object_unref(pb);

	/* coverage */
	pb = gtk_image_get_pixbuf(GTK_IMAGE(pt->progressive_coverage));
	g_object_ref(pb);
	pixbuf_draw_rect_fill(pb, x, y, w, h,
			      0, 0, 0, 128);
	gtk_image_set_from_pixbuf(GTK_IMAGE(pt->progressive_coverage), pb);
	g_object_unref(pb);

	if (pt->slow_draw && !pt->step_manual)
		{
		/* do not click on anything but slow draw toggle while here or boom! */
		pt->drawing = TRUE;
		while (g_main_context_iteration(NULL, FALSE));
		pt->drawing = FALSE;
		usleep(250000);
		}
}

static void loader_stop(PBTest *pt)
{
	if (pt->idle_id != -1)
		{
		g_source_remove(pt->idle_id);
		pt->idle_id = -1;
		}

	if (pt->loader)
		{
		gdk_pixbuf_loader_close(pt->loader, NULL);
		g_object_unref(G_OBJECT(pt->loader));
		pt->loader = NULL;
		}

	if (pt->fd != -1)
		{
		close(pt->fd);
		pt->fd = -1;
		}
}

static gboolean loader_idle_cb(gpointer data)
{
	PBTest *pt = data;
	guchar *buf;
	gint b;
	GError *error = NULL;
	gint result = FALSE;

	if (pt->fd == -1)
		{
		pt->idle_id = -1;
		loader_stop(pt);
		return FALSE;
		}

	buf = g_malloc(pt->step_size);

	b = read(pt->fd, buf, pt->step_size);
	if (b == 0)
		{
		printf("done %s\n", pt->path);
		}
	else if (b < 0)
		{
		printf("** read error %s\n", pt->path);
		}
	else if (!gdk_pixbuf_loader_write(pt->loader, buf, b, &error))
		{
		if (error)
			{
			printf("** loader return error on write %s\n"
			       "** error: %s\n", pt->path, error->message);
			g_error_free(error);
			}
		}
	else
		{
		gchar *text;
		pt->bytes += b;

		text = g_strdup_printf("bytes read: %d", pt->bytes);
		gtk_label_set_text(GTK_LABEL(pt->status), text);
		g_free(text);

		result = TRUE;
		}

	if (!result)
		{
		pt->idle_id = -1;
		loader_stop(pt);
		}

	g_free(buf);

	if (pt->step_manual && pt->idle_id != -1)
		{
		pt->idle_id = -1;
		return FALSE;
		}

	return result;
}

static void loader_idle_start(PBTest *pt, gboolean reset_off)
{
	if (pt->idle_id != -1)
		{
		g_source_remove(pt->idle_id);
		pt->idle_id = -1;
		}

	if (!reset_off)
		{
		pt->idle_id = g_idle_add(loader_idle_cb, pt);
		}
}

static void loader_start(PBTest *pt)
{
	loader_stop(pt);

	if (!pt->path) return;

	pt->bytes = 0;
	gtk_label_set_text(GTK_LABEL(pt->status), "bytes read: 0");

	pt->loader = gdk_pixbuf_loader_new();
	g_signal_connect(G_OBJECT(pt->loader), "area_updated",
			 G_CALLBACK(loader_area_cb), pt);

	pt->fd = open(pt->path, O_RDONLY | O_NONBLOCK);
	if (pt->fd == -1)
		{
		printf("fail to open %s\n", pt->path);
		loader_stop(pt);
		return;
		}

	if (!pt->step_manual) loader_idle_start(pt, FALSE);
}

static void window_reset(PBTest *pt)
{
	loader_stop(pt);

	gtk_image_set_from_pixbuf(GTK_IMAGE(pt->simple), NULL);
	gtk_image_set_from_pixbuf(GTK_IMAGE(pt->progressive_norm), NULL);
	gtk_image_set_from_pixbuf(GTK_IMAGE(pt->progressive_change), NULL);
	gtk_image_set_from_pixbuf(GTK_IMAGE(pt->progressive_coverage), NULL);

	g_free(pt->path);
	pt->path = NULL;
}

static void window_set_path(PBTest *pt, const gchar *path)
{
	window_reset(pt);

	if (!path) return;

	printf("loading image \"%s\"\n", path);

	pt->path = g_strdup(path);

	gtk_image_set_from_file(GTK_IMAGE(pt->simple), pt->path);

	loader_start(pt);
}

static void step_cb(GtkWidget *button, gpointer data)
{
	PBTest *pt = data;

	if (!pt->step_manual)
		{
		gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(pt->button_manual), TRUE);
		}

	if (pt->drawing) return;

	loader_idle_cb(pt);
}

static void step_size_cb(GtkSpinButton *spin_button, gpointer data)
{
	PBTest *pt = data;

	pt->step_size = gtk_spin_button_get_value_as_int(spin_button);
}

static void manual_cb(GtkWidget *button, gpointer data)
{
	PBTest *pt = data;

	pt->step_manual = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button));

	if (pt->drawing) return;

	loader_idle_start(pt, pt->step_manual);
}

static void slow_cb(GtkWidget *button, gpointer data)
{
	PBTest *pt = data;

	pt->slow_draw = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button));
}

static void open_cb(GtkWidget *button, gpointer data)
{
	PBTest *pt = data;
	const gchar *path;

	if (pt->drawing) return;

	path = gtk_entry_get_text(GTK_ENTRY(pt->entry));
	window_set_path(pt, path);
}

static GtkWidget *window_pack_image(GtkWidget *box, const gchar *text)
{
	GtkWidget *vbox;
	GtkWidget *label;
	GtkWidget *scrolled;
	GtkWidget *image;

	vbox = gtk_vbox_new(FALSE, 0);
	gtk_box_pack_start(GTK_BOX(box), vbox, TRUE, TRUE, 0);
	gtk_widget_show(vbox);

	label = gtk_label_new(text);
	gtk_box_pack_start(GTK_BOX(vbox), label , FALSE, FALSE, 0);
	gtk_widget_show(label);

	scrolled = gtk_scrolled_window_new(NULL, NULL);
	gtk_box_pack_start(GTK_BOX(vbox), scrolled, TRUE, TRUE, 0);
	gtk_widget_show(scrolled);

	image = gtk_image_new();
	gtk_scrolled_window_add_with_viewport(GTK_SCROLLED_WINDOW(scrolled), image);
	gtk_widget_show(image);

	return image;
}

static gboolean window_delete_cb(GtkWidget *widget, GdkEvent *event, gpointer data)
{
	gtk_main_quit();
	return TRUE;
}

static void window_destroy_cb(GtkWidget *widget, gpointer data)
{
	PBTest *pt = data;

	window_reset(pt);
	g_free(pt);
}

static void window_build(const gchar *path)
{
	PBTest *pt;
	GtkWidget *vbox;
	GtkWidget *hbox;
	GtkWidget *label;
	GtkWidget *button;

	pt = g_new0(PBTest, 1);
	pt->fd = -1;
	pt->idle_id = -1;
	pt->step_size = 512;

	pt->window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
	g_signal_connect(G_OBJECT(pt->window), "destroy",
			 G_CALLBACK(window_destroy_cb), pt);
	g_signal_connect(G_OBJECT(pt->window), "delete_event",
			 G_CALLBACK(window_delete_cb), pt);

	gtk_widget_set_size_request(pt->window, 800, 600);

	vbox = gtk_vbox_new(FALSE, 0);
	gtk_container_add(GTK_CONTAINER(pt->window), vbox);
	gtk_widget_show(vbox);

	hbox = gtk_hbox_new(TRUE, 0);
	gtk_box_pack_start(GTK_BOX(vbox), hbox, TRUE, TRUE, 0);
	gtk_widget_show(hbox);

	pt->simple = window_pack_image(hbox, "gdk_pixbuf_new_from_file");
	pt->progressive_norm = window_pack_image(hbox, "progressive (current pixels in pixbuf)");

	hbox = gtk_hbox_new(TRUE, 0);
	gtk_box_pack_start(GTK_BOX(vbox), hbox, TRUE, TRUE, 0);
	gtk_widget_show(hbox);

	pt->progressive_change = window_pack_image(hbox, "change (last area signal is white)");
	pt->progressive_coverage = window_pack_image(hbox, "coverage (accumulated area signals are gray)");

	hbox = gtk_hbox_new(FALSE, 0);
	gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 0);
	gtk_widget_show(hbox);

	pt->entry = gtk_entry_new();
	if (path) gtk_entry_set_text(GTK_ENTRY(pt->entry), path);
	g_signal_connect(G_OBJECT(pt->entry), "activate",
			 G_CALLBACK(open_cb), pt);
	gtk_box_pack_start(GTK_BOX(hbox), pt->entry, TRUE, TRUE, 0);
	gtk_widget_show(pt->entry);

	button = gtk_button_new_from_stock(GTK_STOCK_OPEN);
	g_signal_connect(G_OBJECT(button), "clicked",
			 G_CALLBACK(open_cb), pt);
	gtk_box_pack_start(GTK_BOX(hbox), button, FALSE, FALSE, 0);
	gtk_widget_show(button);

	hbox = gtk_hbox_new(FALSE, 8);
	gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 0);
	gtk_widget_show(hbox);

	label = gtk_label_new("chunk read size:");
	gtk_box_pack_start(GTK_BOX(hbox), label, FALSE, FALSE, 0);
	gtk_widget_show(label);

	button = gtk_spin_button_new_with_range(1.0, 1024.0 * 1024.0 * 10.0, 1024.0);
	gtk_spin_button_set_value(GTK_SPIN_BUTTON(button), (gdouble)pt->step_size);
	g_signal_connect(G_OBJECT(button), "value_changed",
			 G_CALLBACK(step_size_cb), pt);
	gtk_box_pack_start(GTK_BOX(hbox), button, FALSE, FALSE, 0);
	gtk_widget_show(button);

	pt->button_manual = gtk_check_button_new_with_label("manual step");
	g_signal_connect(G_OBJECT(pt->button_manual), "clicked",
			 G_CALLBACK(manual_cb), pt);
	gtk_box_pack_start(GTK_BOX(hbox), pt->button_manual, FALSE, FALSE, 0);
	gtk_widget_show(pt->button_manual);

	button = gtk_button_new_with_label("step 1 chunk");
	g_signal_connect(G_OBJECT(button), "clicked",
			 G_CALLBACK(step_cb), pt);
	gtk_box_pack_start(GTK_BOX(hbox), button, FALSE, FALSE, 0);
	gtk_widget_show(button);

	pt->status = gtk_label_new("");
	gtk_box_pack_start(GTK_BOX(hbox), pt->status, FALSE, FALSE, 0);
	gtk_widget_show(pt->status);

	button = gtk_check_button_new_with_label("slow draw");
	g_signal_connect(G_OBJECT(button), "clicked",
			 G_CALLBACK(slow_cb), pt);
	gtk_box_pack_start(GTK_BOX(hbox), button, FALSE, FALSE, 0);
	gtk_widget_show(button);


	gtk_widget_show(pt->window);

	if (path) window_set_path(pt, path);
}

int main (int argc, char *argv[])
{
	gchar *path = NULL;

	gtk_init (&argc, &argv);

	if (argc > 1)
		{
		path = argv[1];
		}

	window_build(path);
	gtk_main();

	return 0;
}

