/* The GIMP -- an 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 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

#include "config.h"

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

#include <glib-object.h>

#include "libgimpcolor/gimpcolor.h"
#include "libgimpmath/gimpmath.h"
#include "libgimpbase/gimpbase.h"

#include "core-types.h"

#include "base/temp-buf.h"

#include "gimpimage.h"
#include "gimpgradient.h"

#include "errors.h"

#include "libgimp/gimpintl.h"


#define EPSILON 1e-10


static void       gimp_gradient_class_init      (GimpGradientClass *klass);
static void       gimp_gradient_init            (GimpGradient      *gradient);

static void       gimp_gradient_finalize        (GObject           *object);

static TempBuf  * gimp_gradient_get_new_preview (GimpViewable      *viewable,
						 gint               width,
						 gint               height);
static void       gimp_gradient_dirty           (GimpData          *data);
static gboolean   gimp_gradient_save            (GimpData          *data);
static gchar    * gimp_gradient_get_extension   (GimpData          *data);
static GimpData * gimp_gradient_duplicate       (GimpData          *data);

static gdouble    gimp_gradient_calc_linear_factor            (gdouble  middle,
							       gdouble  pos);
static gdouble    gimp_gradient_calc_curved_factor            (gdouble  middle,
							       gdouble  pos);
static gdouble    gimp_gradient_calc_sine_factor              (gdouble  middle,
							       gdouble  pos);
static gdouble    gimp_gradient_calc_sphere_increasing_factor (gdouble  middle,
							       gdouble  pos);
static gdouble    gimp_gradient_calc_sphere_decreasing_factor (gdouble  middle,
							       gdouble  pos);


static GimpDataClass *parent_class = NULL;


GType
gimp_gradient_get_type (void)
{
  static GType gradient_type = 0;

  if (! gradient_type)
    {
      static const GTypeInfo gradient_info =
      {
        sizeof (GimpGradientClass),
	(GBaseInitFunc) NULL,
	(GBaseFinalizeFunc) NULL,
	(GClassInitFunc) gimp_gradient_class_init,
	NULL,           /* class_finalize */
	NULL,           /* class_data     */
	sizeof (GimpGradient),
	0,              /* n_preallocs    */
	(GInstanceInitFunc) gimp_gradient_init,
      };

      gradient_type = g_type_register_static (GIMP_TYPE_DATA,
					      "GimpGradient",
					      &gradient_info, 0);
  }

  return gradient_type;
}

static void
gimp_gradient_class_init (GimpGradientClass *klass)
{
  GObjectClass      *object_class;
  GimpViewableClass *viewable_class;
  GimpDataClass     *data_class;

  object_class   = G_OBJECT_CLASS (klass);
  viewable_class = GIMP_VIEWABLE_CLASS (klass);
  data_class     = GIMP_DATA_CLASS (klass);

  parent_class = g_type_class_peek_parent (klass);

  object_class->finalize          = gimp_gradient_finalize;

  viewable_class->get_new_preview = gimp_gradient_get_new_preview;

  data_class->dirty               = gimp_gradient_dirty;
  data_class->save                = gimp_gradient_save;
  data_class->get_extension       = gimp_gradient_get_extension;
  data_class->duplicate           = gimp_gradient_duplicate;
}

static void
gimp_gradient_init (GimpGradient *gradient)
{
  gradient->segments     = NULL;
  gradient->last_visited = NULL;
}

static void
gimp_gradient_finalize (GObject *object)
{
  GimpGradient *gradient;

  gradient = GIMP_GRADIENT (object);

  if (gradient->segments)
    {
      gimp_gradient_segments_free (gradient->segments);
      gradient->segments = NULL;
    }

  G_OBJECT_CLASS (parent_class)->finalize (object);
}

