#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
#include <gtk/gtk.h>
#include <libgimp/gimp.h>
#include <libgimp/gimpui.h>

#define PLUG_IN_NAME        "shapeit"
#define PLUG_IN_PRINT_NAME  "ShapeIt"
#define PLUG_IN_VERSION     "v0.6 060619"
#define PLUG_IN_MENU_PATH   "<Image>/Filters/Map/Shape It"
#define PLUG_IN_IMAGE_TYPES "RGBA, GRAYA"
#define PLUG_IN_AUTHOR      "Claes G Lindblad ported to 2.2 by Victor Ramirez [vramirez@gmail.com]. Thank you, Victor!"
#define PLUG_IN_COPYRIGHT   "Claes G Lindblad <claesg@algonet.se>"
#define PLUG_IN_DESCRIPTION "Shapes active layer according to information from a black & white map."
#define PLUG_IN_HELP	    "Create a white map layer and fill the shape you need with black. Then use this map layer to displace any other layer. Please note that both must have alpha."
#define NUMBER_IN_ARGS		6
#define NUMBER_OUT_ARGS 0
#define OUT_ARGS NULL

#define ENTRY_WIDTH 40
#define TILE_CACHE_SIZE 48

typedef struct {
	gint shape_map_y;
	gint offset;
	gint invert;
} ShapeVals;

typedef struct {
	GtkWidget *menu_y;
	GtkWidget *offset;
	GtkWidget *invert;
	gint run;
} ShapeInterface;


static void query(void);
static void run(const gchar * name,
		gint nparams,
		const GimpParam * param,
		gint * nreturn_vals, GimpParam ** return_vals);

static void shape(GimpDrawable * drawable);

static gint shape_dialog(GimpDrawable * drawable);

static gint shape_map_constrain(gint32 image_id,
				gint32 drawable_id, gpointer data);

static void shape_map_y_callback(gint32 id, gpointer data);

static void shape_close_callback(GtkWidget * widget, gpointer data);

static void shape_ok_callback(GtkWidget * widget, gpointer data);

static void shape_entry_callback(GtkWidget * widget, gpointer data);

static void invert_toggle_update(GtkWidget * widget, gpointer data);

/***** Local vars *****/

GimpPlugInInfo PLUG_IN_INFO = {
	NULL,			/* init_proc */
	NULL,			/* quit_proc */
	query,			/* query_proc */
	run,			/* run_proc */
};

static ShapeVals dvals = {
	-1,			/* shape_map_y */
	0,			/* offset */
	FALSE			/* invert */
};

static ShapeInterface dint = {
	NULL,			/*  menu_y  */
	NULL,			/* offset */
	NULL,			/* invert */
	FALSE			/*  run  */
};

/***** Functions *****/

MAIN()
static void query(void)
{
	static GimpParamDef args[] = {
		{GIMP_PDB_INT32, "run-mode",
		 "Interactive, non-interactive"},
		{GIMP_PDB_IMAGE, "image", "Input image"},
		{GIMP_PDB_DRAWABLE, "drawable", "Input drawable"},
		{GIMP_PDB_DRAWABLE, "shape_map_y",
		 "Displacement map for Y direction"},
		{GIMP_PDB_INT32, "offset", "Offset amount."},
		{GIMP_PDB_INT32, "invert", "Inversion"}
	};
	static gint nargs = NUMBER_IN_ARGS;
	static GimpParamDef *return_vals = OUT_ARGS;
	static gint nreturn_vals = NUMBER_OUT_ARGS;

/* the actual installation of the plugin */
	gimp_install_procedure(PLUG_IN_NAME,
			       PLUG_IN_DESCRIPTION,
			       PLUG_IN_HELP,
			       PLUG_IN_AUTHOR,
			       PLUG_IN_COPYRIGHT,
			       PLUG_IN_VERSION,
			       PLUG_IN_MENU_PATH,
			       PLUG_IN_IMAGE_TYPES,
			       GIMP_PLUGIN,
			       nargs, nreturn_vals, args, return_vals);
}

