/* GIMP - The GNU Image Manipulation Program
 * Copyright (C) 1995 Spencer Kimball and Peter Mattis
 *
 * 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 <gegl.h>

#include "libgimpcolor/gimpcolor.h"

#include "core-types.h"

#include "base/pixel-processor.h"
#include "base/pixel-region.h"
#include "base/tile.h"
#include "base/tile-manager.h"

#include "gimpchannel.h"
#include "gimpimage.h"
#include "gimpimage-contiguous-region.h"
#include "gimppickable.h"


typedef struct
{
  GimpImage           *image;
  GimpImageType        type;
  gboolean             sample_merged;
  gboolean             antialias;
  gint                 threshold;
  gboolean             select_transparent;
  GimpSelectCriterion  select_criterion;
  gboolean             has_alpha;
  guchar               color[MAX_CHANNELS];
} ContinuousRegionData;


/*  local function prototypes  */

static void contiguous_region_by_color    (ContinuousRegionData *cont,
                                           PixelRegion          *imagePR,
                                           PixelRegion          *maskPR);

static gint pixel_difference              (const guchar        *col1,
                                           const guchar        *col2,
                                           gboolean             antialias,
                                           gint                 threshold,
                                           gint                 bytes,
                                           gboolean             has_alpha,
                                           gboolean             select_transparent,
                                           GimpSelectCriterion  select_criterion);
static void ref_tiles                     (TileManager         *src,
                                           TileManager         *mask,
                                           Tile               **s_tile,
                                           Tile               **m_tile,
                                           gint                 x,
                                           gint                 y,
                                           guchar             **s,
                                           guchar             **m);
static gboolean find_contiguous_segment   (GimpImage           *image,
                                           const guchar        *col,
                                           PixelRegion         *src,
                                           PixelRegion         *mask,
                                           gint                 width,
                                           gint                 bytes,
                                           GimpImageType        src_type,
                                           gboolean             has_alpha,
                                           gboolean             select_transparent,
                                           GimpSelectCriterion  select_criterion,
                                           gboolean             antialias,
                                           gint                 threshold,
                                           gint                 initial,
                                           gint                *start,
                                           gint                *end);
static void find_contiguous_region_helper (GimpImage           *image,
                                           PixelRegion         *mask,
                                           PixelRegion         *src,
                                           GimpImageType        src_type,
                                           gboolean             has_alpha,
                                           gboolean             select_transparent,
                                           GimpSelectCriterion  select_criterion,
                                           gboolean             antialias,
                                           gint                 threshold,
                                           gint                 x,
                                           gint                 y,
                                           const guchar        *col);


/*  public functions  */