static TempBuf *
gimp_gradient_get_new_preview (GimpViewable *viewable,
			       gint          width,
			       gint          height)
{
  GimpGradient *gradient;
  TempBuf      *temp_buf;
  guchar       *buf;
  guchar       *p0, *p1;
  guchar       *even, *odd;
  gint          x, y;
  gdouble       dx, cur_x;
  GimpRGB       color;
  gdouble       c0, c1;

  gradient = GIMP_GRADIENT (viewable);

  dx    = 1.0 / (width - 1);
  cur_x = 0.0;
  p0    = even = g_malloc (width * 3);
  p1    = odd  = g_malloc (width * 3);

  /* Create lines to fill the image */

  for (x = 0; x < width; x++)
    {
      gimp_gradient_get_color_at (gradient, cur_x, &color);

      if ((x / GIMP_CHECK_SIZE_SM) & 1)
        {
          c0 = GIMP_CHECK_LIGHT;
          c1 = GIMP_CHECK_DARK;
        }
      else
        {
          c0 = GIMP_CHECK_DARK;
          c1 = GIMP_CHECK_LIGHT;
        }

      *p0++ = (c0 + (color.r - c0) * color.a) * 255.0;
      *p0++ = (c0 + (color.g - c0) * color.a) * 255.0;
      *p0++ = (c0 + (color.b - c0) * color.a) * 255.0;

      *p1++ = (c1 + (color.r - c1) * color.a) * 255.0;
      *p1++ = (c1 + (color.g - c1) * color.a) * 255.0;
      *p1++ = (c1 + (color.b - c1) * color.a) * 255.0;

      cur_x += dx;
    }

  temp_buf = temp_buf_new (width, height,
			   3,
			   0, 0,
			   NULL);

  buf = temp_buf_data (temp_buf);

  for (y = 0; y < height; y++)
    {
      if ((y / GIMP_CHECK_SIZE_SM) & 1)
        {
          memcpy (buf + (width * y * 3), odd, width * 3); 
        }
      else
        {
          memcpy (buf + (width * y * 3), even, width * 3); 
        }
    }

  g_free (even);
  g_free (odd);

  return temp_buf;
}

static GimpData *
gimp_gradient_duplicate (GimpData *data)
{
  GimpGradient        *gradient;
  GimpGradientSegment *head, *prev, *cur, *orig;

  gradient = GIMP_GRADIENT (g_object_new (GIMP_TYPE_GRADIENT, NULL));

  gimp_data_dirty (GIMP_DATA (gradient));

  prev = NULL;
  orig = GIMP_GRADIENT (data)->segments;
  head = NULL;

  while (orig)
    {
      cur = gimp_gradient_segment_new ();

      *cur = *orig;  /* Copy everything */

      cur->prev = prev;
      cur->next = NULL;

      if (prev)
	prev->next = cur;
      else
	head = cur;  /* Remember head */

      prev = cur;
      orig = orig->next;
    }

  gradient->segments = head;

  return GIMP_DATA (gradient);
}

GimpData *
gimp_gradient_new (const gchar *name)
{
  GimpGradient *gradient;

  g_return_val_if_fail (name != NULL, NULL);

  gradient = GIMP_GRADIENT (g_object_new (GIMP_TYPE_GRADIENT, NULL));

  gimp_object_set_name (GIMP_OBJECT (gradient), name);

  gradient->segments = gimp_gradient_segment_new ();

  return GIMP_DATA (gradient);
}

GimpData *
gimp_gradient_get_standard (void)
{
  static GimpGradient *standard_gradient = NULL;

  if (! standard_gradient)
    {
      standard_gradient = GIMP_GRADIENT (gimp_gradient_new ("Standard"));

      g_object_ref (G_OBJECT (standard_gradient));
    }

  return GIMP_DATA (standard_gradient);
}