static void
run(const gchar * name,
    gint nparams,
    const GimpParam * param, gint * nreturn_vals, GimpParam ** return_vals)
{
	static GimpParam values[1];
	GimpDrawable *drawable;
	GimpPDBStatusType status = GIMP_PDB_SUCCESS;
	GimpRunMode run_mode;


	*nreturn_vals = 1;
	*return_vals = values;

	values[0].type = GIMP_PDB_STATUS;
	values[0].data.d_status = status;

	/* Get the run mode from the in-parameters */
	run_mode = param[0].data.d_int32;

	/*  Get the specified drawable  */
	drawable = gimp_drawable_get(param[2].data.d_drawable);

	switch (run_mode) {
	case GIMP_RUN_INTERACTIVE:
		/*  Possibly retrieve data  */
		gimp_get_data(PLUG_IN_NAME, &dvals);

		/*  First acquire information with a dialog  */
		if (!shape_dialog(drawable))
			return;
		break;

	case GIMP_RUN_NONINTERACTIVE:
		/*  Make sure all the arguments are there!  */
		if (nparams != NUMBER_IN_ARGS)
			status = GIMP_PDB_CALLING_ERROR;
		if (status == GIMP_PDB_SUCCESS) {
			dvals.shape_map_y = param[3].data.d_int32;
			dvals.offset = param[4].data.d_int32;
			dvals.invert = param[5].data.d_int32;
		}
		break;

	case GIMP_RUN_WITH_LAST_VALS:
		/*  Possibly retrieve data  */
		gimp_get_data(PLUG_IN_NAME, &dvals);
		break;

	default:
		break;
	}

	if (status == GIMP_PDB_SUCCESS) {
		gimp_progress_init("Shaping...");

		/*  set the tile cache size  */
		gimp_tile_cache_ntiles(TILE_CACHE_SIZE);

		/*  run the shape effect  */
		shape(drawable);

		if (run_mode != GIMP_RUN_NONINTERACTIVE)
			gimp_displays_flush();

		/*  Store data  */
		if (run_mode == GIMP_RUN_INTERACTIVE)
			gimp_set_data(PLUG_IN_NAME, &dvals,
				      sizeof(ShapeVals));
	}

	values[0].data.d_status = status;

	gimp_drawable_detach(drawable);
}

