/* Convolution Matrix plug-in for GIMP -- Version 0.1
 * Copyright (C) 1997 Lauri Alanko <la@iki.fi>
 *
 *
 * 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 3 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, see <http://www.gnu.org/licenses/>.
 */

#include "config.h"

#include <stdlib.h>
#include <string.h>

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

#include "libgimp/stdplugins-intl.h"


#define PLUG_IN_PROC   "plug-in-convmatrix"
#define PLUG_IN_BINARY "convolution-matrix"
#define PLUG_IN_ROLE   "gimp-convolution-matrix"

#define RESPONSE_RESET 1


#define BIG_MATRIX  /* toggle for 11x11 matrix code experimental*/
#undef BIG_MATRIX


#ifndef BIG_MATRIX
#define MATRIX_SIZE   (5)
#else
#define MATRIX_SIZE   (11)
#endif

#define HALF_WINDOW   (MATRIX_SIZE/2)
#define MATRIX_CELLS  (MATRIX_SIZE*MATRIX_SIZE)
#define DEST_ROWS     (MATRIX_SIZE/2 + 1)
#define CHANNELS      (5)
#define BORDER_MODES  (3)



typedef enum
{
  EXTEND,
  WRAP,
  CLEAR
} BorderMode;

static gchar * const channel_labels[] =
{
  N_("Gr_ey"),
  N_("Re_d"),
  N_("_Green"),
  N_("_Blue"),
  N_("_Alpha")
};

static gchar * const bmode_labels[] =
{
  N_("E_xtend"),
  N_("_Wrap"),
  N_("Cro_p")
};

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

static gboolean  convolve_image_dialog (GimpDrawable  *drawable);

static void      convolve_image        (GimpDrawable  *drawable,
                                        GimpPreview   *preview);

static void      check_config          (GimpDrawable  *drawable);

static gfloat    convolve_pixel        (guchar       **src_row,
                                        gint           x_offset,
                                        gint           channel,
                                        GimpDrawable  *drawable);

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

static gboolean run_flag = FALSE;

typedef struct
{
  gfloat     matrix[MATRIX_SIZE][MATRIX_SIZE];
  gfloat     divisor;
  gfloat     offset;
  gint       alpha_weighting;
  BorderMode bmode;
  gboolean   channels[CHANNELS];
  gboolean   autoset;
} config_struct;

#ifndef BIG_MATRIX
static const config_struct default_config =
{
  {
    { 0.0, 0.0, 0.0, 0.0, 0.0 },
    { 0.0, 0.0, 0.0, 0.0, 0.0 },
    { 0.0, 0.0, 1.0, 0.0, 0.0 },
    { 0.0, 0.0, 0.0, 0.0, 0.0 },
    { 0.0, 0.0, 0.0, 0.0, 0.0 }
  },                 /* matrix */
  1,                 /* divisor */
  0,                 /* offset */
  1,                 /* Alpha-handling algorithm */
  CLEAR,             /* border-mode */
  { TRUE, TRUE, TRUE, TRUE, TRUE }, /* Channels mask */
  FALSE,              /* autoset */
};

#else

static const config_struct default_config =
{
  {
    { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  },
    { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  },
    { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  },
    { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  },
    { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  },
    { 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0  },
    { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  },
    { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  },
    { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  },
    { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  },
    { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0  }
  },                 /* matrix */
  1,                 /* divisor */
  0,                 /* offset */
  1,                 /* Alpha-handling algorithm */
  CLEAR,             /* border-mode */
  { TRUE, TRUE, TRUE, TRUE, TRUE }, /* Channels mask */
  FALSE,              /* autoset */
};

#endif


static config_struct config;

struct
{
  GtkWidget *matrix[MATRIX_SIZE][MATRIX_SIZE];
  GtkWidget *divisor;
  GtkWidget *offset;
  GtkWidget *alpha_weighting;
  GtkWidget *bmode[BORDER_MODES];
  GtkWidget *channels[CHANNELS];
  GtkWidget *autoset;
} static widget_set;


MAIN ()