GimpData *
gimp_gradient_load (const gchar *filename)
{
  GimpGradient        *gradient;
  GimpGradientSegment *seg;
  GimpGradientSegment *prev;
  gint                 num_segments;
  gint                 i;
  gint                 type, color;
  FILE                *file;
  gchar                line[1024];

  g_return_val_if_fail (filename != NULL, NULL);

  file = fopen (filename, "rb");
  if (!file)
    return NULL;

  fgets (line, 1024, file);
  if (strcmp (line, "GIMP Gradient\n") != 0)
    {
      fclose (file);
      return NULL;
    }

  gradient = GIMP_GRADIENT (g_object_new (GIMP_TYPE_GRADIENT, NULL));

  gimp_data_set_filename (GIMP_DATA (gradient), filename);

  fgets (line, 1024, file);
  if (! strncmp (line, "Name: ", strlen ("Name: ")))
    {
      if (g_utf8_validate (line, -1, NULL))
        {
          gimp_object_set_name (GIMP_OBJECT (gradient),
                                g_strstrip (&line[strlen ("Name: ")]));
        }
      else
        {
          g_message (_("Invalid UTF-8 string in GIMP gradient file \"%s\"."), 
                     filename);
          gimp_object_set_name (GIMP_OBJECT (gradient), _("Unnamed"));
        }

      fgets (line, 1024, file);
    }
  else /* old gradient format */
    {
      gchar *basename;

      basename = g_path_get_basename (filename);

      gimp_object_set_name (GIMP_OBJECT (gradient), basename);

      g_free (basename);
    }

  num_segments = atoi (line);

  if (num_segments < 1)
    {
      g_message ("%s(): invalid number of segments in \"%s\"",
		 G_GNUC_FUNCTION, filename);
      g_object_unref (G_OBJECT (gradient));
      fclose (file);
      return NULL;
    }

  prev = NULL;

  for (i = 0; i < num_segments; i++)
    {
      seg = gimp_gradient_segment_new ();

      seg->prev = prev;

      if (prev)
	prev->next = seg;
      else
	gradient->segments = seg;

      fgets (line, 1024, file);

      if (sscanf (line, "%lf%lf%lf%lf%lf%lf%lf%lf%lf%lf%lf%d%d",
		  &(seg->left), &(seg->middle), &(seg->right),
		  &(seg->left_color.r),
		  &(seg->left_color.g),
		  &(seg->left_color.b),
		  &(seg->left_color.a),
		  &(seg->right_color.r),
		  &(seg->right_color.g),
		  &(seg->right_color.b),
		  &(seg->right_color.a),
		  &type, &color) != 13)
	{
	  g_message ("%s(): badly formatted gradient segment %d in \"%s\" --- "
		     "bad things may happen soon",
		     G_GNUC_FUNCTION, i, filename);
	}
      else
	{
	  seg->type  = (GimpGradientSegmentType) type;
	  seg->color = (GimpGradientSegmentColor) color;
	}

      prev = seg;
    }

  fclose (file);

  GIMP_DATA (gradient)->dirty = FALSE;

  return GIMP_DATA (gradient);
}

static void
gimp_gradient_dirty (GimpData *data)
{
  GimpGradient *gradient;

  gradient = GIMP_GRADIENT (data);

  gradient->last_visited = NULL;

  if (GIMP_DATA_CLASS (parent_class)->dirty)
    GIMP_DATA_CLASS (parent_class)->dirty (data);
}

static gboolean
gimp_gradient_save (GimpData *data)
{
  GimpGradient        *gradient;
  GimpGradientSegment *seg;
  gint                 num_segments;
  FILE                *file;

  gradient = GIMP_GRADIENT (data);

  file = fopen (data->filename, "wb");
  if (! file)
    {
      g_message ("%s(): can't open \"%s\"",
		 G_GNUC_FUNCTION, data->filename);
      return FALSE;
    }

  /* File format is:
   *
   *   GIMP Gradient
   *   Name: name
   *   number_of_segments
   *   left middle right r0 g0 b0 a0 r1 g1 b1 a1 type coloring
   *   left middle right r0 g0 b0 a0 r1 g1 b1 a1 type coloring
   *   ...
   */

  fprintf (file, "GIMP Gradient\n");

  fprintf (file, "Name: %s\n", GIMP_OBJECT (gradient)->name);

  /* Count number of segments */
  num_segments = 0;
  seg          = gradient->segments;

  while (seg)
    {
      num_segments++;
      seg = seg->next;
    }

  /* Write rest of file */
  fprintf (file, "%d\n", num_segments);

  for (seg = gradient->segments; seg; seg = seg->next)
    {
      fprintf (file, "%f %f %f %f %f %f %f %f %f %f %f %d %d\n",
	       seg->left, seg->middle, seg->right,
	       seg->left_color.r,
	       seg->left_color.g,
	       seg->left_color.b,
	       seg->left_color.a,
	       seg->right_color.r,
	       seg->right_color.g,
	       seg->right_color.b,
	       seg->right_color.a,
	       (gint) seg->type,
	       (gint) seg->color);
    }

  fclose (file);

  return TRUE;
}