GimpChannel *
gimp_image_contiguous_region_by_seed (GimpImage           *image,
                                      GimpDrawable        *drawable,
                                      gboolean             sample_merged,
                                      gboolean             antialias,
                                      gint                 threshold,
                                      gboolean             select_transparent,
                                      GimpSelectCriterion  select_criterion,
                                      gint                 x,
                                      gint                 y)
{
  PixelRegion    srcPR, maskPR;
  GimpPickable  *pickable;
  TileManager   *tiles;
  GimpChannel   *mask;
  GimpImageType  src_type;
  gboolean       has_alpha;
  gint           bytes;
  Tile          *tile;

  g_return_val_if_fail (GIMP_IS_IMAGE (image), NULL);
  g_return_val_if_fail (GIMP_IS_DRAWABLE (drawable), NULL);

  if (sample_merged)
    pickable = GIMP_PICKABLE (gimp_image_get_projection (image));
  else
    pickable = GIMP_PICKABLE (drawable);

  gimp_pickable_flush (pickable);

  src_type  = gimp_pickable_get_image_type (pickable);
  has_alpha = GIMP_IMAGE_TYPE_HAS_ALPHA (src_type);
  bytes     = GIMP_IMAGE_TYPE_BYTES (src_type);

  tiles = gimp_pickable_get_tiles (pickable);
  pixel_region_init (&srcPR, tiles,
                     0, 0,
                     tile_manager_width (tiles),
                     tile_manager_height (tiles),
                     FALSE);

  mask = gimp_channel_new_mask (image, srcPR.w, srcPR.h);
  pixel_region_init (&maskPR, gimp_drawable_get_tiles (GIMP_DRAWABLE (mask)),
                     0, 0,
                     gimp_item_get_width  (GIMP_ITEM (mask)),
                     gimp_item_get_height (GIMP_ITEM (mask)),
                     TRUE);

  tile = tile_manager_get_tile (srcPR.tiles, x, y, TRUE, FALSE);
  if (tile)
    {
      const guchar *start;
      guchar        start_col[MAX_CHANNELS];

      start = tile_data_pointer (tile, x, y);

      if (has_alpha)
        {
          if (select_transparent)
            {
              /*  don't select transparent regions if the start pixel isn't
               *  fully transparent
               */
              if (start[bytes - 1] > 0)
                select_transparent = FALSE;
            }
        }
      else
        {
          select_transparent = FALSE;
        }

      if (GIMP_IMAGE_TYPE_IS_INDEXED (src_type))
        {
          gimp_image_get_color (image, src_type, start, start_col);
        }
      else
        {
          gint i;

          for (i = 0; i < bytes; i++)
            start_col[i] = start[i];
        }

      find_contiguous_region_helper (image, &maskPR, &srcPR,
                                     src_type, has_alpha,
                                     select_transparent, select_criterion,
                                     antialias, threshold,
                                     x, y, start_col);

      tile_release (tile, FALSE);
    }

  return mask;
}

GimpChannel *
gimp_image_contiguous_region_by_color (GimpImage            *image,
                                       GimpDrawable         *drawable,
                                       gboolean              sample_merged,
                                       gboolean              antialias,
                                       gint                  threshold,
                                       gboolean              select_transparent,
                                       GimpSelectCriterion  select_criterion,
                                       const GimpRGB        *color)
{
  /*  Scan over the image's active layer, finding pixels within the
   *  specified threshold from the given R, G, & B values.  If
   *  antialiasing is on, use the same antialiasing scheme as in
   *  fuzzy_select.  Modify the image's mask to reflect the
   *  additional selection
   */
  GimpPickable *pickable;
  TileManager  *tiles;
  GimpChannel  *mask;
  PixelRegion   imagePR, maskPR;
  gint          width, height;

  ContinuousRegionData  cont;

  g_return_val_if_fail (GIMP_IS_IMAGE (image), NULL);
  g_return_val_if_fail (GIMP_IS_DRAWABLE (drawable), NULL);
  g_return_val_if_fail (color != NULL, NULL);

  gimp_rgba_get_uchar (color,
                       cont.color + 0,
                       cont.color + 1,
                       cont.color + 2,
                       cont.color + 3);

  if (sample_merged)
    pickable = GIMP_PICKABLE (gimp_image_get_projection (image));
  else
    pickable = GIMP_PICKABLE (drawable);

  gimp_pickable_flush (pickable);

  cont.type      = gimp_pickable_get_image_type (pickable);
  cont.has_alpha = GIMP_IMAGE_TYPE_HAS_ALPHA (cont.type);

  tiles  = gimp_pickable_get_tiles (pickable);
  width  = tile_manager_width (tiles);
  height = tile_manager_height (tiles);

  pixel_region_init (&imagePR, tiles, 0, 0, width, height, FALSE);

  if (cont.has_alpha)
    {
      if (select_transparent)
        {
          /*  don't select transparancy if "color" isn't fully transparent
           */
          if (cont.color[3] > 0)
            select_transparent = FALSE;
        }
    }
  else
    {
      select_transparent = FALSE;
    }

  cont.image              = image;
  cont.antialias          = antialias;
  cont.threshold          = threshold;
  cont.select_transparent = select_transparent;
  cont.select_criterion   = select_criterion;

  mask = gimp_channel_new_mask (image, width, height);

  pixel_region_init (&maskPR, gimp_drawable_get_tiles (GIMP_DRAWABLE (mask)),
                     0, 0, width, height,
                     TRUE);

  pixel_regions_process_parallel ((PixelProcessorFunc)
                                  contiguous_region_by_color, &cont,
                                  2, &imagePR, &maskPR);

  return mask;
}