static void
query (void)
{
  static const GimpParamDef args[] =
  {
    { GIMP_PDB_INT32,      "run-mode",    "The run mode { RUN-INTERACTIVE (0), RUN-NONINTERACTIVE (1) }" },
    { GIMP_PDB_IMAGE,      "image",       "Input image (unused)" },
    { GIMP_PDB_DRAWABLE,   "drawable",    "Input drawable" },
    { GIMP_PDB_INT32,      "argc-matrix", "The number of elements in the following array. Should be always 25." },
    { GIMP_PDB_FLOATARRAY, "matrix",      "The 5x5 convolution matrix" },
    { GIMP_PDB_INT32,      "alpha-alg",   "Enable weighting by alpha channel" },
    { GIMP_PDB_FLOAT,      "divisor",     "Divisor" },
    { GIMP_PDB_FLOAT,      "offset",      "Offset" },

    { GIMP_PDB_INT32,      "argc-channels", "The number of elements in following array. Should be always 5." },
    { GIMP_PDB_INT32ARRAY, "channels",      "Mask of the channels to be filtered" },
    { GIMP_PDB_INT32,      "bmode",         "Mode for treating image borders { EXTEND (0), WRAP (1), CLEAR (2) }" },
  };

  gimp_install_procedure (PLUG_IN_PROC,
                          N_("Apply a generic 5x5 convolution matrix"),
                          "",
                          "Lauri Alanko",
                          "Lauri Alanko",
                          "1997",
                          N_("_Convolution Matrix..."),
                          "RGB*, GRAY*",
                          GIMP_PLUGIN,
                          G_N_ELEMENTS (args), 0,
                          args, NULL);

  gimp_plugin_menu_register (PLUG_IN_PROC, "<Image>/Filters/Generic");
}

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

  INIT_I18N ();

  *nreturn_vals = 1;
  *return_vals = values;

  run_mode = param[0].data.d_int32;

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

  /*  The plug-in is not able to handle images smaller than 3x3 pixels  */
  if (drawable->width < 3 || drawable->height < 3)
    {
      g_message (_("Convolution does not work on layers "
                   "smaller than 3x3 pixels."));
      status = GIMP_PDB_EXECUTION_ERROR;
      values[0].type = GIMP_PDB_STATUS;
      values[0].data.d_status = status;
      return;
    }

  config = default_config;
  if (run_mode == GIMP_RUN_NONINTERACTIVE)
    {
      if ((nparams != 11) && (nparams != 12))
        {
          status = GIMP_PDB_CALLING_ERROR;
        }
      else
        {
          if (param[3].data.d_int32 != MATRIX_CELLS)
            {
              status = GIMP_PDB_CALLING_ERROR;
            }
          else
            {
              for (y = 0; y < MATRIX_SIZE; y++)
                for (x = 0; x < MATRIX_SIZE; x++)
                  config.matrix[x][y]
                    = param[4].data.d_floatarray[x * MATRIX_SIZE + y];
            }

          config.alpha_weighting = param[5].data.d_int32;
          config.divisor         = param[6].data.d_float;
          config.offset          = param[7].data.d_float;

          if (param[8].data.d_int32 != CHANNELS)
            {
              status = GIMP_PDB_CALLING_ERROR;
            }
          else
            {
              for (y = 0; y < CHANNELS; y++)
                config.channels[y] = param[9].data.d_int32array[y];
            }

          config.bmode = param[10].data.d_int32;

          check_config (drawable);
        }
    }
  else
    {
      gimp_get_data (PLUG_IN_PROC, &config);

      if (run_mode == GIMP_RUN_INTERACTIVE)
        {
          /*  Oh boy. We get to do a dialog box, because we can't really
           *  expect the user to set us up with the right values using gdb.
           */
          check_config (drawable);

          if (! convolve_image_dialog (drawable))
            {
              /* The dialog was closed, or something similarly evil happened. */
              status = GIMP_PDB_EXECUTION_ERROR;
            }
        }
    }

  if (status == GIMP_PDB_SUCCESS)
    {
      /*  Make sure that the drawable is gray or RGB color  */
      if (gimp_drawable_is_rgb (drawable->drawable_id) ||
          gimp_drawable_is_gray (drawable->drawable_id))
        {
          gimp_progress_init (_("Applying convolution"));
          gimp_tile_cache_ntiles (2 * (drawable->width /
                                  gimp_tile_width () + 1));
          convolve_image (drawable, NULL);

          if (run_mode != GIMP_RUN_NONINTERACTIVE)
            gimp_displays_flush ();

          if (run_mode == GIMP_RUN_INTERACTIVE)
            gimp_set_data (PLUG_IN_PROC, &config, sizeof (config));
        }
      else
        {
          status = GIMP_PDB_EXECUTION_ERROR;
        }

      gimp_drawable_detach (drawable);
    }

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


