/* vpropagate.c -- This is a plug-in for GIMP (1.0's API)
 * Author: Shuji Narazaki <narazaki@InetQ.or.jp>
 * Time-stamp: <2000-01-09 15:50:46 yasuhiro>
 * Version: 0.89a
 *
 * Copyright (C) 1996-1997 Shuji Narazaki <narazaki@InetQ.or.jp>
 *
 * 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/>.
 */

/* memo
   the initial value of each pixel is the value of the pixel itself.
   To determine whether it is an isolated local peak point, use:
   (self == min && (! modified_flag))   ; modified_flag holds history of update
   In other word, pixel itself is not a neighbor of it.
*/
/*
   in response to bug #156545, after lengthy discussion, the meanings
   of "dilate" and "erode" are being swapped -- 19 May 2006.
*/

#include "config.h"

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

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

#include "libgimp/stdplugins-intl.h"


#define VPROPAGATE_PROC      "plug-in-vpropagate"
#define ERODE_PROC           "plug-in-erode"
#define DILATE_PROC          "plug-in-dilate"
#define PLUG_IN_BINARY       "value-propagate"
#define PLUG_IN_IMAGE_TYPES  "RGB*, GRAY*"

#define VP_RGB          (1 << 0)
#define VP_GRAY         (1 << 1)
#define VP_WITH_ALPHA   (1 << 2)
#define VP_WO_ALPHA     (1 << 3)
#define num_direction   4
#define Right2Left      0
#define Bottom2Top      1
#define Left2Right      2
#define Top2Bottom      3

static void         query                      (void);
static void         run                        (const gchar       *name,
                                                gint               nparams,
                                                const GimpParam   *param,
                                                gint              *nreturn_vals,
                                                GimpParam        **return_vals);

static GimpPDBStatusType  value_propagate      (GimpDrawable  *drawable);
static void         value_propagate_body       (GimpDrawable  *drawable,
                                                GimpPreview   *preview);
static gboolean     vpropagate_dialog          (GimpDrawable  *drawable);
static void         prepare_row                (GimpPixelRgn  *pixel_rgn,
                                                guchar        *data,
                                                gint           x,
                                                gint           y,
                                                gint           w);

static void    vpropagate_toggle_button_update (GtkWidget     *widget,
                                                gpointer       data);
static GtkWidget *  gtk_table_add_toggle       (GtkWidget     *table,
                                                const gchar   *name,
                                                gint           x1,
                                                gint           x2,
                                                gint           y,
                                                GCallback      update,
                                                gint          *value);

static int          value_difference_check  (guchar *, guchar *, int);
static void         set_value               (GimpImageBaseType,
                                             int, guchar *, guchar *, guchar *,
                                             void *);
static void         initialize_white        (GimpImageBaseType,
                                             int, guchar *, guchar *,
                                             void **);
static void         propagate_white         (GimpImageBaseType,
                                             int, guchar *, guchar *, guchar *,
                                             void *);
static void         initialize_black        (GimpImageBaseType,
                                             int, guchar *, guchar *,
                                             void **);
static void         propagate_black         (GimpImageBaseType,
                                             int, guchar *, guchar *, guchar *,
                                             void *);
static void         initialize_middle       (GimpImageBaseType,
                                             int, guchar *, guchar *,
                                             void **);
static void         propagate_middle        (GimpImageBaseType,
                                             int, guchar *, guchar *, guchar *,
                                             void *);
static void         set_middle_to_peak      (GimpImageBaseType,
                                             int, guchar *, guchar *, guchar *,
                                             void *);
static void         set_foreground_to_peak  (GimpImageBaseType,
                                             int, guchar *, guchar *, guchar *,
                                             void *);
static void         initialize_foreground   (GimpImageBaseType,
                                             int, guchar *, guchar *,
                                             void **);
static void         initialize_background   (GimpImageBaseType,
                                             int, guchar *, guchar *,
                                             void **);
static void         propagate_a_color       (GimpImageBaseType,
                                             int, guchar *, guchar *, guchar *,
                                             void *);
static void         propagate_opaque        (GimpImageBaseType,
                                             int, guchar *, guchar *, guchar *,
                                             void *);
static void         propagate_transparent   (GimpImageBaseType,
                                             int, guchar *, guchar *, guchar *,
                                             void *);

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

#define SCALE_WIDTH        100
#define PROPAGATING_VALUE  (1 << 0)
#define PROPAGATING_ALPHA  (1 << 1)

/* parameters */
typedef struct
{
  gint     propagate_mode;
  gint     propagating_channel;
  gdouble  propagating_rate;
  gint     direction_mask;
  gint     lower_limit;
  gint     upper_limit;
} VPValueType;

static VPValueType vpvals =
{
  0,        /* propagate_mode                        */
  3,        /* PROPAGATING_VALUE + PROPAGATING_ALPHA */
  1.0,      /* [0.0:1.0]                             */
  15,       /* propagate to all 4 directions         */
  0,        /* lower_limit                           */
  255       /* upper_limit                           */
};

/* dialog variables */
static gint       propagate_alpha;
static gint       propagate_value;
static gint       direction_mask_vec[4];
static gint       channel_mask[] = { 1, 1, 1 };
static gint       peak_max = 1;
static gint       peak_min = 1;
static gint       peak_includes_equals = 1;
static guchar     fore[3];
static GtkWidget *preview;

