/*
 * This is a plug-in for the GIMP.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 *
 */

/*
 * Analyze colorcube.
 * 
 * Author: robert@experimental.net
 */

#include <glib.h>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include "libgimp/gimp.h"
#include "gtk/gtk.h"

#ifndef HAVE_RINT
#include <math.h>
#define rint(x) floor((x) + 0.5)
#endif
#ifndef bzero
#define bzero(p,size) memset(p,0,size)
#endif

/* 0 = not verbose, rest = verbose */
#define	VERBOSE		0

/* calloc new colornode, and sanitycheck */
#define CALLOC_COLOR(x)	{ \
				if ((x = (ColorNode *) calloc(1, sizeof(ColorNode))) == NULL) \
				{ \
					printf("Ick, couldn't calloc!()\n"); \
					return; \
				} \
			}

/*
 * I found the following implementation of storing a sparse color matrix
 * in Dr. Dobb's Journal #232 (July 1995).
 * 
 * The matrix is build as three linked lists, each representing a color-
 * cube axis. Each node in the matrix contains two pointers: one to its 
 * neighbour and one to the next color-axis. 
 *
 * Each red node contains a pointer to the next red node, and a pointer to
 * the green nodes. Green nodes, in turn, each contain a pointer to the next
 * green node, and a pointer to the blue axis.
 *
 * If we want to find an RGB triplet, we first walk down the red axis, match
 * the red values, from where we start walking down the green axis, etc.
 * If we haven't found our color at the end of the blue axis, it's a new color
 * and we store it in the matrix.
 *
 * For the textual-impaired (number in parentheses are color values):
 *
 *      start of table
 *      |
 *      v
 * 	RED(91)  ->  RED(212)  ->  ...
 *      |            |             
 *      |            v 
 *      |            GREEN(81)  ->  GREEN(128)  ->  ...
 *      |            |              |
 *      |            |              v
 *      |            |              BLUE(93)
 *      |            v
 *      |            BLUE(206)  ->  BLUE(93)  ->  ...
 *      v
 *      GREEN(1)  ->  ...
 *      |
 *      v
 *      BLUE(206)  ->  BLUE(12)  ->  ...
 *
 * So, some colors stored are (in RGB triplets): (91, 1, 206), (91, 1, 12),
 * (212, 128, 93), ...
 *
 */
typedef enum { RED, GREEN, BLUE } ColorType;

typedef struct ColorNode
{
	struct ColorNode	*next_neighbour;
	struct ColorNode	*next_axis;
	ColorType		color;
	unsigned int		r;
	unsigned int		g;
	unsigned int		b;
	int			count;
}	ColorNode;

typedef	struct
{
	unsigned char	r;
	unsigned char	g;
	unsigned char	b;
}	Color;

/* lets prototype */
static void	query();
static void	run(const char *, int, const GimpParam *, int *, GimpParam **);
static int	doDialog();
static void	analyze(GimpDrawable *);

static void	histogram(guchar, guchar, guchar);
static void	fillPreview(GtkWidget *);
static void	insertcolor(guchar, guchar, guchar);

static void	doLabel(GtkWidget *, char *);

static void	ok_callback(GtkWidget *, gpointer);
static void	close_callback(GtkWidget *, gpointer);

/* some global variables */
char		*filename = NULL;
int		running = 0, width, height, bpp;
const	int	verbose = VERBOSE;
ColorNode	*table = NULL;
char		*inputname;
gint32		hist_red[256], hist_green[256], hist_blue[256];
int		maxred = 0, maxgreen = 0, maxblue = 0;
int		uniques = 0;
gint32  	imageID;

/* size of histogram image */
static const int PREWIDTH = 256;
static const int PREHEIGHT = 150;

/* lets declare what we want to do */
GimpPlugInInfo PLUG_IN_INFO =
{
	NULL,			       /* init_proc */
	NULL,			       /* quit_proc */
	query,			       /* query_proc */
	run,			       /* run_proc */
};

/* run program */
MAIN();

/* tell GIMP who we are */
static
void	query()
{
	static GimpParamDef args[] =
	{
		{ GIMP_PDB_INT32, "run_mode", "Interactive" },
		{ GIMP_PDB_IMAGE, "image", "Input image" },
		{ GIMP_PDB_DRAWABLE, "drawable", "Input drawable" },
	};
	static GimpParamDef *return_vals = NULL;
	static int nargs = sizeof(args) / sizeof(args[0]);
	static int nreturn_vals = 0;

	gimp_install_procedure("plug_in_ccanalyze",
			       "Colorcube analysis",
			       "Analyze colorcube and print some information about the current image (also displays a color-histogram)",
			       "robert@experimental.net",
			       "robert@experimental.net",
			       "June 20th, 1997",
			       "<Image>/Image/Colors/Colorcube analysis",
			       "RGB*, INDEXED*, GRAY*",
			       GIMP_PLUGIN,
			       nargs, nreturn_vals,
			       args, return_vals);
}