/*  A generic wrapper to gimp_pixel_rgn_get_row which handles unlimited
 *  wrapping or gives the transparent regions outside the image
 *  fills additional bytes before and after image row to provide border modes.
 */

static void
my_get_row (GimpPixelRgn *PR,
            guchar       *dest,
            gint          x,
            gint          y,
            gint          w)
{
  gint width, height, bpp;
  gint i;

  width  = PR->drawable->width;
  height = PR->drawable->height;
  bpp  = PR->drawable->bpp;

  /* Y-wrappings */
  switch (config.bmode)
    {
    case WRAP:
      /* Wrapped, so we get the proper row from the other side */
      while (y < 0) /* This is the _sure_ way to wrap. :) */
        y += height;
      while (y >= height)
        y -= height;
      break;

    case CLEAR:
      /* Beyond borders, so set full transparent. */
      if (y < 0 || y >= height)
        {
          memset (dest, 0, w * bpp);
          return; /* Done, so back. */
        }

    case EXTEND:
      y = CLAMP (y , 0 , height - 1);
      break;
    }


  /* X-wrappings */
  switch (config.bmode)
    {
    case CLEAR:
      if (x < 0)
        {
          i = MIN (w, -x);
          memset (dest, 0, i * bpp);
          dest += i * bpp;
          w -= i;
          x += i;
        }
      if (w)
        {
          i = MIN (w, width);
          gimp_pixel_rgn_get_row (PR, dest, x, y, i);
          dest += i * bpp;
          w -= i;
          x += i;
        }
      if (w)
        memset (dest, 0, w * bpp);
      break;

    case WRAP:
      while (x < 0)
        x += width;
      i = MIN (w, width - x);
      gimp_pixel_rgn_get_row (PR, dest, x, y, i);
      w -= i;
      dest += i * bpp;
      x = 0;
      while (w)
        {
          i = MIN (w, width);
          gimp_pixel_rgn_get_row (PR, dest, x, y, i);
          w -= i;
          dest += i * bpp;
        }
      break;

    case EXTEND:
      if (x < 0)
        {
          gimp_pixel_rgn_get_pixel (PR, dest, 0, y);
          x++;
          w--;
          dest += bpp;

          while (x < 0 && w)
            {
              for (i = 0; i < bpp; i++)
                {
                  *dest = *(dest - bpp);
                  dest++;
                }
              x++;
              w--;
            }
        }
      if (w && width - x > 0)
        {
          i = MIN (w, width - x);
          gimp_pixel_rgn_get_row (PR, dest, x, y, i);
          w -= i;
          dest += i * bpp;
        }
      while (w)
        {
          for (i = 0; i < bpp; i++)
            {
              *dest= *(dest - bpp);
              dest++;
            }
          x++;
          w--;
        }
      break;

    }
}

static gfloat
convolve_pixel (guchar       **src_row,
                gint           x_offset,
                gint           channel,
                GimpDrawable  *drawable)
{
  static gfloat matrixsum = 0; /* FIXME: this certainly breaks the preview */
  static gint bpp         = 0;

  gfloat sum              = 0;
  gfloat alphasum         = 0;
  gint   x, y;
  gint   alpha_channel;

  if (!bpp)
    {
      bpp = drawable->bpp;

      for (y = 0; y < MATRIX_SIZE; y++)
        for (x = 0; x < MATRIX_SIZE; x++)
          matrixsum += ABS (config.matrix[x][y]);
    }

  alpha_channel = bpp - 1;

  for (y = 0; y < MATRIX_SIZE; y++)
    for (x = 0; x < MATRIX_SIZE; x++)
      {
        gfloat temp = config.matrix[x][y];

        if (channel != alpha_channel && config.alpha_weighting == 1)
          {
            temp *= src_row[y][x_offset + x * bpp + alpha_channel - channel];
            alphasum += ABS (temp);
          }

        temp *= src_row[y][x_offset + x * bpp];
        sum += temp;
      }

  sum /= config.divisor;

  if (channel != alpha_channel && config.alpha_weighting == 1)
    {
      if (alphasum != 0)
        sum = sum * matrixsum / alphasum;
      else
        sum = 0;
    }

  sum += config.offset;

  return sum;
}