/*  private functions  */

static void
contiguous_region_by_color (ContinuousRegionData *cont,
                            PixelRegion          *imagePR,
                            PixelRegion          *maskPR)
{
  const guchar *image = imagePR->data;
  guchar       *mask  = maskPR->data;
  gint          x, y;

  for (y = 0; y < imagePR->h; y++)
    {
      const guchar *i = image;
      guchar       *m = mask;

      for (x = 0; x < imagePR->w; x++)
        {
          guchar  rgb[MAX_CHANNELS];

          /*  Get the rgb values for the color  */
          gimp_image_get_color (cont->image, cont->type, i, rgb);

          /*  Find how closely the colors match  */
          *m++ = pixel_difference (cont->color, rgb,
                                   cont->antialias,
                                   cont->threshold,
                                   cont->has_alpha ? 4 : 3,
                                   cont->has_alpha,
                                   cont->select_transparent,
                                   cont->select_criterion);

          i += imagePR->bytes;
        }

      image += imagePR->rowstride;
      mask += maskPR->rowstride;
    }
}

static gint
pixel_difference (const guchar        *col1,
                  const guchar        *col2,
                  gboolean             antialias,
                  gint                 threshold,
                  gint                 bytes,
                  gboolean             has_alpha,
                  gboolean             select_transparent,
                  GimpSelectCriterion  select_criterion)
{
  gint max = 0;

  /*  if there is an alpha channel, never select transparent regions  */
  if (! select_transparent && has_alpha && col2[bytes - 1] == 0)
    return 0;

  if (select_transparent && has_alpha)
    {
      max = abs (col1[bytes - 1] - col2[bytes - 1]);
    }
  else
    {
      gint diff;
      gint b;
      gint av0, av1, av2;
      gint bv0, bv1, bv2;

      if (has_alpha)
        bytes--;

      switch (select_criterion)
        {
        case GIMP_SELECT_CRITERION_COMPOSITE:
          for (b = 0; b < bytes; b++)
            {
              diff = abs (col1[b] - col2[b]);
              if (diff > max)
                max = diff;
            }
          break;

        case GIMP_SELECT_CRITERION_R:
          max = abs (col1[0] - col2[0]);
          break;

        case GIMP_SELECT_CRITERION_G:
          max = abs (col1[1] - col2[1]);
          break;

        case GIMP_SELECT_CRITERION_B:
          max = abs (col1[2] - col2[2]);
          break;

        case GIMP_SELECT_CRITERION_H:
          av0 = (gint) col1[0];
          av1 = (gint) col1[1];
          av2 = (gint) col1[2];
          bv0 = (gint) col2[0];
          bv1 = (gint) col2[1];
          bv2 = (gint) col2[2];
          gimp_rgb_to_hsv_int (&av0, &av1, &av2);
          gimp_rgb_to_hsv_int (&bv0, &bv1, &bv2);
          /* wrap around candidates for the actual distance */
          {
            gint dist1 = abs (av0 - bv0);
            gint dist2 = abs (av0 - 360 - bv0);
            gint dist3 = abs (av0 - bv0 + 360);
            max = MIN (dist1, dist2);
            if (max > dist3)
              max = dist3;
          }
          break;

        case GIMP_SELECT_CRITERION_S:
          av0 = (gint) col1[0];
          av1 = (gint) col1[1];
          av2 = (gint) col1[2];
          bv0 = (gint) col2[0];
          bv1 = (gint) col2[1];
          bv2 = (gint) col2[2];
          gimp_rgb_to_hsv_int (&av0, &av1, &av2);
          gimp_rgb_to_hsv_int (&bv0, &bv1, &bv2);
          max = abs (av1 - bv1);
          break;

        case GIMP_SELECT_CRITERION_V:
          av0 = (gint) col1[0];
          av1 = (gint) col1[1];
          av2 = (gint) col1[2];
          bv0 = (gint) col2[0];
          bv1 = (gint) col2[1];
          bv2 = (gint) col2[2];
          gimp_rgb_to_hsv_int (&av0, &av1, &av2);
          gimp_rgb_to_hsv_int (&bv0, &bv1, &bv2);
          max = abs (av2 - bv2);
          break;
        }
    }

  if (antialias && threshold > 0)
    {
      gfloat aa = 1.5 - ((gfloat) max / threshold);

      if (aa <= 0.0)
        return 0;
      else if (aa < 0.5)
        return (guchar) (aa * 512);
      else
        return 255;
    }
  else
    {
      if (max > threshold)
        return 0;
      else
        return 255;
    }
}