static int shape_dialog(GimpDrawable * drawable)
{
	GtkWidget *dlg;
	GtkWidget *button;
	GtkWidget *button3;
	GtkWidget *frame;
	GtkWidget *label;
	GtkWidget *table;
	GtkWidget *entry;
	GtkWidget *option_menu;
	GtkWidget *menu;
	gchar buffer[32];
	gchar **argv;
	gint argc;

	argc = 1;
	argv = g_new(gchar *, 1);
	argv[0] = g_strdup("shape");

	gtk_init(&argc, &argv);
	gtk_rc_parse(gimp_gtkrc());
	dlg = gtk_dialog_new();
	gtk_window_set_title(GTK_WINDOW(dlg), "Shape It");
	gtk_window_position(GTK_WINDOW(dlg), GTK_WIN_POS_MOUSE);
	gtk_signal_connect(GTK_OBJECT(dlg), "destroy",
			   (GtkSignalFunc) shape_close_callback, NULL);

	/*  Action area  */
	button = gtk_button_new_with_label("OK");
	GTK_WIDGET_SET_FLAGS(button, GTK_CAN_DEFAULT);
	gtk_signal_connect(GTK_OBJECT(button), "clicked",
			   (GtkSignalFunc) shape_ok_callback, dlg);
	gtk_box_pack_start(GTK_BOX(GTK_DIALOG(dlg)->action_area), button,
			   TRUE, TRUE, 0);
	gtk_widget_grab_default(button);
	gtk_widget_show(button);

	button = gtk_button_new_with_label("Cancel");
	GTK_WIDGET_SET_FLAGS(button, GTK_CAN_DEFAULT);
	gtk_signal_connect_object(GTK_OBJECT(button), "clicked",
				  (GtkSignalFunc) gtk_widget_destroy,
				  GTK_OBJECT(dlg));
	gtk_box_pack_start(GTK_BOX(GTK_DIALOG(dlg)->action_area), button,
			   TRUE, TRUE, 0);
	gtk_widget_grab_default(button);
	gtk_widget_show(button);

	/*  The main table  */
	frame = gtk_frame_new
	    ("Which layer to use as Shape Map on active layer?");
	gtk_frame_set_shadow_type(GTK_FRAME(frame), GTK_SHADOW_ETCHED_IN);
	gtk_container_border_width(GTK_CONTAINER(frame), 10);
	gtk_box_pack_start(GTK_BOX(GTK_DIALOG(dlg)->vbox), frame, TRUE,
			   TRUE, 0);

	table = gtk_table_new(3, 3, FALSE);
	gtk_container_border_width(GTK_CONTAINER(table), 5);
	gtk_container_add(GTK_CONTAINER(frame), table);

	gtk_table_set_row_spacing(GTK_TABLE(table), 0, 10);
	gtk_table_set_row_spacing(GTK_TABLE(table), 1, 10);
	gtk_table_set_col_spacing(GTK_TABLE(table), 0, 10);
	gtk_table_set_col_spacing(GTK_TABLE(table), 1, 10);

/* label */
	label = gtk_label_new("Offset amount");
	gtk_misc_set_alignment(GTK_MISC(label), 0.0, 0.5);
	gtk_table_attach(GTK_TABLE(table), label, 1, 2, 0, 1, GTK_FILL,
			 GTK_FILL, 4, 0);
	gtk_widget_show(label);


/* invert */
	button3 = gtk_check_button_new_with_label("Invert shape");
	gtk_table_attach(GTK_TABLE(table), button3, 0, 1, 1, 2,
			 GTK_EXPAND | GTK_FILL, GTK_EXPAND | GTK_FILL, 0,
			 0);
	gtk_signal_connect(GTK_OBJECT(button3), "toggled",
			   (GtkSignalFunc) invert_toggle_update,
			   &dvals.invert);
	gtk_toggle_button_set_state(GTK_TOGGLE_BUTTON(button3),
				    dvals.invert);
	gtk_widget_show(button3);

/* offset */
	entry = gtk_entry_new();
	gtk_table_attach(GTK_TABLE(table), entry, 1, 2, 1, 2, GTK_FILL,
			 GTK_FILL, 0, 0);
	gtk_widget_set_usize(entry, ENTRY_WIDTH, 0);
	sprintf(buffer, "%d", dvals.offset);
	gtk_entry_set_text(GTK_ENTRY(entry), buffer);
	gtk_signal_connect(GTK_OBJECT(entry), "changed",
			   (GtkSignalFunc) shape_entry_callback,
			   &dvals.offset);
	gtk_widget_show(entry);

/*  menu_y  */
	dint.menu_y = option_menu = gtk_option_menu_new();
	gtk_table_attach(GTK_TABLE(table), option_menu, 0, 1, 0, 1,
			 GTK_EXPAND | GTK_FILL, GTK_EXPAND | GTK_FILL, 0,
			 0);
	menu =
	    gimp_drawable_menu_new(shape_map_constrain,
				   shape_map_y_callback, drawable,
				   dvals.shape_map_y);
	gtk_option_menu_set_menu(GTK_OPTION_MENU(option_menu), menu);
	gtk_widget_set_sensitive(dint.menu_y, TRUE);

	gtk_widget_show(option_menu);
	gtk_widget_show(table);
	gtk_widget_show(frame);
	gtk_widget_show(dlg);

	gtk_main();
	gdk_flush();

	return dint.run;
}

/* The shaping is done here. */