typedef struct
{
  gint    applicable_image_type;
  gchar  *name;
  void  (*initializer) (GimpImageBaseType, gint, guchar *, guchar *, gpointer *);
  void  (*updater) (GimpImageBaseType, gint, guchar *, guchar *, guchar *, gpointer);
  void  (*finalizer) (GimpImageBaseType, gint, guchar *, guchar *, guchar *, gpointer);
} ModeParam;

#define num_mode 8
static ModeParam modes[num_mode] =
{
  { VP_RGB | VP_GRAY | VP_WITH_ALPHA | VP_WO_ALPHA,
    N_("More _white (larger value)"),
    initialize_white,      propagate_white,       set_value },
  { VP_RGB | VP_GRAY | VP_WITH_ALPHA | VP_WO_ALPHA,
    N_("More blac_k (smaller value)"),
    initialize_black,      propagate_black,       set_value },
  { VP_RGB | VP_GRAY | VP_WITH_ALPHA | VP_WO_ALPHA,
    N_("_Middle value to peaks"),
    initialize_middle,     propagate_middle,      set_middle_to_peak },
  { VP_RGB | VP_GRAY | VP_WITH_ALPHA | VP_WO_ALPHA,
    N_("_Foreground to peaks"),
    initialize_middle,     propagate_middle,      set_foreground_to_peak },
  { VP_RGB | VP_WITH_ALPHA | VP_WO_ALPHA,
    N_("O_nly foreground"),
    initialize_foreground, propagate_a_color,     set_value },
  { VP_RGB | VP_WITH_ALPHA | VP_WO_ALPHA,
    N_("Only b_ackground"),
    initialize_background, propagate_a_color,     set_value },
  { VP_RGB | VP_GRAY | VP_WITH_ALPHA,
    N_("Mor_e opaque"),
    NULL,                  propagate_opaque,      set_value },
  { VP_RGB | VP_GRAY | VP_WITH_ALPHA,
    N_("More t_ransparent"),
    NULL,                  propagate_transparent, set_value },
};

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 (not used)" },
    { GIMP_PDB_DRAWABLE, "drawable",            "Input drawable" },
    { GIMP_PDB_INT32,    "propagate-mode",      "propagate 0:white, 1:black, 2:middle value 3:foreground to peak, 4:foreground, 5:background, 6:opaque, 7:transparent" },
    { GIMP_PDB_INT32,    "propagating-channel", "channels which values are propagated" },
    { GIMP_PDB_FLOAT,    "propagating-rate",    "0.0 <= propagatating_rate <= 1.0" },
    { GIMP_PDB_INT32,    "direction-mask",      "0 <= direction-mask <= 15" },
    { GIMP_PDB_INT32,    "lower-limit",         "0 <= lower-limit <= 255" },
    { GIMP_PDB_INT32,    "upper-limit",         "0 <= upper-limit <= 255" }
  };

  gimp_install_procedure (VPROPAGATE_PROC,
                          N_("Propagate certain colors to neighboring pixels"),
                          "Propagate values of the layer",
                          "Shuji Narazaki (narazaki@InetQ.or.jp)",
                          "Shuji Narazaki",
                          "1996-1997",
                          N_("_Value Propagate..."),
                          PLUG_IN_IMAGE_TYPES,
                          GIMP_PLUGIN,
                          G_N_ELEMENTS (args), 0,
                          args, NULL);

  gimp_install_procedure (ERODE_PROC,
                          N_("Shrink lighter areas of the image"),
                          "Erode image",
                          "Shuji Narazaki (narazaki@InetQ.or.jp)",
                          "Shuji Narazaki",
                          "1996-1997",
                          N_("E_rode"),
                          PLUG_IN_IMAGE_TYPES,
                          GIMP_PLUGIN,
                          G_N_ELEMENTS (args), 0,
                          args, NULL);

  gimp_install_procedure (DILATE_PROC,
                          N_("Grow lighter areas of the image"),
                          "Dilate image",
                          "Shuji Narazaki (narazaki@InetQ.or.jp)",
                          "Shuji Narazaki",
                          "1996-1997",
                          N_("_Dilate"),
                          PLUG_IN_IMAGE_TYPES,
                          GIMP_PLUGIN,
                          G_N_ELEMENTS (args), 0,
                          args, NULL);

  gimp_plugin_menu_register (VPROPAGATE_PROC, "<Image>/Filters/Distorts");
  gimp_plugin_menu_register (ERODE_PROC,      "<Image>/Filters/Generic");
  gimp_plugin_menu_register (DILATE_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];
  GimpPDBStatusType  status = GIMP_PDB_SUCCESS;
  GimpRunMode        run_mode;
  GimpDrawable      *drawable;

  run_mode = param[0].data.d_int32;
  drawable = gimp_drawable_get (param[2].data.d_int32);

  INIT_I18N ();

  *nreturn_vals = 1;
  *return_vals  = values;

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

  switch (run_mode)
    {
    case GIMP_RUN_INTERACTIVE:
      if (strcmp (name, VPROPAGATE_PROC) == 0)
        {
          gimp_get_data (VPROPAGATE_PROC, &vpvals);
          /* building the values of dialog variables from vpvals. */
          propagate_alpha =
            (vpvals.propagating_channel & PROPAGATING_ALPHA) ? TRUE : FALSE;
          propagate_value =
            (vpvals.propagating_channel & PROPAGATING_VALUE) ? TRUE : FALSE;

          {
            gint i;
            for (i = 0; i < 4; i++)
              direction_mask_vec[i] =
                (vpvals.direction_mask & (1 << i)) ? TRUE : FALSE;
          }

          if (! vpropagate_dialog (drawable))
            return;
        }
      else if (strcmp (name, ERODE_PROC) == 0 ||
               strcmp (name, DILATE_PROC) == 0)
        {
          vpvals.propagating_channel = PROPAGATING_VALUE;
          vpvals.propagating_rate    = 1.0;
          vpvals.direction_mask      = 15;
          vpvals.lower_limit         = 0;
          vpvals.upper_limit         = 255;

          if (strcmp (name, ERODE_PROC) == 0)
            vpvals.propagate_mode = 1;
          else if (strcmp (name, DILATE_PROC) == 0)
            vpvals.propagate_mode = 0;
        }
      break;

    case GIMP_RUN_NONINTERACTIVE:
      if (strcmp (name, VPROPAGATE_PROC) == 0)
        {
          vpvals.propagate_mode      = param[3].data.d_int32;
          vpvals.propagating_channel = param[4].data.d_int32;
          vpvals.propagating_rate    = param[5].data.d_float;
          vpvals.direction_mask      = param[6].data.d_int32;
          vpvals.lower_limit         = param[7].data.d_int32;
          vpvals.upper_limit         = param[8].data.d_int32;
        }
      else if (strcmp (name, ERODE_PROC) == 0 ||
               strcmp (name, DILATE_PROC) == 0)
        {
          vpvals.propagating_channel = PROPAGATING_VALUE;
          vpvals.propagating_rate    = 1.0;
          vpvals.direction_mask      = 15;
          vpvals.lower_limit         = 0;
          vpvals.upper_limit         = 255;

          if (strcmp (name, ERODE_PROC) == 0)
            vpvals.propagate_mode = 1;
          else if (strcmp (name, DILATE_PROC) == 0)
            vpvals.propagate_mode = 0;
        }
      break;

    case GIMP_RUN_WITH_LAST_VALS:
      gimp_get_data (name, &vpvals);
      break;
    }

  status = value_propagate (drawable);

  if (status == GIMP_PDB_SUCCESS)
    {
      if (run_mode != GIMP_RUN_NONINTERACTIVE)
        gimp_displays_flush ();

      if (run_mode == GIMP_RUN_INTERACTIVE)
        gimp_set_data (name, &vpvals, sizeof (VPValueType));
    }

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