/* main function */
static
void	run(const char *name, int nparams, const GimpParam *param, int *nreturn_vals, GimpParam **return_vals)
{
	static GimpParam	values[1];
	GimpDrawable 	*drawable;
	GimpPDBStatusType 	status = GIMP_PDB_SUCCESS;

	*nreturn_vals = 1;
	*return_vals = values;

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

	drawable = gimp_drawable_get(param[2].data.d_drawable);
	imageID = param[1].data.d_image;

	/* not much use of running this in non-interactive mode */
	if (param[0].data.d_int32 == GIMP_RUN_NONINTERACTIVE)
	{
		status = GIMP_PDB_EXECUTION_ERROR;
		return;
	}

	if (status == GIMP_PDB_SUCCESS)
	{
		if (gimp_drawable_is_rgb(drawable->drawable_id) || gimp_drawable_is_indexed(drawable->drawable_id))
		{
			bzero(hist_red, sizeof(hist_red));
			bzero(hist_green, sizeof(hist_green));
			bzero(hist_blue, sizeof(hist_blue));
			filename = gimp_image_get_filename(imageID);
			gimp_progress_init("Colorcube analysis...");
			gimp_tile_cache_ntiles(2 * (drawable->width / gimp_tile_width() + 1));
			analyze(drawable);
			/* show dialog after we analyzed image */
			doDialog();
		}
		else
			status = GIMP_PDB_EXECUTION_ERROR;
	}
	values[0].data.d_status = status;
	gimp_drawable_detach(drawable);
}

/* do the analyzing */
static
void	analyze(GimpDrawable *drawable)
{
	GimpPixelRgn	srcPR;
	guchar		*src_row, *cmap;
	gint    	x, y, numcol;
	gint    	x1, y1, x2, y2;

	/* 
	 * Get the input area. This is the bounding box of the selection in
	 * the image (or the entire image if there is no selection). Only
	 * operating on the input area is simply an optimization. It doesn't
	 * need to be done for correct operation. (It simply makes it go
	 * faster, since fewer pixels need to be operated on).
	 */
	gimp_drawable_mask_bounds(drawable->drawable_id, &x1, &y1, &x2, &y2);

        /* 
         * Get the size of the input image (this will/must be the same
         * as the size of the output image).
         */
	width = drawable->width;
	height = drawable->height;
	bpp = drawable->bpp;

	/* allocate row buffer */
	src_row = (guchar *) malloc((x2 - x1) * bpp);

	/* initialize the pixel region */
	gimp_pixel_rgn_init(&srcPR, drawable, 0, 0, width, height, FALSE, FALSE);

	cmap = gimp_image_get_cmap(imageID, &numcol);
	for (y = y1; y < y2; y++)
	{
		gimp_pixel_rgn_get_row(&srcPR, src_row, x1, y, (x2 - x1));
		for (x = x1; x < x2; x++)
		{
			guchar	red, green, blue;

			/* 
			 * If the image is indexed, fetch RGB values
			 * from colormap.
			 */
			if (cmap)
			{
				guchar	idx = src_row[x];

				red = cmap[idx * 3];
				green = cmap[idx * 3 + 1];
				blue = cmap[idx * 3 + 2];
			}
			else
			{
				red = src_row[x * bpp];
				green =	src_row[x * bpp + 1];
				blue = src_row[x * bpp + 2];
			}
			insertcolor(red, green, blue);
		}
		/* tell the user what we're doing */
		if ((y % 10) == 0)
			gimp_progress_update((double) y / (double) (y2 - y1));
	}
	/* clean up */
	free(src_row);
}