static void
convolve_image (GimpDrawable *drawable,
                GimpPreview  *preview)
{
  GimpPixelRgn  srcPR, destPR;
  gint          width, height, row, col;
  gint          src_w, src_row_w, src_h, i;
  gint          src_x1, src_y1, src_x2, src_y2;
  gint          x1, x2, y1, y2;
  guchar       *dest_row[DEST_ROWS];
  guchar       *src_row[MATRIX_SIZE];
  guchar       *tmp_row;
  gint          x_offset;
  gboolean      chanmask[CHANNELS - 1];
  gint          bpp;
  gint          alpha_channel;

  /* 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).
   */
  if (preview)
    {
      gimp_preview_get_position (preview, &src_x1, &src_y1);
      gimp_preview_get_size (preview, &src_w, &src_h);
      src_x2 = src_x1 + src_w;
      src_y2 = src_y1 + src_h;
    }
  else
    {
      gimp_drawable_mask_bounds (drawable->drawable_id,
                                 &src_x1, &src_y1, &src_x2, &src_y2);
      src_w = src_x2 - src_x1;
      src_h = src_y2 - src_y1;
    }

  /* 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;
  alpha_channel = bpp - 1;

  if (gimp_drawable_is_rgb (drawable->drawable_id))
    {
      for (i = 0; i < CHANNELS - 1; i++)
        chanmask[i] = config.channels[i + 1];
    }
  else /* Grayscale */
    {
      chanmask[0] = config.channels[0];
    }

  if (gimp_drawable_has_alpha (drawable->drawable_id))
    chanmask[alpha_channel] = config.channels[4];

  src_row_w = src_w + HALF_WINDOW + HALF_WINDOW;

  for (i = 0; i < MATRIX_SIZE; i++)
    src_row[i] = g_new (guchar, src_row_w * bpp);

  for (i = 0; i < DEST_ROWS; i++)
    dest_row[i]= g_new (guchar, src_w * bpp);

  /*  initialize the pixel regions  */
  x1 = MAX (src_x1 - HALF_WINDOW, 0);
  y1 = MAX (src_y1 - HALF_WINDOW, 0);
  x2 = MIN (src_x2 + HALF_WINDOW, width);
  y2 = MIN (src_y2 + HALF_WINDOW, height);
  gimp_pixel_rgn_init (&srcPR, drawable,
                       x1, y1, x2 - x1, y2 - y1, FALSE, FALSE);
  gimp_pixel_rgn_init (&destPR, drawable,
                       src_x1, src_y1, src_w, src_h,
                       preview == NULL, TRUE);

  /* initialize source arrays */
  for (i = 0; i < MATRIX_SIZE; i++)
    my_get_row (&srcPR, src_row[i], src_x1 - HALF_WINDOW,
                src_y1 - HALF_WINDOW + i , src_row_w);

  for (row = src_y1; row < src_y2; row++)
    {
      gint channel;

      x_offset = 0;

      for (col = src_x1; col < src_x2; col++)
        for (channel = 0; channel < bpp; channel++)
          {
            guchar d;

            if (chanmask[channel])
              {
                gint result;

                result = ROUND (convolve_pixel (src_row,
                                                x_offset, channel, drawable));
                d = CLAMP (result, 0, 255);
              }
            else
              {
                /* copy unmodified pixel */
                d = src_row[HALF_WINDOW][x_offset + HALF_WINDOW * bpp];
              }

            dest_row[HALF_WINDOW][x_offset] = d;
            x_offset++;
          }

      if (row >= src_y1 + HALF_WINDOW)
        gimp_pixel_rgn_set_row (&destPR,
                                dest_row[0], src_x1, row - HALF_WINDOW, src_w);

      if (row < src_y2 - 1)
        {
          tmp_row = dest_row[0];

          for (i = 0; i < DEST_ROWS - 1; i++)
            dest_row[i] = dest_row[i + 1];

          dest_row[DEST_ROWS - 1] = tmp_row;

          tmp_row = src_row[0];

          for (i = 0; i < MATRIX_SIZE - 1; i++)
            src_row[i] = src_row[i + 1];

          src_row[MATRIX_SIZE-1] = tmp_row;

          my_get_row (&srcPR, src_row[MATRIX_SIZE - 1],
                      src_x1 - HALF_WINDOW, row + HALF_WINDOW + 1, src_row_w);
        }

      if ((row % 10 == 0) && !preview)
        gimp_progress_update ((double) (row - src_y1) / src_h);
    }

  /* put the remaining rows in the buffer in place */
  for (i = 1; i <  DEST_ROWS; i++)
    gimp_pixel_rgn_set_row (&destPR, dest_row[i],
                            src_x1, src_y2 + i - 1 - HALF_WINDOW, src_w);


  /*  update the region  */
  if (preview)
    {
      gimp_drawable_preview_draw_region (GIMP_DRAWABLE_PREVIEW (preview),
                                         &destPR);
    }
  else
    {
      gimp_progress_update (1.0);
      gimp_drawable_flush (drawable);
      gimp_drawable_merge_shadow (drawable->drawable_id, TRUE);
      gimp_drawable_update (drawable->drawable_id,
                            src_x1, src_y1, src_x2 - src_x1, src_y2 - src_y1);
    }

  for (i = 0; i < MATRIX_SIZE; i++)
    g_free (src_row[i]);

  for (i = 0; i < DEST_ROWS; i++)
    g_free (dest_row[i]);
}