/* registered function entry */
static GimpPDBStatusType
value_propagate (GimpDrawable *drawable)
{
  /* check the validness of parameters */
  if (!(vpvals.propagating_channel & (PROPAGATING_VALUE | PROPAGATING_ALPHA)))
    {
      /* gimp_message ("No channel selected."); */
      return GIMP_PDB_EXECUTION_ERROR;
    }
  if (vpvals.direction_mask == 0)
    {
      /* gimp_message ("No direction selected."); */
      return GIMP_PDB_EXECUTION_ERROR;
    }
  if ((vpvals.lower_limit < 0) || (255 < vpvals.lower_limit) ||
       (vpvals.upper_limit < 0) || (255 < vpvals.lower_limit) ||
       (vpvals.upper_limit < vpvals.lower_limit))
    {
      /* gimp_message ("Limit values are not valid."); */
      return GIMP_PDB_EXECUTION_ERROR;
    }
  value_propagate_body (drawable, NULL);
  return GIMP_PDB_SUCCESS;
}

static void
value_propagate_body (GimpDrawable *drawable,
                      GimpPreview  *preview)
{
  GimpImageBaseType  dtype;
  ModeParam          operation;
  GimpPixelRgn       srcRgn, destRgn;
  guchar            *here, *best, *dest;
  guchar            *dest_row, *prev_row, *cur_row, *next_row;
  guchar            *pr, *cr, *nr, *swap;
  gint               width, height, bytes, index;
  gint               begx, begy, endx, endy, x, y, dx;
  gint               left_index, right_index, up_index, down_index;
  gpointer           tmp;
  GimpRGB            foreground;

  /* calculate neighbors' indexes */
  left_index  = (vpvals.direction_mask & (1 << Left2Right)) ? -1 : 0;
  right_index = (vpvals.direction_mask & (1 << Right2Left)) ?  1 : 0;
  up_index    = (vpvals.direction_mask & (1 << Top2Bottom)) ? -1 : 0;
  down_index  = (vpvals.direction_mask & (1 << Bottom2Top)) ?  1 : 0;
  operation   = modes[vpvals.propagate_mode];
  tmp         = NULL;

  dtype = gimp_drawable_type (drawable->drawable_id);
  bytes = drawable->bpp;

  /* Here I use the algorithm of blur.c */
  if (preview)
    {
       gimp_preview_get_position (preview, &begx, &begy);
       gimp_preview_get_size (preview, &width, &height);

       endx = begx + width;
       endy = begy + height;
    }
  else
    {
      if (! gimp_drawable_mask_intersect (drawable->drawable_id,
                                          &begx, &begy, &width, &height))
        return;

      endx = begx + width;
      endy = begy + height;
    }

  gimp_tile_cache_ntiles (2 * ((width) / gimp_tile_width () + 1));

  prev_row = g_new (guchar, (width + 2) * bytes);
  cur_row  = g_new (guchar, (width + 2) * bytes);
  next_row = g_new (guchar, (width + 2) * bytes);
  dest_row = g_new (guchar, width * bytes);

  gimp_pixel_rgn_init (&srcRgn, drawable,
                       begx, begy, width, height,
                       FALSE, FALSE);
  gimp_pixel_rgn_init (&destRgn, drawable,
                       begx, begy, width, height,
                       (preview == NULL), TRUE);

  pr = prev_row + bytes;
  cr = cur_row + bytes;
  nr = next_row + bytes;

  prepare_row (&srcRgn, pr, begx, (0 < begy) ? begy : begy - 1, endx-begx);
  prepare_row (&srcRgn, cr, begx, begy, endx-begx);

  best = g_new (guchar, bytes);

  if (!preview)
    gimp_progress_init (_("Value Propagate"));

  gimp_context_get_foreground (&foreground);
  gimp_rgb_get_uchar (&foreground, fore+0, fore+1, fore+2);

  /* start real job */
  for (y = begy ; y < endy ; y++)
    {
      prepare_row (&srcRgn, nr, begx, ((y+1) < endy) ? y+1 : endy, endx-begx);

      for (index = 0; index < (endx - begx) * bytes; index++)
        dest_row[index] = cr[index];

      for (x = 0 ; x < endx - begx; x++)
        {
          dest = dest_row + (x * bytes);
          here = cr + (x * bytes);

          /* *** copy source value to best value holder *** */
          memcpy (best, here, bytes);

          if (operation.initializer)
            (* operation.initializer)(dtype, bytes, best, here, &tmp);

          /* *** gather neighbors' values: loop-unfolded version *** */
          if (up_index == -1)
            for (dx = left_index ; dx <= right_index ; dx++)
              (* operation.updater)(dtype, bytes, here, pr+((x+dx)*bytes), best, tmp);
          for (dx = left_index ; dx <= right_index ; dx++)
            if (dx != 0)
              (* operation.updater)(dtype, bytes, here, cr+((x+dx)*bytes), best, tmp);
          if (down_index == 1)
            for (dx = left_index ; dx <= right_index ; dx++)
              (* operation.updater)(dtype, bytes, here, nr+((x+dx)*bytes), best, tmp);
          /* *** store it to dest_row*** */
          (* operation.finalizer)(dtype, bytes, best, here, dest, tmp);
        }

      /* now store destline to destRgn */
      gimp_pixel_rgn_set_row (&destRgn, dest_row, begx, y, endx - begx);

      /* shift the row pointers  */
      swap = pr;
      pr = cr;
      cr = nr;
      nr = swap;


      if (((y % 16) == 0) && !preview)
        gimp_progress_update ((gdouble) y / (gdouble) (endy - begy));
    }

  if (preview)
    {
      gimp_drawable_preview_draw_region (GIMP_DRAWABLE_PREVIEW (preview),
                                         &destRgn);
    }
  else
    {
      /*  update the region  */
      gimp_progress_update (1.0);
      gimp_drawable_flush (drawable);
      gimp_drawable_merge_shadow (drawable->drawable_id, TRUE);
      gimp_drawable_update (drawable->drawable_id, begx, begy, endx-begx, endy-begy);
    }
}

