/* Selective gaussian blur filter for the GIMP, version 0.1
 * Adapted from the original gaussian blur filter by Spencer Kimball and
 * Peter Mattis.
 *
 * Copyright (C) 1995 Spencer Kimball and Peter Mattis
 * Copyright (C) 1999 Thom van Os <thom@vanos.com>
 *
 * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 *
 *
 * Changelog:
 *
 * v0.1 	990202, TVO
 * 	First release
 *
 * To do:
 *	- support for horizontal or vertical only blur
 *	- use memory more efficiently, smaller regions at a time
 *	- integrating with other convolution matrix based filters ?
 *	- create more selective and adaptive filters
 *	- threading
 *	- optimization
 *
 */
#include "config.h"

#include <stdio.h>
#include <stdlib.h>

#include <gtk/gtk.h>

#include <libgimp/gimp.h>
#include <libgimp/gimpui.h>

#include "libgimp/stdplugins-intl.h"

typedef struct
{
  gdouble radius;
  gint    maxdelta;
} BlurValues;

typedef struct
{
  gint run;
} BlurInterface;


/* Declare local functions.
 */
static void   query  (void);
static void   run    (gchar     *name,
		      gint       nparams,
		      GParam     *param,
		      gint      *nreturn_vals,
		      GParam   **return_vals);

static void   sel_gauss (GDrawable *drawable,
			 gdouble    radius,
			 gint       maxdelta);

static gint   sel_gauss_dialog      (void);
static void   sel_gauss_ok_callback (GtkWidget *widget,
				     gpointer   data);

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

static BlurValues bvals =
{
  5.0, /* radius   */
  50   /* maxdelta */
};

static BlurInterface bint =
{
  FALSE  /* run */
};

MAIN ()

static void
query (void)
{
  static GParamDef args[] =
  {
    { PARAM_INT32, "run_mode", "Interactive, non-interactive" },
    { PARAM_IMAGE, "image", "Input image (unused)" },
    { PARAM_DRAWABLE, "drawable", "Input drawable" },
    { PARAM_FLOAT, "radius", "Radius of gaussian blur (in pixels > 1.0)" },
    { PARAM_INT32, "maxdelta", "Maximum delta" },
  };
  static GParamDef *return_vals = NULL;
  static gint nargs = sizeof (args) / sizeof (args[0]);
  static gint nreturn_vals = 0;

  INIT_I18N();

  gimp_install_procedure ("plug_in_sel_gauss",
			  "Applies a selective gaussian blur to the specified drawable.",
			  "This filter functions similar to the regular gaussian blur filter except that neighbouring pixels that differ more than the given maxdelta parameter will not be blended with. This way with the correct parameters, an image can be smoothed out without losing details. However, this filter can be rather slow.",
			  "Thom van Os",
			  "Thom van Os",
			  "1999",
			  N_("<Image>/Filters/Blur/Selective Gaussian Blur..."),
			  "RGB*, GRAY*",
			  PROC_PLUG_IN,
			  nargs, nreturn_vals,
			  args, return_vals);
}

static void
run (gchar   *name,
     gint     nparams,
     GParam  *param,
     gint    *nreturn_vals,
     GParam **return_vals)
{
  static GParam	values[1];
  GRunModeType	run_mode;
  GStatusType	status = STATUS_SUCCESS;
  GDrawable	*drawable;
  gdouble	radius;

  run_mode = param[0].data.d_int32;

  *nreturn_vals = 1;
  *return_vals = values;

  INIT_I18N_UI(); 

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

  switch (run_mode)
    {
    case RUN_INTERACTIVE:
      /* Possibly retrieve data */
      gimp_get_data ("plug_in_sel_gauss", &bvals);

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

    case RUN_NONINTERACTIVE:
      /* Make sure all the arguments are there! */
      if (nparams != 7)
	status = STATUS_CALLING_ERROR;
      if (status == STATUS_SUCCESS)
	{
	  bvals.radius   = param[3].data.d_float;
	  bvals.maxdelta = CLAMP (param[4].data.d_int32, 0, 255);
	}
      if (status == STATUS_SUCCESS && (bvals.radius < 1.0))
	status = STATUS_CALLING_ERROR;
      break;

    case RUN_WITH_LAST_VALS:
      /* Possibly retrieve data */
      gimp_get_data ("plug_in_sel_gauss", &bvals);
      break;

    default:
      break;
    }

  if (status != STATUS_SUCCESS)
    {
      values[0].data.d_status = status;
      return;
    }

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

  /* Make sure that the drawable is gray or RGB color */
  if (gimp_drawable_is_rgb (drawable->id) ||
      gimp_drawable_is_gray (drawable->id))
    {
      gimp_progress_init (_("Selective Gaussian Blur"));

      radius = fabs (bvals.radius) + 1.0;

      /* run the gaussian blur */
      sel_gauss (drawable, radius, bvals.maxdelta);

      /* Store data */
      if (run_mode == RUN_INTERACTIVE)
	gimp_set_data ("plug_in_sel_gauss",
		       &bvals, sizeof (BlurValues));

      if (run_mode != RUN_NONINTERACTIVE)
	gimp_displays_flush ();
    }
  else
    {
      gimp_message (_("sel_gauss: Cannot operate on indexed color images"));
      status = STATUS_EXECUTION_ERROR;
    }

  gimp_drawable_detach (drawable);
  values[0].data.d_status = status;
}