/***************************************************
 * GUI stuff
 */

static void
redraw_matrix (void)
{
  gint  x, y;
  gchar buffer[12];

  for (y = 0; y < MATRIX_SIZE; y++)
    for (x = 0; x < MATRIX_SIZE; x++)
      {
        g_snprintf (buffer, sizeof (buffer), "%g", config.matrix[x][y]);
        gtk_entry_set_text (GTK_ENTRY (widget_set.matrix[x][y]), buffer);
      }
}

static void
redraw_channels (void)
{
  gint i;

  for (i = 0; i < CHANNELS; i++)
    if (widget_set.channels[i])
      gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (widget_set.channels[i]),
                                    config.channels[i]);
}

static void
redraw_autoset (void)
{
  gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (widget_set.autoset),
                                config.autoset);
}

static void
redraw_alpha_weighting (void)
{
  gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (widget_set.alpha_weighting),
                                config.alpha_weighting > 0);
}

static void
redraw_off_and_div (void)
{
  gchar buffer[12];

  g_snprintf (buffer, sizeof (buffer), "%g", config.divisor);
  gtk_entry_set_text (GTK_ENTRY (widget_set.divisor), buffer);

  g_snprintf (buffer, sizeof (buffer), "%g", config.offset);
  gtk_entry_set_text (GTK_ENTRY (widget_set.offset), buffer);
}

static void
redraw_bmode (void)
{
  gtk_toggle_button_set_active
    (GTK_TOGGLE_BUTTON (widget_set.bmode[config.bmode]), TRUE);
}

static void
redraw_all (void)
{
  redraw_matrix ();
  redraw_off_and_div ();
  redraw_autoset ();
  redraw_alpha_weighting ();
  redraw_bmode ();
  redraw_channels ();
}

static void
check_matrix (void)
{
  gint      x, y;
  gfloat    sum   = 0.0;

  for (y = 0; y < MATRIX_SIZE; y++)
    for (x = 0; x < MATRIX_SIZE; x++)
      sum += config.matrix[x][y];

  if (config.autoset)
    {
      if (sum > 0)
        {
          config.offset = 0;
          config.divisor = sum;
        }
      else if (sum < 0)
        {
          config.offset = 255;
          config.divisor = -sum;
        }
      else
        {
          config.offset = 128;
          /* The sum is 0, so this is probably some sort of
           * embossing filter. Should divisor be autoset to 1
           * or left undefined, ie. for the user to define? */
          config.divisor = 1;
        }
      redraw_off_and_div ();
    }
}

