/* degrain.c
 * A film grain removal plugin for Gimp 1.3
 * Copyright (C) 2002 - 2004 Stefan M Fendt and David Hodson
 * Processing code by Stefan M Fendt, gui and assistance by David Hodson.
 *
 * 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.
 */


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <libgimp/gimp.h>
#include <libgimp/gimpui.h>

/* set to 1 to print timing info */
#define TESTING 1

#if TESTING
#include <time.h>
#endif

#define PLUGIN_NAME "plug_in_filmdegrain"
#define PLUGIN_TITLE "Film Degrain"

static void query (void);
static void run   (const char *name, 
		   int n_params, 
		   const GimpParam * param, 
		   int *nreturn_vals, 
		   GimpParam ** return_vals);

static int degrain_params[] = { 15, 10, 20 };

static void start(gint32 image_id, GimpDrawable * drawable);
static int degrainDialog();

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

MAIN()

     static void query (void)
{
  static GimpParamDef args[] =
    {
      {GIMP_PDB_INT32,     "run_mode",     "Interactive"},
      {GIMP_PDB_IMAGE,     "image",        "Input image"},
      {GIMP_PDB_DRAWABLE,  "drawable",     "Input drawable"},
      {GIMP_PDB_INT32,     "red",          "Red grain level" },
      {GIMP_PDB_INT32,     "green",        "Green grain level" },
      {GIMP_PDB_INT32,     "blue",         "Blue grain level" },
    };
        
  static GimpParamDef *return_vals = NULL;
  static guint nargs = sizeof(args) / sizeof(args[0]);
  static guint nreturn_vals = 0;
  gimp_install_procedure
    (
     PLUGIN_NAME,
     "Film Grain reduction for Gimp 1.3 / 2.0",
     "Reduces film grain or video noise",
     "Stefan M. Fendt and David Hodson",
     "Stefan M. Fendt and David Hodson",
     "2002 - 2004",
     "<Image>/Filters/Enhance/Film Grain Remover...",
     "RGB",
     GIMP_PLUGIN,
     nargs,
     nreturn_vals,
     args,
     return_vals
     );

}