static gint
sel_gauss_dialog (void)
{
  GtkWidget *dlg;
  GtkWidget *frame;
  GtkWidget *table;
  GtkWidget *spinbutton;
  GtkObject *adj;
  gchar **argv;
  gint    argc;

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

  gtk_init (&argc, &argv);
  gtk_rc_parse (gimp_gtkrc ());

  dlg = gimp_dialog_new (_("Selective Gaussian Blur"), "sel_gauss",
			 gimp_plugin_help_func, "filters/sel_gauss.html",
			 GTK_WIN_POS_MOUSE,
			 FALSE, TRUE, FALSE,

			 _("OK"), sel_gauss_ok_callback,
			 NULL, NULL, NULL, TRUE, FALSE,
			 _("Cancel"), gtk_widget_destroy,
			 NULL, 1, NULL, FALSE, TRUE,

			 NULL);

  gtk_signal_connect (GTK_OBJECT (dlg), "destroy",
		      GTK_SIGNAL_FUNC (gtk_main_quit),
		      NULL);

  /* parameter settings */
  frame = gtk_frame_new (_("Parameter Settings"));
  gtk_frame_set_shadow_type (GTK_FRAME (frame), GTK_SHADOW_ETCHED_IN);
  gtk_container_border_width (GTK_CONTAINER (frame), 6);
  gtk_box_pack_start (GTK_BOX (GTK_DIALOG (dlg)->vbox), frame,
		      TRUE, TRUE, 0);

  table = gtk_table_new (2, 3, FALSE);
  gtk_table_set_col_spacings (GTK_TABLE (table), 4);
  gtk_table_set_row_spacings (GTK_TABLE (table), 2);
  gtk_container_set_border_width (GTK_CONTAINER (table), 4);
  gtk_container_add (GTK_CONTAINER (frame), table);

  spinbutton = gimp_spin_button_new (&adj,
				     bvals.radius, 1.0, G_MAXINT, 1.0, 5.0,
				     0, 1, 2);
  gimp_table_attach_aligned (GTK_TABLE (table), 0, 0,
			     _("Blur Radius:"), 1.0, 0.5,
			     spinbutton, 1, TRUE);
  gtk_signal_connect (GTK_OBJECT (adj), "value_changed",
		      GTK_SIGNAL_FUNC (gimp_double_adjustment_update),
		      &bvals.radius);

  adj = gimp_scale_entry_new (GTK_TABLE (table), 0, 1,
			      _("Max. Delta:"), 128, 0,
			      bvals.maxdelta, 0, 255, 1, 8, 0,
			      FALSE, FALSE);
  gtk_signal_connect (GTK_OBJECT (adj), "value_changed",
		      GTK_SIGNAL_FUNC (gimp_int_adjustment_update),
		      &bvals.maxdelta);

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

  gtk_main ();
  gdk_flush ();

  return bint.run;
}

void
init_matrix (gdouble   radius,
	     gdouble **mat,
	     gint      num)
{
  int dx, dy;
  gdouble sd, c1, c2;

  /* This formula isn't really correct, but it'll do */
  sd = radius / 3.329042969;
  c1 = 1.0 / sqrt(2.0 * G_PI * sd);
  c2 = -2.0 * (sd * sd);

  for (dy=0; dy<num; dy++)
    {
      for (dx=dy; dx<num; dx++)
	{
	  mat[dx][dy] = c1 * exp(((dx*dx)+ (dy*dy))/ c2);
	  mat[dy][dx] = mat[dx][dy];
	}
    }
}