static void
response_callback (GtkWidget    *widget,
                   gint          response_id,
                   GimpDrawable *drawable)
{
  switch (response_id)
    {
    case RESPONSE_RESET:
      config = default_config;
      check_config (drawable);
      redraw_all ();
      break;

    case GTK_RESPONSE_OK:
      run_flag = TRUE;

    default:
      gtk_widget_destroy (GTK_WIDGET (widget));
      break;
    }
}

/* Checks that the configuration is valid for the image type */
static void
check_config (GimpDrawable *drawable)
{
  config.alpha_weighting = 0;

  if (!gimp_drawable_has_alpha (drawable->drawable_id))
    {
      config.alpha_weighting = -1;
      config.bmode           = EXTEND;
    }
}

static void
entry_callback (GtkWidget *widget,
                gpointer   data)
{
  gfloat *value = (gfloat *) data;

  *value = atof (gtk_entry_get_text (GTK_ENTRY (widget)));

  if (widget == widget_set.divisor)
    gtk_dialog_set_response_sensitive (GTK_DIALOG (gtk_widget_get_toplevel (widget)),
                                       GTK_RESPONSE_OK,
                                       (*value != 0.0));
  else if (widget != widget_set.offset)
    check_matrix ();
}


static void
my_toggle_callback (GtkWidget *widget,
                    gboolean  *data)
{
  gint val = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (widget));

  *data = val;

  if (widget == widget_set.alpha_weighting)
    {
      gtk_widget_set_sensitive (widget_set.bmode[CLEAR], val);
      if (val == 0 && config.bmode == CLEAR)
        {
          config.bmode = EXTEND;
          redraw_bmode ();
        }
    }
  else if (widget == widget_set.autoset)
    {
      gtk_widget_set_sensitive (widget_set.divisor, !val);
      gtk_widget_set_sensitive (widget_set.offset, !val);
      check_matrix ();
    }
}

static void
my_bmode_callback (GtkWidget *widget,
                   gpointer   data)
{
  config.bmode = GPOINTER_TO_INT (data) - 1;
}

