/* hpf.c
 * A high pass filter plugin for Gimp 2.x
 * Smooths overall brightness of an image
 * Copyright (C) 2004-2008 David Hodson hodsond@ozemail.com.au
 *
 * 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.
 */
/* version 1.1 - handle alpha, fix undo
 * version 1.2 - fix reported crashes in Gimp 2.4 / gcc something-or-other
 */

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

#define TESTING 0

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

#define PLUGIN_NAME "plug_in_hpf"
#define PLUGIN_TITLE "High Pass Filter"

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

typedef struct {
  int radius;
  int brightness;
} HpfParams;
static HpfParams hpfParams = { 100, 128 };
static gint32 do_it = FALSE;

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

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,     "radius",       "Smoothing radius" },
		{GIMP_PDB_INT32,     "brightness",   "Average brightness" },
        };
        
        static GimpParamDef *return_vals = NULL;
        static guint nargs = sizeof(args) / sizeof(args[0]);
        static guint nreturn_vals = 0;

        gimp_install_procedure
        (
	        PLUGIN_NAME,
                "High pass filter v1.2",
                "Smooths out average brightness of image",
                "David Hodson",
                "David Hodson",
                "2004-2008",
                "<Image>/Filters/Enhance/High Pass Filter...",
                "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, &hpfParams);
            hpfDialog();
            if (!do_it) {
	        return;
            }
            break;

        case GIMP_RUN_NONINTERACTIVE:
            if (n_params != 5)
	        status = GIMP_PDB_CALLING_ERROR;
            if (status == GIMP_PDB_SUCCESS) {
	        hpfParams.radius = param[3].data.d_int32;
	        hpfParams.brightness = param[4].data.d_int32;
	    }
            break;

        case GIMP_RUN_WITH_LAST_VALS:
            gimp_get_data(PLUGIN_NAME, &hpfParams);
            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, &hpfParams, sizeof(HpfParams));

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