static void run   (const char *name, 
		   int n_params, 
		   const GimpParam * param, 
		   int *nreturn_vals, 
		   GimpParam ** return_vals)
{
  static GimpParam values[1];
  GimpRunMode run_mode;
  GimpPDBStatusType status = GIMP_PDB_SUCCESS;
        
  GimpDrawable *drawable;
  gint32 image_id;

  *nreturn_vals = 1;
  *return_vals  = values;

  run_mode = param[0].data.d_int32;
  image_id = param[1].data.d_image;
  drawable = gimp_drawable_get (param[2].data.d_drawable);

  switch (run_mode) {
  case GIMP_RUN_INTERACTIVE:
    gimp_get_data(PLUGIN_NAME, degrain_params);
    if (!degrainDialog()) {
      return;
    }
    break;

  case GIMP_RUN_NONINTERACTIVE:
    if (n_params != 6)
      status = GIMP_PDB_CALLING_ERROR;
    if (status == GIMP_PDB_SUCCESS) {
      degrain_params[0] = param[3].data.d_int32;
      degrain_params[1] = param[4].data.d_int32;
      degrain_params[2] = param[5].data.d_int32;
    }
    break;

  case GIMP_RUN_WITH_LAST_VALS:
    gimp_get_data(PLUGIN_NAME, degrain_params);
    break;
    
  default:
    break;
  }

  start (image_id, drawable);

  if (run_mode != GIMP_RUN_NONINTERACTIVE)
    gimp_displays_flush();
  if (run_mode == GIMP_RUN_INTERACTIVE)
    gimp_set_data(PLUGIN_NAME, degrain_params, 3 * sizeof(int));

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

/* idea lifted from convmatrix.c, code totally different */
/* get any rect which overlaps source image */
/* repeats edge pixels */
static void
gimp_pixel_rgn_get_rect_overlapping (GimpPixelRgn* src, guchar* dest,
				     gint srcX, gint srcY, gint srcW, gint srcH)
{
  int xx;
  int yy;
  guchar* dd;

  /* x1, x2 to y1, y2 is dest area inside source image */
  int x1 = 0;
  int x2 = srcW;
  int y1 = 0;
  int y2 = srcH;

  gint width  = src->drawable->width;
  gint height = src->drawable->height;
  gint bytes  = src->drawable->bpp;

  int dstStride = srcW * bytes;

  /* check request against image size */
  int xOK = ((srcX >= 0) && ((srcX + srcW) <= width));
  int yOK = ((srcY >= 0) && ((srcY + srcH) <= height));

  if (xOK && yOK) {
    /* region is entirely within source image */
    gimp_pixel_rgn_get_rect(src, dest, srcX, srcY, srcW, srcH);
    return;
  }

  /* clip y to image size */
  if (srcY < 0) y1 = - srcY;
  if ((srcY + srcH) > height) y2 = height - srcY;

  if (xOK) {

    /* region width is within image, top and/or bottom is out */  
    /* get central block */
    dd = dest + y1 * dstStride;
    gimp_pixel_rgn_get_rect(src, dd, srcX, srcY + y1, srcW, y2 - y1);

  } else {

    /* right and/or left is outside image */
    /* get centre block by rows */

    /* clip x to image size */
    if (srcX < 0) x1 = - srcX;
    if ((srcX + srcW) > width) x2 = width - srcX;

    /* get region inside image */
    dd = dest + y1 * dstStride + x1 * bytes;
    for (yy = y1; yy < y2; ++yy) {
      gimp_pixel_rgn_get_row(src, dd, srcX + x1, srcY + yy, x2 - x1);
      dd += dstStride;
    }

    /* copy lines out to right and left */
    for (yy = y1; yy < y2; ++yy) {
      dd = dest + yy * dstStride;
      for (xx = 0; xx < x1; ++xx) {
	memcpy(dd + xx * bytes, dd + x1 * bytes, bytes);
      }
      for (xx = x2; xx < srcW; ++xx) {
	memcpy(dd + xx * bytes, dd + (x2 - 1) * bytes, bytes);
      }
    }
  }

  /* copy lines above and below */
  dd = dest + y1 * dstStride;
  for (yy = 0; yy < y1; ++yy) {
    memcpy(dest + yy * dstStride, dd, dstStride);
  }
  dd = dest + (y2 - 1) * dstStride;
  for (yy = y2; yy < srcH; ++yy) {
    memcpy(dest + yy * dstStride, dd, dstStride);
  }
}

void start(gint32 image_id, GimpDrawable* drawable)
{
  int x, y, c, i;

  GimpPixelRgn srcPR, dstPR;
  guchar* pr;

  int index[49];
  int lastTileWidth = -1;

#if TESTING
  time_t startTime = clock();
#endif

  int w = drawable->width;
  int h = drawable->height;
  int tw = gimp_tile_width();
  int th = gimp_tile_height();

  int bpp = drawable->bpp;

  int nr_of_tiles = ((w + tw - 1)/tw) * ((h + th - 1)/th);
  int nr_processed = 0;

  gimp_pixel_rgn_init (&srcPR, drawable, 0, 0, w, h, FALSE, FALSE);
  gimp_pixel_rgn_init (&dstPR, drawable, 0, 0, w, h, TRUE, TRUE);

  gimp_tile_cache_ntiles (3 * (w + tw - 1) / tw);

  guchar* src = malloc((tw + 6) * (th + 6) * bpp);

  gimp_progress_init ("Film Grain Remover...");

  for (pr = gimp_pixel_rgns_register (1, &dstPR);
       pr != NULL;
       pr = gimp_pixel_rgns_process (pr)) {

      /* need a buffer three pixels beyond the destination tile all around */
      gimp_pixel_rgn_get_rect_overlapping
	(&srcPR, src, dstPR.x - 3, dstPR.y - 3, dstPR.w + 6, dstPR.h + 6);

      /* calculate indices for source pixels */
      if (lastTileWidth != dstPR.w) {
	int i = 0;
	for (y = 0; y < 7; ++y) {
	  for (x = 0; x < 7; ++x) {
	    index[i] = (y * (dstPR.w + 6) + x) * bpp;
	    ++i;
	  }
	}
	lastTileWidth = dstPR.w;
      }

      /* process tile */
      for (y = 0; y < dstPR.h; ++y) {
	for (x = 0; x < dstPR.w; ++x) {
	  for (c = 0; c < bpp; ++c) {

	    int pSq;
	    int deltaSq;
	    int mean;
	    int variance;
	    int delta;

	    guchar* d = dstPR.data + (bpp * x) + (dstPR.w * bpp * y) + c;
	    guchar* s = src + (bpp * x) + ((dstPR.w + 6) * bpp * y) + c;
  
	    int sum = 0;
	    int sumSq = 0;
	    for (i = 0; i < 49; ++i) {
	      sum += s[index[i]];
	      sumSq += s[index[i]] * s[index[i]];
	    }
	    mean = sum / 49;
	    variance = (sumSq - sum * sum / 49) / 48;

	    delta = s[index[24]] - mean;
	    deltaSq = delta * delta;

	    pSq = degrain_params[c]*degrain_params[c];

#define SHOWME 0
/* set to 1 to show processed areas */
#if SHOWME
	    if ((deltaSq <= variance) || (deltaSq >= pSq*2))
	      *d = 0;
	    else if (deltaSq < pSq)
	      *d = 255;
	    else
	      *d = 255 * (pSq - deltaSq) / pSq;
#else
	    if ((deltaSq <= variance) || (deltaSq >= pSq*2))
	      *d = s[index[24]];
	    else if (deltaSq < pSq)
	      *d = mean;
	    else
	      *d = mean + (deltaSq - pSq) * delta / pSq;
#endif
	  }
	}
      }
      ++nr_processed;
      gimp_progress_update((float)nr_processed/(float)nr_of_tiles);
    }

  gimp_drawable_flush        (drawable);
  gimp_drawable_merge_shadow (drawable->drawable_id, TRUE);
  gimp_drawable_update       (drawable->drawable_id, 0, 0, w, h);
  gimp_displays_flush();
  gimp_drawable_detach(drawable);

  free(src);

#if TESTING
  printf("Elapsed time %f sec\n", (float)(clock() - startTime) / CLOCKS_PER_SEC);
#endif
}

static void
adjustmentChanged(GtkAdjustment *adjust, gpointer data) {
  int which = (int)data;
  degrain_params[which] = adjust->value;
}

static GtkAdjustment*
addControl(GtkWidget* vbox, const char* name, int index) {

  GtkWidget* label;
  GtkWidget* hbox;
  GtkWidget* hbox2;
  GtkWidget* slider;
  GtkWidget* spinner;
  GtkAdjustment* adjustment;
  gint width;

  label = gtk_label_new(name);

  adjustment = GTK_ADJUSTMENT(gtk_adjustment_new(degrain_params[index], 0, 99, 1, 1, 1));

  slider = gtk_hscale_new(adjustment);
  gtk_scale_set_digits(GTK_SCALE(slider), 0);
  gtk_scale_set_draw_value(GTK_SCALE(slider), FALSE);

  spinner = gtk_spin_button_new(adjustment, 0.0, 0);
  /* Need sensible width for value display */
  /* Add a small amount to accommodate the arrows */
  /* Of course, this should be done automagically */
  width = gdk_string_width(gtk_style_get_font(GTK_STYLE(gtk_widget_get_style(spinner))), "99") + 25;
  gtk_widget_set_usize(GTK_WIDGET(spinner), width, 0);

  hbox = gtk_hbox_new(TRUE, 2);
  hbox2 = gtk_hbox_new(TRUE, 2);
  gtk_box_pack_start(GTK_BOX(hbox2), label, TRUE, TRUE, 0);
  gtk_box_pack_start(GTK_BOX(hbox2), spinner, TRUE, TRUE, 0);
  gtk_box_pack_start(GTK_BOX(hbox), hbox2, TRUE, TRUE, 0);
  gtk_box_pack_start(GTK_BOX(hbox), slider, TRUE, TRUE, 0);

  gtk_signal_connect(GTK_OBJECT(adjustment), "value_changed",
		     GTK_SIGNAL_FUNC(adjustmentChanged),
		     (gpointer)index);

  gtk_box_pack_start(GTK_BOX(vbox), hbox, TRUE, FALSE, 0);

  return adjustment;
}

static int
degrainDialog()
{
  GtkWidget* dialog;
  GtkWidget* vbox;

  int usesPreview = FALSE;
  GtkDialogFlags flags = (GtkDialogFlags)0;

  gimp_ui_init(PLUGIN_NAME, usesPreview);

  dialog = gimp_dialog_new(
			   PLUGIN_TITLE,
			   PLUGIN_NAME,
			   0, flags, 0, 0,
			   GTK_STOCK_QUIT, 0,
			   GTK_STOCK_OK, 1,
			   0);

  vbox = GTK_DIALOG(dialog)->vbox;

  addControl(vbox, "Red", 0);
  addControl(vbox, "Green", 1);
  addControl(vbox, "Blue", 2);

  gtk_widget_show_all(dialog);

  return gimp_dialog_run(GIMP_DIALOG(dialog));

  /* gdk_flush(); */
}