static void
prepare_row (GimpPixelRgn *pixel_rgn,
             guchar       *data,
             gint          x,
             gint          y,
             gint          w)
{
  gint b;

  if (y <= 0)
    gimp_pixel_rgn_get_row (pixel_rgn, data, x, (y + 1), w);
  else if (y >= pixel_rgn->h)
    gimp_pixel_rgn_get_row (pixel_rgn, data, x, (y - 1), w);
  else
    gimp_pixel_rgn_get_row (pixel_rgn, data, x, y, w);

  /*  Fill in edge pixels  */
  for (b = 0; b < pixel_rgn->bpp; b++)
    {
      data[-(gint)pixel_rgn->bpp + b] = data[b];
      data[w * pixel_rgn->bpp + b] = data[(w - 1) * pixel_rgn->bpp + b];
    }
}

static void
set_value (GimpImageBaseType  dtype,
           gint               bytes,
           guchar            *best,
           guchar            *here,
           guchar            *dest,
           void              *tmp)
{
  gint  value_chs = 0;
  gint  alpha = 0;
  gint  ch;

  switch (dtype)
    {
    case GIMP_RGB_IMAGE:
      value_chs = 3;
      break;
    case GIMP_RGBA_IMAGE:
      value_chs = 3;
      alpha = 3;
      break;
    case GIMP_GRAY_IMAGE:
      value_chs = 1;
      break;
    case GIMP_GRAYA_IMAGE:
      value_chs = 1;
      alpha = 1;
      break;
    default:
      break;
    }
  for (ch = 0; ch < value_chs; ch++)
    {
      if (vpvals.propagating_channel & PROPAGATING_VALUE) /* value channel */
        *dest++ = (guchar)(vpvals.propagating_rate * best[ch]
                       + (1.0 - vpvals.propagating_rate) * here[ch]);
      else
        *dest++ = here[ch];
    }
  if (alpha)
    {
      if (vpvals.propagating_channel & PROPAGATING_ALPHA) /* alpha channel */
        *dest++ = (guchar)(vpvals.propagating_rate * best[alpha]
                       + (1.0 - vpvals.propagating_rate) * here[alpha]);
      else
        *dest++ = here[alpha];
    }
}