static void
ref_tiles (TileManager  *src,
           TileManager  *mask,
           Tile        **s_tile,
           Tile        **m_tile,
           gint          x,
           gint          y,
           guchar      **s,
           guchar      **m)
{
  if (*s_tile != NULL)
    tile_release (*s_tile, FALSE);
  if (*m_tile != NULL)
    tile_release (*m_tile, TRUE);

  *s_tile = tile_manager_get_tile (src, x, y, TRUE, FALSE);
  *m_tile = tile_manager_get_tile (mask, x, y, TRUE, TRUE);

  *s = tile_data_pointer (*s_tile, x, y);
  *m = tile_data_pointer (*m_tile, x, y);
}

static gboolean
find_contiguous_segment (GimpImage           *image,
                         const guchar        *col,
                         PixelRegion         *src,
                         PixelRegion         *mask,
                         gint                 width,
                         gint                 bytes,
                         GimpImageType        src_type,
                         gboolean             has_alpha,
                         gboolean             select_transparent,
                         GimpSelectCriterion  select_criterion,
                         gboolean             antialias,
                         gint                 threshold,
                         gint                 initial,
                         gint                *start,
                         gint                *end)
{
  guchar *s;
  guchar *m;
  guchar  s_color[MAX_CHANNELS];
  guchar  diff;
  gint    col_bytes = bytes;
  Tile   *s_tile    = NULL;
  Tile   *m_tile    = NULL;

  ref_tiles (src->tiles, mask->tiles,
             &s_tile, &m_tile, src->x, src->y, &s, &m);

  if (GIMP_IMAGE_TYPE_IS_INDEXED (src_type))
    {
      col_bytes = has_alpha ? 4 : 3;

      gimp_image_get_color (image, src_type, s, s_color);

      diff = pixel_difference (col, s_color, antialias, threshold,
                               col_bytes, has_alpha, select_transparent,
                               select_criterion);
     }
  else
    {
      diff = pixel_difference (col, s, antialias, threshold,
                               col_bytes, has_alpha, select_transparent,
                               select_criterion);
    }

  /* check the starting pixel */
  if (! diff)
    {
      tile_release (s_tile, FALSE);
      tile_release (m_tile, TRUE);
      return FALSE;
    }

  *m-- = diff;
  s -= bytes;
  *start = initial - 1;

  while (*start >= 0 && diff)
    {
      if (! ((*start + 1) % TILE_WIDTH))
        ref_tiles (src->tiles, mask->tiles,
                   &s_tile, &m_tile, *start, src->y, &s, &m);

      if (GIMP_IMAGE_TYPE_IS_INDEXED (src_type))
        {
          gimp_image_get_color (image, src_type, s, s_color);

          diff = pixel_difference (col, s_color, antialias, threshold,
                                   col_bytes, has_alpha, select_transparent,
                                   select_criterion);
        }
      else
        {
          diff = pixel_difference (col, s, antialias, threshold,
                                   col_bytes, has_alpha, select_transparent,
                                   select_criterion);
        }

      if ((*m-- = diff))
        {
          s -= bytes;
          (*start)--;
        }
    }

  diff = 1;
  *end = initial + 1;

  if (*end % TILE_WIDTH && *end < width)
    ref_tiles (src->tiles, mask->tiles,
               &s_tile, &m_tile, *end, src->y, &s, &m);

  while (*end < width && diff)
    {
      if (! (*end % TILE_WIDTH))
        ref_tiles (src->tiles, mask->tiles,
                   &s_tile, &m_tile, *end, src->y, &s, &m);

      if (GIMP_IMAGE_TYPE_IS_INDEXED (src_type))
        {
          gimp_image_get_color (image, src_type, s, s_color);

          diff = pixel_difference (col, s_color, antialias, threshold,
                                   col_bytes, has_alpha, select_transparent,
                                   select_criterion);
        }
      else
        {
          diff = pixel_difference (col, s, antialias, threshold,
                                   col_bytes, has_alpha, select_transparent,
                                   select_criterion);
        }

      if ((*m++ = diff))
        {
          s += bytes;
          (*end)++;
        }
    }

  tile_release (s_tile, FALSE);
  tile_release (m_tile, TRUE);

  return TRUE;
}