void start(gint32 image_id, GimpDrawable* drawable)
{
  int offset = hpfParams.brightness * 3;

  int blurId;
  GimpDrawable* blur;

  GimpParam* returns;
  int numReturns;
  GimpParam params[6];
  int success;

  float progress;
  float progressStep;
  int progressChunk;

  gint32 x1, y1, x2, y2;
  GimpPixelRgn srcRegion, blurRegion, dstRegion;
  guchar* pr;

  int x, y;
  int bpp;
  gboolean undoOK = FALSE;

  undoOK = gimp_image_undo_group_start(image_id);


#if TESTING
  time_t startTime;
#endif

  blurId = gimp_layer_copy(drawable->drawable_id);
  gimp_image_add_layer(image_id, blurId, 0);
  blur = gimp_drawable_get(blurId);

  params[0].type = GIMP_PDB_INT32;
  params[0].data.d_int32 = GIMP_RUN_NONINTERACTIVE;
  params[1].type = GIMP_PDB_IMAGE;
  params[1].data.d_image = image_id;
  params[2].type = GIMP_PDB_DRAWABLE;
  params[2].data.d_drawable = blurId;
  params[3].type = GIMP_PDB_FLOAT;
  params[3].data.d_float = hpfParams.radius;
  params[4].type = GIMP_PDB_INT32;
  params[4].data.d_int32 = 1;
  params[5].type = GIMP_PDB_INT32;
  params[5].data.d_int32 = 1;

  returns =
    gimp_run_procedure2("plug_in_gauss_iir", &numReturns, 6, params);
  success = returns &&
    (numReturns > 0) &&
    (returns[0].type == GIMP_PDB_STATUS) &&
    (returns[0].data.d_status == GIMP_PDB_SUCCESS);
  gimp_destroy_params(returns, numReturns);
  if (! success) {
    gimp_drawable_detach(blur);
    gimp_image_remove_layer(image_id, blurId);
    if (undoOK) {
      gimp_image_undo_group_end(image_id);
    }
    return;
  }

  gimp_drawable_mask_bounds(drawable->drawable_id, &x1, &y1, &x2, &y2);
  bpp = gimp_drawable_bpp(drawable->drawable_id);

#if 0
  has_alpha = gimp_drawable_has_alpha (drawable->drawable_id);
  alpha = (has_alpha) ? drawable->bpp - 1 : drawable->bpp;
#endif

  /* Initialize progress */
  progress = 0;
  progressStep = 1.0 / ((x2 - x1) * (y2 - y1));
  progressChunk = 0;
  gimp_progress_init(PLUGIN_TITLE);

  gimp_pixel_rgn_init(&srcRegion, drawable,
    x1, y1, (x2 - x1), (y2 - y1), FALSE, FALSE);
  gimp_pixel_rgn_init(&blurRegion, blur,
    x1, y1, (x2 - x1), (y2 - y1), FALSE, FALSE);
  gimp_pixel_rgn_init(&dstRegion, drawable,
    x1, y1, (x2 - x1), (y2 - y1), TRUE, TRUE);

  pr = gimp_pixel_rgns_register (3, &srcRegion, &blurRegion, &dstRegion);

#if TESTING
  startTime = clock();
#endif

  for ( ; pr != 0; pr = gimp_pixel_rgns_process(pr)) {
    guchar* src = srcRegion.data;
    guchar* blur = blurRegion.data;
    guchar* dst = dstRegion.data;

    for (y = 0; y < srcRegion.h; ++y) {
      guchar* s = src;
      guchar* b = blur;
      guchar* d = dst;

      for (x = 0; x < srcRegion.w; ++x) {

        int newR, newG, newB;
        int total = b[0] + b[1] + b[2];
	if (total) {
	  newR = s[0] + b[0] * offset / total - b[0];
	  newG = s[1] + b[1] * offset / total - b[1];
	  newB = s[2] + b[2] * offset / total - b[2];
	} else {
	  newR = s[0] + hpfParams.brightness;
	  newG = s[1] + hpfParams.brightness;
	  newB = s[2] + hpfParams.brightness;
	}
	d[0] = (newR < 0) ? 0 : (newR > 255) ? 255 : newR;
	d[1] = (newG < 0) ? 0 : (newG > 255) ? 255 : newG;
	d[2] = (newB < 0) ? 0 : (newB > 255) ? 255 : newB;

        s += srcRegion.bpp;
        b += blurRegion.bpp;
        d += dstRegion.bpp;
      }

      src += srcRegion.rowstride;
      blur += blurRegion.rowstride;
      dst += dstRegion.rowstride;

    }

#if 1
    /* this is the slowest part of the whole process!! */
    progress += progressStep * srcRegion.w * srcRegion.h;
    ++progressChunk;
    if (progressChunk == 16) {
      gimp_progress_update(progress);
      progressChunk = 0;
    }
#endif
  }

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

  gimp_image_remove_layer(image_id, blurId);
  gimp_drawable_flush(drawable);
  gimp_drawable_merge_shadow(drawable->drawable_id, TRUE);
/* I think using region iterators makes this unnecessary */
/*  gimp_drawable_update(drawable->drawable_id, x1, y1, x2 - x1, y2 - y1); */
  gimp_drawable_detach(drawable);
  if (undoOK) {
    gimp_image_undo_group_end(image_id);
  }
  gimp_displays_flush();
}

static void
adjustmentChanged(GtkAdjustment *adjust, gpointer data) {
  int* param = (int*)data;
  *param = adjust->value;
}

static GtkAdjustment*
addControl(GtkWidget* vbox, const char* name, int* data, int min, int max) {

  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(*data, min, max, 1, 1, 0));

  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_widget_get_style(spinner)), "-100.0") + 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)data);

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

  return adjustment;
}

static void
hpfDialog(void)
{
  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,
    NULL);

  vbox = GTK_DIALOG(dialog)->vbox;

  addControl(vbox, "Radius", &hpfParams.radius, 1, 1000);
  addControl(vbox, "Brightness", &hpfParams.brightness, 0, 255);

  gtk_widget_show_all(dialog);

  if (gimp_dialog_run(GIMP_DIALOG(dialog))) {
    do_it = TRUE;
  }
}