static inline int
value_difference_check (guchar *pos1,
                        guchar *pos2,
                        gint   ch)
{
  gint  index;
  int   diff;

  for (index = 0 ; index < ch; index++)
    if (channel_mask[index] != 0)
      {
        diff = abs(pos1[index] - pos2[index]);
        if (! ((vpvals.lower_limit <= diff) && (diff <= vpvals.upper_limit)))
          return 0;
      }
  return 1;
}

/* mothods for each mode */
static void
initialize_white (GimpImageBaseType   dtype,
                  gint                bytes,
                  guchar             *best,
                  guchar             *here,
                  void              **tmp)
{

  switch (dtype)
    {
    case GIMP_RGB_IMAGE:
    case GIMP_RGBA_IMAGE:
      if (*tmp == NULL)
        *tmp = (void *) g_new (gfloat, 1);
      **(float **)tmp = channel_mask[0] * here[0] * here[0]
                     + channel_mask[1] * here[1] * here[1]
                     + channel_mask[2] * here[2] * here[2];
      break;
    case GIMP_GRAYA_IMAGE:
    case GIMP_GRAY_IMAGE:
      break;
    default:
      break;
    }
}

static void
propagate_white (GimpImageBaseType  dtype,
                 gint               bytes,
                 guchar            *orig,
                 guchar            *here,
                 guchar            *best,
                 void              *tmp)
{
  float v_here;

  switch (dtype)
    {
    case GIMP_RGB_IMAGE:
    case GIMP_RGBA_IMAGE:
      v_here = channel_mask[0] * here[0] * here[0]
                     + channel_mask[1] * here[1] * here[1]
                     + channel_mask[2] * here[2] * here[2];
     if (*(float *)tmp < v_here && value_difference_check(orig, here, 3))
        {
          *(float *)tmp = v_here;
          memcpy(best, here, 3 * sizeof(guchar)); /* alpha channel holds old value */
        }
      break;
    case GIMP_GRAYA_IMAGE:
    case GIMP_GRAY_IMAGE:
      if (*best < *here && value_difference_check(orig, here, 1))
        *best = *here;
      break;
    default:
      break;
    }
}

static void
initialize_black (GimpImageBaseType   dtype,
                  gint                channels,
                  guchar             *best,
                  guchar             *here,
                  void              **tmp)
{
  switch (dtype)
    {
    case GIMP_RGB_IMAGE:
    case GIMP_RGBA_IMAGE:
      if (*tmp == NULL)
        *tmp = (void *) g_new (gfloat, 1);
      **(float **)tmp = (channel_mask[0] * here[0] * here[0]
                     + channel_mask[1] * here[1] * here[1]
                     + channel_mask[2] * here[2] * here[2]);
      break;
    case GIMP_GRAYA_IMAGE:
    case GIMP_GRAY_IMAGE:
      break;
    default:
      break;
    }
}

static void
propagate_black (GimpImageBaseType  image_type,
                 gint               channels,
                 guchar            *orig,
                 guchar            *here,
                 guchar            *best,
                 void              *tmp)
{
  float v_here;

  switch (image_type)
    {
    case GIMP_RGB_IMAGE:
    case GIMP_RGBA_IMAGE:
      v_here = (channel_mask[0] * here[0] * here[0]
                     + channel_mask[1] * here[1] * here[1]
                     + channel_mask[2] * here[2] * here[2]);
      if (v_here < *(float *)tmp && value_difference_check(orig, here, 3))
        {
          *(float *)tmp = v_here;
          memcpy (best, here, 3 * sizeof(guchar)); /* alpha channel holds old value */
        }
      break;
    case GIMP_GRAYA_IMAGE:
    case GIMP_GRAY_IMAGE:
      if (*here < *best && value_difference_check(orig, here, 1))
        *best = *here;
      break;
    default:
      break;
    }
}

typedef struct
{
  gshort min_modified;
  gshort max_modified;
  glong  original_value;
  glong  minv;
  guchar min[3];
  glong  maxv;
  guchar max[3];
} MiddlePacket;

static void
initialize_middle (GimpImageBaseType   image_type,
                   gint                channels,
                   guchar             *best,
                   guchar             *here,
                   void              **tmp)
{
  int index;
  MiddlePacket *data;

  if (*tmp == NULL)
    *tmp = (void *) g_new (MiddlePacket, 1);
  data = (MiddlePacket *)*tmp;
  for (index = 0; index < channels; index++)
    data->min[index] = data->max[index] = here[index];
  switch (image_type)
    {
    case GIMP_RGB_IMAGE:
    case GIMP_RGBA_IMAGE:
      data->original_value = (channel_mask[0] * here[0] * here[0]
                                   + channel_mask[1] * here[1] * here[1]
                                   + channel_mask[2] * here[2] * here[2]);
      break;
    case GIMP_GRAYA_IMAGE:
    case GIMP_GRAY_IMAGE:
      data->original_value = *here;
      break;
    default:
      break;
    }
  data->minv = data->maxv = data->original_value;
  data->min_modified = data->max_modified = 0;
}