static void shape(GimpDrawable * drawable)
{
	GimpDrawable *map_y;
	GimpPixelRgn mapPR, destPR, activePR;
	gint width, height, bytes;
	gint mapbytes;
	gint row, col;
	gint position;
	gint start;
	gint pixel, pixelA;
	guchar *map_col, *active_col, *temp_col;

	/* initialize */
	row = col = 0;
	map_y = gimp_drawable_get(dvals.shape_map_y);

	/* Get the size of the drawable. */
	width = drawable->width;
	height = drawable->height;
	bytes = drawable->bpp;
	mapbytes = map_y->bpp;

	if (bytes != mapbytes) {
		gimp_message
		    ("Active layer and Map layer must be of same type.\nDo you need to add Alpha channel to one of them?\n");
		return;
	}

	gimp_pixel_rgn_init(&mapPR, map_y, 0, 0, width, height, FALSE,
			    FALSE);
	gimp_pixel_rgn_init(&activePR, drawable, 0, 0, width, height,
			    FALSE, FALSE);
	gimp_pixel_rgn_init(&destPR, drawable, 0, 0, width, height, TRUE,
			    TRUE);

	map_col = (guchar *) g_malloc(height * bytes);
	active_col = (guchar *) g_malloc(height * bytes);
	temp_col = (guchar *) g_malloc(2 * height * bytes);

	for (col = 0; col < width; col++) {
		gimp_pixel_rgn_get_col(&mapPR, map_col, col, 0, height);
		row = 0;
		position = FALSE;

		while ((row < height) && (position == FALSE)) {
			pixel = map_col[row * bytes];
			pixelA = map_col[row * bytes + bytes - 1];
			if ((pixel == 0) && (pixelA == 255))
				position = height - row;
			row++;
		}
		if (position == FALSE)
			position = 0;
		if (dvals.invert == TRUE)
			position = height - position;

		while (dvals.offset > height) {
			dvals.offset -= height;
		}
		while (dvals.offset < (height * -1)) {
			dvals.offset += height;
		}

		gimp_pixel_rgn_get_col(&activePR, active_col, col, 0,
				       height);
		memcpy(temp_col, active_col, height * bytes);
		memcpy(temp_col + height * bytes, active_col,
		       height * bytes);
		start = height + dvals.offset + position;
		start = start % height;
		gimp_pixel_rgn_set_col(&destPR, temp_col + start * bytes,
				       col, 0, height);

		if ((col % 10) == 0)
			gimp_progress_update((double) col /
					     (double) width);
	}

	g_free(map_col);
	g_free(active_col);
	g_free(temp_col);

	gimp_progress_update(1.0);
	gimp_drawable_flush(drawable);
	gimp_drawable_merge_shadow(drawable->drawable_id, TRUE);
	gimp_drawable_update(drawable->drawable_id, 0, 0, width, height);

	return;
}

/*  Shape interface functions  */

static gint
shape_map_constrain(gint32 image_id, gint32 drawable_id, gpointer data)
{
	GimpDrawable *drawable;

	drawable = (GimpDrawable *) data;

	if (drawable_id == -1)
		return TRUE;

	if (gimp_drawable_width(drawable_id) == drawable->width &&
	    gimp_drawable_height(drawable_id) == drawable->height)
		return TRUE;
	else
		return FALSE;
}

static void shape_map_y_callback(gint32 id, gpointer data)
{
	dvals.shape_map_y = id;
}

static void shape_close_callback(GtkWidget * widget, gpointer data)
{
	gtk_main_quit();
}

static void shape_ok_callback(GtkWidget * widget, gpointer data)
{
	dint.run = TRUE;
	gtk_widget_destroy(GTK_WIDGET(data));
}

static void invert_toggle_update(GtkWidget * widget, gpointer data)
{
	int *toggle_val;

	toggle_val = (int *) data;

	if (GTK_TOGGLE_BUTTON(widget)->active)
		*toggle_val = TRUE;
	else
		*toggle_val = FALSE;
}

static void shape_entry_callback(GtkWidget * widget, gpointer data)
{
	int *text_val;

	text_val = (int *) data;

	*text_val = atoi(gtk_entry_get_text(GTK_ENTRY(widget)));
}