static void
find_contiguous_region_helper (GimpImage           *image,
                               PixelRegion         *mask,
                               PixelRegion         *src,
                               GimpImageType        src_type,
                               gboolean             has_alpha,
                               gboolean             select_transparent,
                               GimpSelectCriterion  select_criterion,
                               gboolean             antialias,
                               gint                 threshold,
                               gint                 x,
                               gint                 y,
                               const guchar        *col)
{
  gint    start, end;
  gint    new_start, new_end;
  gint    val;
  Tile   *tile;
  GQueue *coord_stack;

  coord_stack = g_queue_new ();

  /* To avoid excessive memory allocation (y, start, end) tuples are
   * stored in interleaved format:
   *
   * [y1] [start1] [end1] [y2] [start2] [end2]
   */
  g_queue_push_tail (coord_stack, GINT_TO_POINTER (y));
  g_queue_push_tail (coord_stack, GINT_TO_POINTER (x - 1));
  g_queue_push_tail (coord_stack, GINT_TO_POINTER (x + 1));

  do
    {
      y     = GPOINTER_TO_INT (g_queue_pop_head (coord_stack));
      start = GPOINTER_TO_INT (g_queue_pop_head (coord_stack));
      end   = GPOINTER_TO_INT (g_queue_pop_head (coord_stack));

      for (x = start + 1; x < end; x++)
        {
          tile = tile_manager_get_tile (mask->tiles, x, y, TRUE, FALSE);
          val = *(const guchar *) tile_data_pointer (tile, x, y);
          tile_release (tile, FALSE);
          if (val != 0)
            continue;

          src->x = x;
          src->y = y;

          if (! find_contiguous_segment (image, col, src, mask, src->w,
                                         src->bytes, src_type, has_alpha,
                                         select_transparent, select_criterion,
                                         antialias, threshold, x,
                                         &new_start, &new_end))
            continue;

          if (y + 1 < src->h)
            {
              g_queue_push_tail (coord_stack, GINT_TO_POINTER (y + 1));
              g_queue_push_tail (coord_stack, GINT_TO_POINTER (new_start));
              g_queue_push_tail (coord_stack, GINT_TO_POINTER (new_end));
            }

          if (y - 1 >= 0)
            {
              g_queue_push_tail (coord_stack, GINT_TO_POINTER (y - 1));
              g_queue_push_tail (coord_stack, GINT_TO_POINTER (new_start));
              g_queue_push_tail (coord_stack, GINT_TO_POINTER (new_end));
            }
        }
    }
  while (! g_queue_is_empty (coord_stack));

  g_queue_free (coord_stack);
}