static void
propagate_middle (GimpImageBaseType  image_type,
                  gint               channels,
                  guchar            *orig,
                  guchar            *here,
                  guchar            *best,
                  void              *tmp)
{
  float v_here;
  MiddlePacket *data;

  data = (MiddlePacket *)tmp;

  switch (image_type)
    {
    case GIMP_RGB_IMAGE:
    case GIMP_RGBA_IMAGE:
      v_here = (channel_mask[0] * here[0] * here[0]
                     + channel_mask[1] * here[1] * here[1]
                     + channel_mask[2] * here[2] * here[2]);
      if ((v_here <= data->minv) && value_difference_check(orig, here, 3))
        {
          data->minv = v_here;
          memcpy (data->min, here, 3*sizeof(guchar));
          data->min_modified = 1;
        }
      if (data->maxv <= v_here && value_difference_check(orig, here, 3))
        {
          data->maxv = v_here;
          memcpy (data->max, here, 3*sizeof(guchar));
          data->max_modified = 1;
        }
      break;
    case GIMP_GRAYA_IMAGE:
    case GIMP_GRAY_IMAGE:
      if ((*here <= data->min[0]) && value_difference_check(orig, here, 1))
        {
          data->min[0] = *here;
          data->min_modified = 1;
        }
      if ((data->max[0] <= *here) && value_difference_check(orig, here, 1))
        {
          data->max[0] = *here;
          data->max_modified = 1;
        }
      break;
    default:
      break;
    }
}

static void
set_middle_to_peak (GimpImageBaseType  image_type,
                    gint               channels,
                    guchar            *here,
                    guchar            *best,
                    guchar            *dest,
                    void              *tmp)
{
  gint  value_chs = 0;
  gint  alpha = 0;
  gint  ch;
  MiddlePacket  *data;

  data = (MiddlePacket *)tmp;
  if (! ((peak_min & (data->minv == data->original_value))
         || (peak_max & (data->maxv == data->original_value))))
    return;
  if ((! peak_includes_equals)
      && ((peak_min & (! data->min_modified))
          || (peak_max & (! data->max_modified))))
      return;

  switch (image_type)
    {
    case GIMP_RGB_IMAGE:
      value_chs = 3;
      break;
    case GIMP_RGBA_IMAGE:
      value_chs = 3;
      alpha = 3;
      break;
    case GIMP_GRAY_IMAGE:
      value_chs = 1;
      break;
    case GIMP_GRAYA_IMAGE:
      value_chs = 1;
      alpha = 1;
      break;
    default:
      break;
    }
  for (ch = 0; ch < value_chs; ch++)
    {
      if (vpvals.propagating_channel & PROPAGATING_VALUE) /* value channel */
        *dest++ = (guchar)(vpvals.propagating_rate * 0.5 * (data->min[ch] + data->max[ch])
                       + (1.0 - vpvals.propagating_rate) * here[ch]);
      else
        *dest++ = here[ch];
    }
  if (alpha)
    {
      if (vpvals.propagating_channel & PROPAGATING_ALPHA) /* alpha channel */
        *dest++ = (guchar)(vpvals.propagating_rate * best[alpha]
                       + (1.0 - vpvals.propagating_rate) * here[alpha]);
      else
        *dest++ = here[alpha];
    }
}

static void
set_foreground_to_peak (GimpImageBaseType  image_type,
                        gint               channels,
                        guchar            *here,
                        guchar            *best,
                        guchar            *dest,
                        void              *tmp)
{
  gint  value_chs = 0;
  gint  alpha = 0;
  gint  ch;
  MiddlePacket  *data;

  data = (MiddlePacket *)tmp;
  if (! ((peak_min & (data->minv == data->original_value))
         || (peak_max & (data->maxv == data->original_value))))
    return;
  if (peak_includes_equals
      && ((peak_min & (! data->min_modified))
          || (peak_max & (! data->max_modified))))
      return;

  switch (image_type)
    {
    case GIMP_RGB_IMAGE:
      value_chs = 3;
      break;
    case GIMP_RGBA_IMAGE:
      value_chs = 3;
      alpha = 3;
      break;
    case GIMP_GRAY_IMAGE:
      value_chs = 1;
      break;
    case GIMP_GRAYA_IMAGE:
      value_chs = 1;
      alpha = 1;
      break;
    default:
      break;
    }
  for (ch = 0; ch < value_chs; ch++)
    {
      if (vpvals.propagating_channel & PROPAGATING_VALUE) /* value channel */
        *dest++ = (guchar)(vpvals.propagating_rate*fore[ch]
                       + (1.0 - vpvals.propagating_rate)*here[ch]);
      else
        *dest++ = here[ch];
    }
}

static void
initialize_foreground (GimpImageBaseType   image_type,
                       gint                channels,
                       guchar             *here,
                       guchar             *best,
                       void              **tmp)
{
  GimpRGB  foreground;
  guchar  *ch;

  if (*tmp == NULL)
    {
      *tmp = (void *) g_new (guchar, 3);
      ch = (guchar *)*tmp;
      gimp_context_get_foreground (&foreground);
      gimp_rgb_get_uchar (&foreground, &ch[0], &ch[1], &ch[2]);
    }
}

static void
initialize_background (GimpImageBaseType   image_type,
                       gint                channels,
                       guchar             *here,
                       guchar             *best,
                       void              **tmp)
{
  GimpRGB  background;
  guchar  *ch;

  if (*tmp == NULL)
    {
      *tmp = (void *) g_new (guchar, 3);
      ch = (guchar *)*tmp;
      gimp_context_get_background (&background);
      gimp_rgb_get_uchar (&background, &ch[0], &ch[1], &ch[2]);
    }
}