/* here's were we actually store our color-table */
static
void	insertcolor(guchar r, guchar g, guchar b)
{
	ColorNode	*node, *next = NULL, *prev = NULL,
			*newred, *newgreen = NULL, *newblue;
	ColorType	type = RED;

	histogram(r, g, b);
	/* lets walk the tree, and see if it already contains this color */
	for (node = table; node != NULL; prev = node, node = next)
	{
		if (node->color == RED)
		{
			if (node->r == r)
			{
				type = GREEN;
				next = node->next_axis;
			}
			else
			{
				type = RED;
				next = node->next_neighbour;
			}
			continue;
		}
		if (node->color == GREEN)
		{
			if (node->g == g)
			{
				type = BLUE;
				next = node->next_axis;
			}
			else
			{
				type = GREEN;
				next = node->next_neighbour;
			}
			continue;
		}
		if (node->color == BLUE)
		{
			/* found it! */
			if (node->b == b)
				break;
			else
			{
				type = BLUE;
				next = node->next_neighbour;
			}
		}
	}
	/* this color was already stored -> update it's count */
	if (node)
	{
		node->count++;
		return;
	}
	/* New color! */
	/* first, create blue node */
	CALLOC_COLOR(newblue);
	newblue->color = BLUE;
	/* no neighbours or links to another axis */
	newblue->next_neighbour = newblue->next_axis = NULL;
	/* 
	 * At the end of the list, we store the entire triplet.
	 * For now, there is no reason whatsoever to do this, but perhaps
	 * it might prove useful someday :)
	 */
	newblue->r = r;
	newblue->g = g;
	newblue->b = b;
	newblue->count = 1;
	/* previous was green: create link to axis */
	if (prev && prev->color == GREEN && type == BLUE)
		prev->next_axis = newblue;
	/* previous was blue: create link to neighbour */
	if (prev && prev->color == BLUE && type == BLUE)
		prev->next_neighbour = newblue;

	/* green node */
	if (type == GREEN || type == RED)
	{
		CALLOC_COLOR(newgreen);
		newgreen->color = GREEN;
		newgreen->next_neighbour =  NULL;
		newgreen->next_axis = newblue;
		newgreen->g = g;
		/* count doesn't matter here */
		newgreen->count = -1;
		/* previous was red: create link to axis */
		if (prev && prev->color == RED && type == GREEN)
			prev->next_axis = newgreen;
		/* previous was green: create link to neighbour */
		if (prev && prev->color == GREEN && type == GREEN)
			prev->next_neighbour = newgreen;
	}

	/* red node */
	if (type == RED)
	{
		CALLOC_COLOR(newred);
		newred->color = RED;
		newred->next_neighbour =  NULL;
		newred->next_axis = newgreen;
		newred->r = r;
		/* count doesn't matter here */
		newred->count = -1;
		/* previous was red, update its neighbour link */
		if (prev)
			prev->next_neighbour = newred;
		else
			table = newred;
	}

	/* increase the number of unique colors */
	uniques++;
}

/* 
 * Update RGB count, and keep track of maximum values (which aren't used 
 * anywhere as of yet, but they might be useful sometime).
 */
void	histogram(guchar r, guchar g, guchar b)
{
	if (++hist_red[r] > maxred)
		maxred = hist_red[r];
	if (++hist_green[g] > maxgreen)
		maxgreen = hist_green[g];
	if (++hist_blue[b] > maxblue)
		maxblue = hist_blue[b];
}

/* show our results */
static
int	doDialog()
{
	struct stat	st;
	GtkWidget	*dialog;
	GtkWidget	*button;
	GtkWidget	*frame;
	GtkWidget	*xframe;
	GtkWidget	*table;
	GtkWidget	*preview;
	gchar		**argv;
	gint		argc;
	int		filesize = 0;
	char		buf[512], *c;

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

	gtk_init(&argc, &argv);

	gtk_preview_set_gamma(gimp_gamma());
	gtk_preview_set_install_cmap(gimp_install_cmap());
	gtk_widget_set_default_visual(gtk_preview_get_visual());
	gtk_widget_set_default_colormap(gtk_preview_get_cmap());


	/* set up the dialog */
	dialog = gtk_dialog_new();
	gtk_window_set_title(GTK_WINDOW(dialog), "Colorcube analysis");
	gtk_window_position(GTK_WINDOW(dialog), GTK_WIN_POS_MOUSE);
	gtk_signal_connect(GTK_OBJECT(dialog), "destroy",
			   (GtkSignalFunc) close_callback,
			   NULL);

	/* lets create some buttons */
	button = gtk_button_new_with_label("Ok");
	GTK_WIDGET_SET_FLAGS(button, GTK_CAN_DEFAULT);
	gtk_signal_connect(GTK_OBJECT(button), "clicked",
			   (GtkSignalFunc) ok_callback,
			   dialog);
	gtk_box_pack_start(GTK_BOX(GTK_DIALOG(dialog)->action_area),
	                   button, TRUE, TRUE, 0);
	gtk_widget_grab_default(button);
	gtk_widget_show(button); 

	/* set up frame */
	frame = gtk_frame_new("Results");
	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(dialog)->vbox),
			   frame, TRUE, TRUE, 0);
	table = gtk_table_new(12, 1, FALSE);
	gtk_container_border_width(GTK_CONTAINER(table), 10);
	gtk_container_add(GTK_CONTAINER(frame), table);

	/* use preview for histogram window */
	preview = gtk_preview_new(GTK_PREVIEW_COLOR);
	gtk_preview_size(GTK_PREVIEW(preview), PREWIDTH, PREHEIGHT);
	xframe = gtk_frame_new(NULL);
	gtk_frame_set_shadow_type(GTK_FRAME(xframe), GTK_SHADOW_IN);
	gtk_container_add(GTK_CONTAINER(xframe), preview);
	fillPreview(preview);
	gtk_widget_show(preview);
	gtk_widget_show(xframe);
	gtk_table_attach(GTK_TABLE(table), xframe, 0, 1, 0, 1, GTK_EXPAND, GTK_EXPAND, 0, 0);
	gtk_widget_show(frame);

	/* output results */
	sprintf(buf, "Name: %s", filename);
	doLabel(table, buf);
	sprintf(buf, "Image dimensions: %ux%u", width, height);
	doLabel(table, buf);
	sprintf(buf, "Uncompressed size in bytes: %u", width * height * bpp);
	doLabel(table, buf);
	if (filename && !stat(filename, &st))
	{
		filesize = st.st_size;
		sprintf(buf, "Compressed size in bytes: %u", filesize);
		doLabel(table, buf);
		sprintf(buf, "Compression ratio (approx.): %u to 1", (int) rint((double) (width * height * bpp) / filesize));
		doLabel(table, buf);
	}
	if (!uniques)
		strcpy(buf, "No colors (??)");
	else
	if (uniques == 1)
		strcpy(buf, "Only one unique color");
	else
		sprintf(buf, "Number of unique colors: %u", uniques);
	doLabel(table, buf);
	
	/* show stuff */
	gtk_widget_show(table);
	gtk_widget_show(dialog);
	gtk_main();
	gdk_flush();

	return running;
}