static void
matrixmult (guchar   *src,
	    guchar   *dest,
	    gint      width,
	    gint      height,
	    gdouble **mat,
	    gint      numrad,
	    gint      bytes,
	    gint      has_alpha,
	    gint      maxdelta)
{
  gint i, j, b, nb, x, y;
  gint six, dix, tmp;
  gdouble sum, fact, d, alpha=1.0;

  nb = bytes - (has_alpha?1:0);

  for (y = 0; y< height; y++)
    {
      for (x = 0; x< width; x++)
	{
	  dix = (bytes*((width*y)+x));
	  if (has_alpha)
	    dest[dix+nb] = src[dix+nb];
	  for (b=0; b<nb; b++)
	    {
	      sum = 0.0;
	      fact = 0.0;
	      for (i= 1-numrad; i<numrad; i++)
		{
		  if (((x+i)< 0) || ((x+i)>= width))
		    continue;
		  for (j= 1-numrad; j<numrad; j++)
		    {
		      if (((y+j)<0)||((y+j)>=height))
			continue;
		      six = (bytes*((width*(y+j))+x+i));
		      if (has_alpha)
			{
			  if (!src[six+nb])
			    continue;
			  alpha = (double)src[six+nb] / 255.0;
			}
		      tmp = src[dix+b] - src[six+b];
		      if ((tmp>maxdelta)||
			  (tmp<-maxdelta))
			continue;
		      d = mat[ABS(i)][ABS(j)];
		      if (has_alpha)
			{
			  d *= alpha;
			}
		      sum += d * src[six+b];
		      fact += d;
		    }
		}
	      if (fact == 0.0)
		dest[dix+b] = src[dix+b];
	      else
		dest[dix+b] = sum/fact;
	    }
	}
      if (!(y % 5))
	gimp_progress_update((double)y / (double)height);
    }
}

static void
sel_gauss (GDrawable *drawable,
	   gdouble    radius,
	   gint       maxdelta)
{
  GPixelRgn src_rgn, dest_rgn;
  gint width, height;
  gint bytes;
  gint has_alpha;
  guchar *dest;
  guchar *src;
  gint x1, y1, x2, y2;
  gint i;
  gdouble **mat;
  gint numrad;

  gimp_drawable_mask_bounds (drawable->id, &x1, &y1, &x2, &y2);

  width  = (x2 - x1);
  height = (y2 - y1);
  bytes  = drawable->bpp;
  has_alpha = gimp_drawable_has_alpha(drawable->id);

  if ((width < 1) || (height < 1) || (bytes < 1))
    return;

  numrad = (gint)(radius + 1.0);
  mat = (gdouble **) g_malloc (numrad * sizeof(gdouble *));
  for (i=0; i<numrad; i++)
    mat[i] = (gdouble *) g_malloc (numrad * sizeof(gdouble));
  init_matrix(radius, mat, numrad);

  src = (guchar *) g_malloc (width * height * bytes);
  dest = (guchar *) g_malloc (width * height * bytes);

  gimp_pixel_rgn_init (&src_rgn, drawable, x1, y1, drawable->width,
		       drawable->height, FALSE, FALSE);

  gimp_pixel_rgn_get_rect (&src_rgn, src, x1, y1, width, height);

  matrixmult (src, dest, width, height, mat, numrad,
	      bytes, has_alpha, maxdelta);

  gimp_pixel_rgn_init (&dest_rgn, drawable, x1, y1, width, height,
		       TRUE, TRUE);
  gimp_pixel_rgn_set_rect (&dest_rgn, dest, x1, y1, width, height);

  /*  merge the shadow, update the drawable  */
  gimp_drawable_flush (drawable);
  gimp_drawable_merge_shadow (drawable->id, TRUE);
  gimp_drawable_update (drawable->id, x1, y1, width, height);

  /* free up buffers */
  g_free (src);
  g_free (dest);
  for (i=0; i<numrad; i++)
    g_free (mat[i]);
  g_free (mat);
}

/*  Gauss interface functions  */

static void
sel_gauss_ok_callback (GtkWidget *widget,
		       gpointer   data)
{
  bint.run = TRUE;

  gtk_widget_destroy (GTK_WIDGET (data));
}