static void
propagate_a_color (GimpImageBaseType  image_type,
                   gint               channels,
                   guchar            *orig,
                   guchar            *here,
                   guchar            *best,
                   void              *tmp)
{
  guchar *fg = (guchar *)tmp;

  switch (image_type)
    {
    case GIMP_RGB_IMAGE:
    case GIMP_RGBA_IMAGE:
      if (here[0] == fg[0] && here[1] == fg[1] && here[2] == fg[2] &&
          value_difference_check(orig, here, 3))
        {
          memcpy (best, here, 3 * sizeof(guchar)); /* alpha channel holds old value */
        }
      break;
    case GIMP_GRAYA_IMAGE:
    case GIMP_GRAY_IMAGE:
      break;
    default:
      break;
    }
}

static void
propagate_opaque (GimpImageBaseType  image_type,
                  gint               channels,
                  guchar            *orig,
                  guchar            *here,
                  guchar            *best,
                  void              *tmp)
{
  switch (image_type)
    {
    case GIMP_RGBA_IMAGE:
      if (best[3] < here[3] && value_difference_check(orig, here, 3))
        memcpy(best, here, channels * sizeof(guchar));
      break;
    case GIMP_GRAYA_IMAGE:
      if (best[1] < here[1] && value_difference_check(orig, here, 1))
        memcpy(best, here, channels * sizeof(guchar));
      break;
    default:
      break;
    }
}

static void
propagate_transparent (GimpImageBaseType  image_type,
                       gint               channels,
                       guchar            *orig,
                       guchar            *here,
                       guchar            *best,
                       void              *tmp)
{
  switch (image_type)
    {
    case GIMP_RGBA_IMAGE:
      if (here[3] < best[3] && value_difference_check(orig, here, 3))
        memcpy(best, here, channels * sizeof(guchar));
      break;
    case GIMP_GRAYA_IMAGE:
      if (here[1] < best[1] && value_difference_check(orig, here, 1))
        memcpy(best, here, channels * sizeof(guchar));
      break;
    default:
      break;
    }
}

/* dialog stuff */