static gchar *
gimp_gradient_get_extension (GimpData *data)
{
  return GIMP_GRADIENT_FILE_EXTENSION;
}

void
gimp_gradient_get_color_at (GimpGradient *gradient,
			    gdouble     pos,
			    GimpRGB    *color)
{
  gdouble              factor = 0.0;
  GimpGradientSegment *seg;
  gdouble              seg_len;
  gdouble              middle;
  GimpRGB              rgb;

  g_return_if_fail (GIMP_IS_GRADIENT (gradient));
  g_return_if_fail (color != NULL);

  /* if there is no gradient return a totally transparent black */
  if (gradient == NULL) 
    {
      gimp_rgba_set (color, 0.0, 0.0, 0.0, 0.0);
      return;
    }

  if (pos < 0.0)
    pos = 0.0;
  else if (pos > 1.0)
    pos = 1.0;

  seg = gimp_gradient_get_segment_at (gradient, pos);

  seg_len = seg->right - seg->left;

  if (seg_len < EPSILON)
    {
      middle = 0.5;
      pos    = 0.5;
    }
  else
    {
      middle = (seg->middle - seg->left) / seg_len;
      pos    = (pos - seg->left) / seg_len;
    }

  switch (seg->type)
    {
    case GRAD_LINEAR:
      factor = gimp_gradient_calc_linear_factor (middle, pos);
      break;

    case GRAD_CURVED:
      factor = gimp_gradient_calc_curved_factor (middle, pos);
      break;

    case GRAD_SINE:
      factor = gimp_gradient_calc_sine_factor (middle, pos);
      break;

    case GRAD_SPHERE_INCREASING:
      factor = gimp_gradient_calc_sphere_increasing_factor (middle, pos);
      break;

    case GRAD_SPHERE_DECREASING:
      factor = gimp_gradient_calc_sphere_decreasing_factor (middle, pos);
      break;

    default:
      gimp_fatal_error ("%s(): Unknown gradient type %d",
			G_GNUC_FUNCTION, (gint) seg->type);
      break;
    }

  /* Calculate color components */

  rgb.a = seg->left_color.a + (seg->right_color.a - seg->left_color.a) * factor;

  if (seg->color == GRAD_RGB)
    {
      rgb.r 
	= seg->left_color.r + (seg->right_color.r - seg->left_color.r) * factor;

      rgb.g =
	seg->left_color.g + (seg->right_color.g - seg->left_color.g) * factor;

      rgb.b =
	seg->left_color.b + (seg->right_color.b - seg->left_color.b) * factor;
    }
  else
    {
      GimpHSV left_hsv;
      GimpHSV right_hsv;

      gimp_rgb_to_hsv (&seg->left_color,  &left_hsv);
      gimp_rgb_to_hsv (&seg->right_color, &right_hsv);

      left_hsv.s = left_hsv.s + (right_hsv.s - left_hsv.s) * factor;
      left_hsv.v = left_hsv.v + (right_hsv.v - left_hsv.v) * factor;

      switch (seg->color)
	{
	case GRAD_HSV_CCW:
	  if (left_hsv.h < right_hsv.h)
	    {
	      left_hsv.h += (right_hsv.h - left_hsv.h) * factor;
	    }
	  else
	    {
	      left_hsv.h += (1.0 - (left_hsv.h - right_hsv.h)) * factor;

	      if (left_hsv.h > 1.0)
		left_hsv.h -= 1.0;
	    }
	  break;

	case GRAD_HSV_CW:
	  if (right_hsv.h < left_hsv.h)
	    {
	      left_hsv.h -= (left_hsv.h - right_hsv.h) * factor;
	    }
	  else
	    {
	      left_hsv.h -= (1.0 - (right_hsv.h - left_hsv.h)) * factor;

	      if (left_hsv.h < 0.0)
		left_hsv.h += 1.0;
	    }
	  break;

	default:
	  gimp_fatal_error ("%s(): Unknown coloring mode %d",
			    G_GNUC_FUNCTION, (gint) seg->color);
	  break;
	}

      gimp_hsv_to_rgb (&left_hsv, &rgb);
    }

  *color = rgb;
}