static gboolean
convolve_image_dialog (GimpDrawable *drawable)
{
  GtkWidget *dialog;
  GtkWidget *main_vbox;
  GtkWidget *preview;
  GtkWidget *main_hbox;
  GtkWidget *table;
  GtkWidget *label;
  GtkWidget *entry;
  GtkWidget *button;
  GtkWidget *box;
  GtkWidget *inbox;
  GtkWidget *vbox;
  GtkWidget *frame;
  gint       x, y, i;
  GSList    *group;

  gimp_ui_init (PLUG_IN_BINARY, FALSE);

  dialog = gimp_dialog_new (_("Convolution Matrix"), PLUG_IN_ROLE,
                            NULL, 0,
                            gimp_standard_help_func, PLUG_IN_PROC,

                            GIMP_STOCK_RESET, RESPONSE_RESET,
                            GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
                            GTK_STOCK_OK,     GTK_RESPONSE_OK,

                            NULL);

  gtk_dialog_set_alternative_button_order (GTK_DIALOG (dialog),
                                           RESPONSE_RESET,
                                           GTK_RESPONSE_OK,
                                           GTK_RESPONSE_CANCEL,
                                           -1);

  gimp_window_set_transient (GTK_WINDOW (dialog));

  main_vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 12);
  gtk_container_set_border_width (GTK_CONTAINER (main_vbox), 12);
  gtk_box_pack_start (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dialog))),
                      main_vbox, TRUE, TRUE, 0);
  gtk_widget_show (main_vbox);

  preview = gimp_drawable_preview_new (drawable, NULL);
  gtk_box_pack_start (GTK_BOX (main_vbox), preview, TRUE, TRUE, 0);
  gtk_widget_show (preview);

  g_signal_connect_swapped (preview, "invalidated",
                            G_CALLBACK (convolve_image),
                            drawable);

  main_hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 12);
  gtk_box_pack_start (GTK_BOX (main_vbox), main_hbox, FALSE, FALSE, 0);
  gtk_widget_show (main_hbox),

  vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 12);
  gtk_box_pack_start (GTK_BOX (main_hbox), vbox, TRUE, TRUE, 0);

  frame = gimp_frame_new (_("Matrix"));
  gtk_box_pack_start (GTK_BOX (vbox), frame, FALSE, FALSE, 0);

  inbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 12);
  gtk_container_add (GTK_CONTAINER (frame), inbox);

  table = gtk_table_new (MATRIX_SIZE, MATRIX_SIZE, FALSE);
  gtk_table_set_row_spacings (GTK_TABLE (table), 4);
  gtk_table_set_col_spacings (GTK_TABLE (table), 4);
  gtk_box_pack_start (GTK_BOX (inbox), table, TRUE, TRUE, 0);

  for (y = 0; y < MATRIX_SIZE; y++)
    for (x = 0; x < MATRIX_SIZE; x++)
      {
        widget_set.matrix[x][y] = entry = gtk_entry_new ();
        gtk_widget_set_size_request (entry, 40, -1);
        gtk_table_attach (GTK_TABLE (table), entry, x, x+1, y, y+1,
                          GTK_EXPAND | GTK_FILL, GTK_EXPAND | GTK_FILL, 0, 0);
        gtk_widget_show (entry);

        g_signal_connect (entry, "changed",
                          G_CALLBACK (entry_callback),
                          &config.matrix[x][y]);
        g_signal_connect_swapped (entry, "changed",
                                  G_CALLBACK (gimp_preview_invalidate),
                                  preview);
      }

  gtk_widget_show (table);

  box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6);
  gtk_box_pack_start (GTK_BOX (inbox), box, FALSE, FALSE, 0);


  table = gtk_table_new (1, 2, FALSE);
  gtk_table_set_col_spacings (GTK_TABLE (table), 6);
  gtk_box_pack_start (GTK_BOX (box), table, TRUE, FALSE, 0);

  label = gtk_label_new_with_mnemonic (_("D_ivisor:"));
  gtk_misc_set_alignment (GTK_MISC (label), 0.0, 0.5);
  gtk_table_attach_defaults (GTK_TABLE (table), label, 0, 1, 0, 1);
  gtk_widget_show (label);

  widget_set.divisor = entry = gtk_entry_new ();
  gtk_widget_set_size_request (entry, 40, -1);
  gtk_table_attach_defaults (GTK_TABLE (table), entry, 1, 2, 0, 1);
  gtk_widget_show (entry);
  gtk_label_set_mnemonic_widget (GTK_LABEL (label), entry);

  g_signal_connect (entry, "changed",
                    G_CALLBACK (entry_callback),
                    &config.divisor);
  g_signal_connect_swapped (entry, "changed",
                            G_CALLBACK (gimp_preview_invalidate),
                            preview);

  gtk_widget_show (table);



  table = gtk_table_new (1, 2, FALSE);
  gtk_table_set_col_spacings (GTK_TABLE (table), 6);
  gtk_box_pack_start (GTK_BOX (box), table, TRUE, FALSE, 0);

  label = gtk_label_new_with_mnemonic (_("O_ffset:"));
  gtk_misc_set_alignment (GTK_MISC (label), 0.0, 0.5);
  gtk_table_attach_defaults (GTK_TABLE (table), label, 0, 1, 0, 1);
  gtk_widget_show (label);

  widget_set.offset = entry = gtk_entry_new ();
  gtk_widget_set_size_request (entry, 40, -1);
  gtk_table_attach_defaults (GTK_TABLE (table), entry, 1, 2, 0, 1);
  gtk_widget_show (entry);
  gtk_label_set_mnemonic_widget (GTK_LABEL (label), entry);

  g_signal_connect (entry, "changed",
                    G_CALLBACK (entry_callback),
                    &config.offset);
  g_signal_connect_swapped (entry, "changed",
                            G_CALLBACK (gimp_preview_invalidate),
                            preview);

  gtk_widget_show (table);

  gtk_widget_show (box);

  gtk_widget_show (inbox);
  gtk_widget_show (frame);

  box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6);
  gtk_box_pack_start (GTK_BOX (vbox), box, FALSE, FALSE, 0);

  widget_set.autoset = button =
    gtk_check_button_new_with_mnemonic (_("N_ormalise"));
  gtk_box_pack_start (GTK_BOX (box), button, TRUE, FALSE, 0);
  gtk_widget_show (button);

  g_signal_connect (button, "toggled",
                    G_CALLBACK (my_toggle_callback),
                    &config.autoset);
  g_signal_connect_swapped (button, "toggled",
                            G_CALLBACK (gimp_preview_invalidate),
                            preview);

  widget_set.alpha_weighting = button =
    gtk_check_button_new_with_mnemonic (_("A_lpha-weighting"));
  if (config.alpha_weighting == -1)
    gtk_widget_set_sensitive (button, FALSE);
  gtk_box_pack_start (GTK_BOX (box), button, TRUE, FALSE, 0);
  gtk_widget_show (button);

  g_signal_connect (button, "toggled",
                    G_CALLBACK (my_toggle_callback),
                    &config.alpha_weighting);
  g_signal_connect_swapped (button, "toggled",
                            G_CALLBACK (gimp_preview_invalidate),
                            preview);

  gtk_widget_show (box);
  gtk_widget_show (vbox);

  inbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 12);
  gtk_box_pack_start (GTK_BOX (main_hbox), inbox, FALSE, FALSE, 0);

  frame = gimp_frame_new (_("Border"));
  gtk_box_pack_start (GTK_BOX (inbox), frame, FALSE, FALSE, 0);

  box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 2);
  gtk_container_add (GTK_CONTAINER (frame), box);

  group = NULL;

  for (i = 0; i < BORDER_MODES; i++)
    {
      widget_set.bmode[i] = button =
        gtk_radio_button_new_with_mnemonic (group, gettext (bmode_labels[i]));
      group = gtk_radio_button_get_group (GTK_RADIO_BUTTON (button));
      gtk_box_pack_start (GTK_BOX (box), button, FALSE, FALSE, 0);
      gtk_widget_show (button);

      g_signal_connect (button, "toggled",
                        G_CALLBACK (my_bmode_callback),
                        GINT_TO_POINTER (i + 1));
      g_signal_connect_swapped (button, "toggled",
                                G_CALLBACK (gimp_preview_invalidate),
                                preview);
    }

  gtk_widget_show (box);
  gtk_widget_show (frame);

  frame = gimp_frame_new (_("Channels"));
  gtk_box_pack_start (GTK_BOX (inbox), frame, FALSE, FALSE, 0);

  box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 2);
  gtk_container_add (GTK_CONTAINER (frame), box);

  for (i = 0; i < CHANNELS; i++)
    {
      if ((gimp_drawable_is_gray (drawable->drawable_id) && i==0) ||
          (gimp_drawable_is_rgb  (drawable->drawable_id) && i>=1 && i<=3) ||
          (gimp_drawable_has_alpha (drawable->drawable_id) && i==4))
        {
          widget_set.channels[i] = button =
            gtk_check_button_new_with_mnemonic (gettext (channel_labels[i]));

          gtk_box_pack_start (GTK_BOX (box), button, FALSE, FALSE, 0);
          gtk_widget_show (button);

          g_signal_connect (button, "toggled",
                            G_CALLBACK (my_toggle_callback),
                            &config.channels[i]);
          g_signal_connect_swapped (button, "toggled",
                                    G_CALLBACK (gimp_preview_invalidate),
                                    preview);
        }
      else
        {
          widget_set.channels[i] = NULL;
        }
    }

  gtk_widget_show (box);
  gtk_widget_show (frame);

  gtk_widget_show (inbox);

  g_signal_connect (dialog, "response",
                    G_CALLBACK (response_callback),
                    drawable);
  g_signal_connect (dialog, "destroy",
                    G_CALLBACK (gtk_main_quit),
                    NULL);

  gtk_widget_show (dialog);
  redraw_all ();

  gtk_widget_set_sensitive (widget_set.bmode[CLEAR],
                            (config.alpha_weighting > 0));

  gtk_main ();

  return run_flag;
}