/* shortcut */
static
void	doLabel(GtkWidget *table, char *text)
{
	static		int	idx = 1;
	GtkWidget	*label;

	label = gtk_label_new(text);
	gtk_misc_set_alignment(GTK_MISC(label), 0.0, 0.5);
	gtk_table_attach(GTK_TABLE(table), label, 0, 1, idx, idx + 1, GTK_FILL, 0, 5, 0);
	gtk_widget_show(label);
	idx += 2;
}

/* fill our preview image with the color-histogram */
static
void	fillPreview(GtkWidget *preview)
{
	guchar	*image = g_malloc(PREWIDTH * PREHEIGHT * 3 * sizeof(guchar)),
		*pimage;
	int	x, y, rowstride;

	if (!image)
	{
		g_warning("Ick, couldn't malloc() for preview!\n");
		return;
	}
	bzero(image, PREWIDTH * PREHEIGHT * 3 * sizeof(guchar));
	rowstride = PREWIDTH * 3;
	for (x = 0; x < PREWIDTH; x++)
	{
		int	histcount, val;

		/*
		 * For every channel, calculate a logarithmic value, scale it,
		 * and build a one-pixel bar.
		 *  ... in the respective channel, preserving the other ones. --hb
		 */
		histcount = hist_red[x] ? hist_red[x] : 1;
		val = (int) (log((double) histcount) * (PREHEIGHT / 12));
		if (val > PREHEIGHT)
			val = PREHEIGHT;
		for (y = PREHEIGHT - 1; y > (PREHEIGHT - val); y--)
		{
			guchar	*pixel = image + (x * 3) + (y * rowstride);

			*pixel = 255;
		}

		histcount = hist_green[x] ? hist_green[x] : 1;
		val = (int) (log((double) histcount) * (PREHEIGHT / 12));
		if (val > PREHEIGHT)
			val = PREHEIGHT;
		for (y = PREHEIGHT - 1; y > (PREHEIGHT - val); y--)
		{
			guchar	*pixel = image + (x * 3) + (y * rowstride);

			*(pixel + 1) = 255;
		}

		histcount = hist_blue[x] ? hist_blue[x] : 1;
		val = (int) (log((double) histcount) * (PREHEIGHT / 12));
		if (val > PREHEIGHT)
			val = PREHEIGHT;
		for (y = PREHEIGHT - 1; y > (PREHEIGHT - val); y--)
		{
			guchar	*pixel = image + (x * 3) + (y * rowstride);

			*(pixel + 2) = 255;
		}
	}
	/* move our data into the preview image */
	for (pimage = image, y = 0; y < PREHEIGHT; y++)
	{
		gtk_preview_draw_row(GTK_PREVIEW(preview), pimage, 0, y, PREWIDTH);
		pimage += 3 * PREWIDTH;
	}
	free(image);
}

static
void	ok_callback(GtkWidget *widget, gpointer data)
{
	running = 1;
	gtk_widget_destroy(GTK_WIDGET(data));
}

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