static gboolean
vpropagate_dialog (GimpDrawable *drawable)
{
  GtkWidget *dialog;
  GtkWidget *main_vbox;
  GtkWidget *hbox;
  GtkWidget *frame;
  GtkWidget *table;
  GtkWidget *toggle_vbox;
  GtkWidget *button;
  GtkObject *adj;
  GSList    *group = NULL;
  gint       index = 0;
  gboolean   run;

  gimp_ui_init (PLUG_IN_BINARY, FALSE);

  dialog = gimp_dialog_new (_("Value Propagate"), PLUG_IN_BINARY,
                            NULL, 0,
                            gimp_standard_help_func, VPROPAGATE_PROC,

                            GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
                            GTK_STOCK_OK,     GTK_RESPONSE_OK,

                            NULL);

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

  gimp_window_set_transient (GTK_WINDOW (dialog));

  main_vbox = gtk_vbox_new (FALSE, 12);
  gtk_container_set_border_width (GTK_CONTAINER (main_vbox), 12);
  gtk_container_add (GTK_CONTAINER (gtk_dialog_get_content_area (GTK_DIALOG (dialog))),
                     main_vbox);
  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 (value_propagate_body),
                            drawable);

  hbox = gtk_hbox_new (FALSE, 12);
  gtk_box_pack_start (GTK_BOX (main_vbox), hbox, FALSE, FALSE, 0);
  gtk_widget_show (hbox);

  /* Propagate Mode */
  frame = gimp_frame_new (_("Mode"));
  gtk_box_pack_start (GTK_BOX (hbox), frame, FALSE, FALSE, 0);
  gtk_widget_show (frame);

  toggle_vbox = gtk_vbox_new (FALSE, 2);
  gtk_container_add (GTK_CONTAINER (frame), toggle_vbox);
  gtk_widget_show (toggle_vbox);

  for (index = 0; index < num_mode; index++)
    {
      button =
        gtk_radio_button_new_with_mnemonic (group,
                                            gettext (modes[index].name));
      group = gtk_radio_button_get_group (GTK_RADIO_BUTTON (button));
      gtk_box_pack_start (GTK_BOX (toggle_vbox), button, FALSE, FALSE, 0);
      gtk_widget_show (button);

      g_object_set_data (G_OBJECT (button), "gimp-item-data",
                         GINT_TO_POINTER (index));

      g_signal_connect (button, "toggled",
                        G_CALLBACK (gimp_radio_button_update),
                        &vpvals.propagate_mode);
      g_signal_connect_swapped (button, "toggled",
                                G_CALLBACK (gimp_preview_invalidate),
                                preview);

      gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (button),
                                    index == vpvals.propagate_mode);
    }

  /* Parameter settings */
  frame = gimp_frame_new (_("Propagate"));
  gtk_box_pack_start (GTK_BOX (hbox), frame, FALSE, FALSE, 0);
  gtk_widget_show (frame);

  table = gtk_table_new (8, 3, FALSE); /* 4 raw, 2 columns(name and value) */
  gtk_table_set_col_spacings (GTK_TABLE (table), 6);
  gtk_table_set_row_spacings (GTK_TABLE (table), 6);
  gtk_table_set_row_spacing (GTK_TABLE (table), 2, 12);
  gtk_table_set_row_spacing (GTK_TABLE (table), 5, 12);
  gtk_container_add (GTK_CONTAINER (frame), table);
  gtk_widget_show (table);

  adj = gimp_scale_entry_new (GTK_TABLE (table), 0, 0,
                              _("Lower t_hreshold:"), SCALE_WIDTH, 4,
                              vpvals.lower_limit, 0, 255, 1, 8, 0,
                              TRUE, 0, 0,
                              NULL, NULL);
  g_signal_connect (adj, "value-changed",
                    G_CALLBACK (gimp_int_adjustment_update),
                    &vpvals.lower_limit);
  g_signal_connect_swapped (adj, "value-changed",
                            G_CALLBACK (gimp_preview_invalidate),
                            preview);

  adj = gimp_scale_entry_new (GTK_TABLE (table), 0, 1,
                              _("_Upper threshold:"), SCALE_WIDTH, 4,
                              vpvals.upper_limit, 0, 255, 1, 8, 0,
                              TRUE, 0, 0,
                              NULL, NULL);
  g_signal_connect (adj, "value-changed",
                    G_CALLBACK (gimp_int_adjustment_update),
                    &vpvals.upper_limit);
  g_signal_connect_swapped (adj, "value-changed",
                            G_CALLBACK (gimp_preview_invalidate),
                            preview);

  adj = gimp_scale_entry_new (GTK_TABLE (table), 0, 2,
                              _("_Propagating rate:"), SCALE_WIDTH, 4,
                              vpvals.propagating_rate, 0, 1, 0.01, 0.1, 2,
                              TRUE, 0, 0,
                              NULL, NULL);
  g_signal_connect (adj, "value-changed",
                    G_CALLBACK (gimp_double_adjustment_update),
                    &vpvals.propagating_rate);
  g_signal_connect_swapped (adj, "value-changed",
                            G_CALLBACK (gimp_preview_invalidate),
                            preview);

  gtk_table_add_toggle (table, _("To l_eft"), 0, 1, 4,
                        G_CALLBACK (vpropagate_toggle_button_update),
                        &direction_mask_vec[Right2Left]);
  gtk_table_add_toggle (table, _("To _right"), 2, 3, 4,
                        G_CALLBACK (vpropagate_toggle_button_update),
                        &direction_mask_vec[Left2Right]);
  gtk_table_add_toggle (table, _("To _top"), 1, 2, 3,
                        G_CALLBACK (vpropagate_toggle_button_update),
                        &direction_mask_vec[Bottom2Top]);
  gtk_table_add_toggle (table, _("To _bottom"), 1, 2, 5,
                        G_CALLBACK (vpropagate_toggle_button_update),
                        &direction_mask_vec[Top2Bottom]);

  if (gimp_drawable_has_alpha (drawable->drawable_id))
    {
      GtkWidget *toggle;

      toggle =
        gtk_table_add_toggle (table, _("Propagating _alpha channel"),
                              0, 3, 6,
                              G_CALLBACK (vpropagate_toggle_button_update),
                              &propagate_alpha);

      if (gimp_layer_get_lock_alpha (drawable->drawable_id))
        {
          gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (toggle), 0);
          gtk_widget_set_sensitive (toggle, FALSE);
        }

      gtk_table_add_toggle (table, _("Propagating value channel"), 0, 3, 7,
                            G_CALLBACK (vpropagate_toggle_button_update),
                            &propagate_value);
    }

  gtk_widget_show (dialog);

  run = (gimp_dialog_run (GIMP_DIALOG (dialog)) == GTK_RESPONSE_OK);

  if (run)
    {
      gint i, result;

      for (i = result = 0; i < 4; i++)
        result |= (direction_mask_vec[i] ? 1 : 0) << i;
      vpvals.direction_mask = result;

      vpvals.propagating_channel = ((propagate_alpha ? PROPAGATING_ALPHA : 0) |
                                    (propagate_value ? PROPAGATING_VALUE : 0));
    }

  gtk_widget_destroy (dialog);

  return run;
}

static void
vpropagate_toggle_button_update (GtkWidget *widget,
                                 gpointer   data)
{
  gint i, result;

  gimp_toggle_button_update (widget, data);

  for (i = result = 0; i < 4; i++)
    result |= (direction_mask_vec[i] ? 1 : 0) << i;
  vpvals.direction_mask = result;

  vpvals.propagating_channel = ((propagate_alpha ? PROPAGATING_ALPHA : 0) |
                                (propagate_value ? PROPAGATING_VALUE : 0));
  gimp_preview_invalidate (GIMP_PREVIEW (preview));
}

static GtkWidget *
gtk_table_add_toggle (GtkWidget   *table,
                      const gchar *name,
                      gint         x1,
                      gint         x2,
                      gint         y,
                      GCallback    update,
                      gint        *value)
{
  GtkWidget *toggle;

  toggle = gtk_check_button_new_with_mnemonic(name);
  gtk_table_attach (GTK_TABLE (table), toggle, x1, x2, y, y+1,
                    GTK_FILL|GTK_EXPAND, 0, 0, 0);
  gtk_widget_show (toggle);

  g_signal_connect (toggle, "toggled",
                    G_CALLBACK (update),
                    value);

  gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (toggle), *value);

  return toggle;
}