GimpGradientSegment *
gimp_gradient_get_segment_at (GimpGradient *gradient,
			      gdouble       pos)
{
  GimpGradientSegment *seg;

  g_return_val_if_fail (GIMP_IS_GRADIENT (gradient), NULL);

  /* handle FP imprecision at the edges of the gradient */
  pos = CLAMP (pos, 0.0, 1.0);

  if (gradient->last_visited)
    seg = gradient->last_visited;
  else
    seg = gradient->segments;

  while (seg)
    {
      if (pos >= seg->left)
	{
	  if (pos <= seg->right)
	    {
	      gradient->last_visited = seg; /* for speed */
	      return seg;
	    }
	  else
	    {
	      seg = seg->next;
	    }
	}
      else
	{
	  seg = seg->prev;
	}
    }

  /* Oops: we should have found a segment, but we didn't */
  gimp_fatal_error ("%s(): no matching segment for position %0.15f",
		    G_GNUC_FUNCTION, pos);

  return NULL;
}

static gdouble
gimp_gradient_calc_linear_factor (gdouble middle,
				  gdouble pos)
{
  if (pos <= middle)
    {
      if (middle < EPSILON)
	return 0.0;
      else
	return 0.5 * pos / middle;
    }
  else
    {
      pos -= middle;
      middle = 1.0 - middle;

      if (middle < EPSILON)
	return 1.0;
      else
	return 0.5 + 0.5 * pos / middle;
    }
}

static gdouble
gimp_gradient_calc_curved_factor (gdouble middle,
				  gdouble pos)
{
  if (middle < EPSILON)
    middle = EPSILON;

  return pow(pos, log (0.5) / log (middle));
}

static gdouble
gimp_gradient_calc_sine_factor (gdouble middle,
				gdouble pos)
{
  pos = gimp_gradient_calc_linear_factor (middle, pos);

  return (sin ((-G_PI / 2.0) + G_PI * pos) + 1.0) / 2.0;
}

static gdouble
gimp_gradient_calc_sphere_increasing_factor (gdouble middle,
					     gdouble pos)
{
  pos = gimp_gradient_calc_linear_factor (middle, pos) - 1.0;

  return sqrt (1.0 - pos * pos); /* Works for convex increasing and concave decreasing */
}

static gdouble
gimp_gradient_calc_sphere_decreasing_factor (gdouble middle,
					     gdouble pos)
{
  pos = gimp_gradient_calc_linear_factor (middle, pos);

  return 1.0 - sqrt(1.0 - pos * pos); /* Works for convex decreasing and concave increasing */
}


/*  gradient segment functions  */

GimpGradientSegment *
gimp_gradient_segment_new (void)
{
  GimpGradientSegment *seg;

  seg = g_new (GimpGradientSegment, 1);

  seg->left   = 0.0;
  seg->middle = 0.5;
  seg->right  = 1.0;

  gimp_rgba_set (&seg->left_color,  0.0, 0.0, 0.0, 1.0);
  gimp_rgba_set (&seg->right_color, 1.0, 1.0, 1.0, 1.0);

  seg->type  = GRAD_LINEAR;
  seg->color = GRAD_RGB;

  seg->prev = seg->next = NULL;

  return seg;
}


void
gimp_gradient_segment_free (GimpGradientSegment *seg)
{
  g_return_if_fail (seg != NULL);

  g_free (seg);
}

void
gimp_gradient_segments_free (GimpGradientSegment *seg)
{
  GimpGradientSegment *tmp;

  g_return_if_fail (seg != NULL);

  while (seg)
    {
      tmp = seg->next;
      gimp_gradient_segment_free (seg);
      seg = tmp;
    }
}

GimpGradientSegment *
gimp_gradient_segment_get_last (GimpGradientSegment *seg)
{
  if (!seg)
    return NULL;

  while (seg->next)
    seg = seg->next;

  return seg;
}
