#include "gskgradientprivate.h"

GskGradient *
gsk_gradient_new (void)
{
  GskGradient *gradient = g_new0 (GskGradient, 1);

  gradient_stops_init (&gradient->stops);

  gradient->interpolation = GDK_COLOR_STATE_SRGB;
  gradient->hue_interpolation = GSK_HUE_INTERPOLATION_SHORTER;
  gradient->premultiplied = TRUE;
  gradient->repeat = GSK_REPEAT_PAD;
  gradient->opaque = TRUE;

  return gradient;
}

GskGradient *
gsk_gradient_copy (const GskGradient *gradient)
{
  return gsk_gradient_init_copy (gsk_gradient_new (), gradient);
}

GskGradient *
gsk_gradient_init_copy (GskGradient       *gradient,
                        const GskGradient *orig)
{
  gradient_stops_splice (&gradient->stops,
                         0, 0, FALSE,
                         gradient_stops_get_data (&orig->stops),
                         gradient_stops_get_size (&orig->stops));

  for (unsigned int i = 0; i < gradient_stops_get_size (&gradient->stops); i++)
    {
      GskGradientStop *s = gradient_stops_index (&gradient->stops, i);
      gdk_color_state_ref (s->color.color_state);
    }

  gradient->interpolation = gdk_color_state_ref (orig->interpolation);
  gradient->hue_interpolation = orig->hue_interpolation;
  gradient->premultiplied = orig->premultiplied;
  gradient->repeat = orig->repeat;
  gradient->opaque = orig->opaque;

  return gradient;
}

void
gsk_gradient_clear (GskGradient *gradient)
{
  gradient_stops_clear (&gradient->stops);
  gdk_color_state_unref (gradient->interpolation);
  g_free (gradient->rgba_stops);
}

void
gsk_gradient_free (GskGradient *gradient)
{
  gsk_gradient_clear (gradient);
  g_free (gradient);
}

gboolean
gsk_gradient_equal (const GskGradient *gradient0,
                    const GskGradient *gradient1)
{
  if (gradient0->repeat != gradient1->repeat ||
      gradient0->hue_interpolation != gradient1->hue_interpolation ||
      gradient0->premultiplied != gradient1->premultiplied ||
      gradient_stops_get_size (&gradient0->stops) != gradient_stops_get_size (&gradient1->stops) ||
      !gdk_color_state_equal (gradient0->interpolation, gradient1->interpolation))
    return FALSE;

  for (gsize i = 0; i < gradient_stops_get_size (&gradient0->stops); i++)
    {
      GskGradientStop *stop0 = gradient_stops_index (&gradient0->stops, i);
      GskGradientStop *stop1 = gradient_stops_index (&gradient1->stops, i);

      if (stop0->offset != stop1->offset ||
          stop0->transition_hint != stop1->transition_hint ||
          !gdk_color_equal (&stop0->color, &stop1->color))
        return FALSE;
    }

  return TRUE;
}

void
gsk_gradient_add_stop (GskGradient    *gradient,
                       float           offset,
                       float           transition_hint,
                       const GdkColor *color)
{
  GskGradientStop stop;

  g_return_if_fail (0 <= offset && offset <= 1);

  if (gradient_stops_get_size (&gradient->stops) > 0)
    g_return_if_fail (offset >= gradient_stops_index (&gradient->stops, gradient_stops_get_size (&gradient->stops) - 1)->offset);

  stop.offset = offset;
  stop.transition_hint = transition_hint;
  gdk_color_init_copy (&stop.color, color);
  gradient_stops_append (&gradient->stops, &stop);

  gradient->opaque = gradient->opaque & gdk_color_is_opaque (color);
}

void
gsk_gradient_add_color_stops (GskGradient        *gradient,
                              const GskColorStop *stops,
                              gsize               n_stops)
{
  for (gsize i = 0; i < n_stops; i++)
    {
      GskGradientStop stop;

      stop.offset = stops[i].offset;
      stop.transition_hint = 0.5;
      gdk_color_init_from_rgba (&stop.color, &stops[i].color);
      gradient_stops_append (&gradient->stops, &stop);
      gradient->opaque = gradient->opaque & gdk_color_is_opaque (&stop.color);
    }
}

void
gsk_gradient_set_repeat (GskGradient *gradient,
                         GskRepeat    repeat)
{
  gradient->repeat = repeat;
}

void
gsk_gradient_set_interpolation (GskGradient   *gradient,
                                GdkColorState *interpolation)
{
  gdk_color_state_unref (gradient->interpolation);
  gradient->interpolation = gdk_color_state_ref (interpolation);
}

void
gsk_gradient_set_hue_interpolation (GskGradient         *gradient,
                                    GskHueInterpolation  hue_interpolation)
{
  gradient->hue_interpolation = hue_interpolation;
}

void
gsk_gradient_set_premultiplied (GskGradient *gradient,
                                gboolean     premultiplied)
{
  gradient->premultiplied = premultiplied;
}

gsize
gsk_gradient_get_n_stops (const GskGradient *gradient)
{
  return gradient_stops_get_size (&gradient->stops);
}

const GskGradientStop *
gsk_gradient_get_stops (const GskGradient *gradient)
{
  return (const GskGradientStop *) gradient_stops_get_data (&gradient->stops);
}

const GdkColor *
gsk_gradient_get_stop_color (const GskGradient *gradient,
                             gsize              idx)
{
  return &gradient_stops_index (&gradient->stops, idx)->color;
}

float
gsk_gradient_get_stop_offset (const GskGradient *gradient,
                              gsize              idx)
{
  return gradient_stops_index (&gradient->stops, idx)->offset;
}

float
gsk_gradient_get_stop_transition_hint (const GskGradient *gradient,
                                       gsize              idx)
{
  return gradient_stops_index (&gradient->stops, idx)->transition_hint;
}

GdkColorState *
gsk_gradient_get_interpolation (const GskGradient *gradient)
{
  return gradient->interpolation;
}

GskHueInterpolation
gsk_gradient_get_hue_interpolation (const GskGradient *gradient)
{
  return gradient->hue_interpolation;
}

gboolean
gsk_gradient_get_premultiplied (const GskGradient *gradient)
{
  return gradient->premultiplied;
}

GskRepeat
gsk_gradient_get_repeat (const GskGradient *gradient)
{
  return gradient->repeat;
}

/*< private >
 * gsk_gradient_get_color_stops:
 * @gradient: a gradient
 * @n_stops: (out): Return location for the number of stops
 *
 * Returns an array of GskColorStop structs representing
 * the color stops of the gradient.
 *
 * This is used to implement the deprecated render node
 * APIs for color stops.
 */
const GskColorStop *
gsk_gradient_get_color_stops (GskGradient *gradient,
                              gsize       *n_stops)
{
  const GskColorStop *stops;

  if (n_stops != NULL)
    *n_stops = gradient_stops_get_size (&gradient->stops);

  if (gradient->rgba_stops == NULL)
    {
      gradient->rgba_stops = g_new (GskColorStop, gradient_stops_get_size (&gradient->stops));
      for (gsize i = 0; i < gradient_stops_get_size (&gradient->stops); i++)
        {
          const GskGradientStop *stop = gradient_stops_index (&gradient->stops, i);
          gradient->rgba_stops[i].offset = stop->offset;
          gdk_color_to_float (&stop->color, GDK_COLOR_STATE_SRGB, (float *) &gradient->rgba_stops[i].color);
        }
    }

  stops = gradient->rgba_stops;

  return stops;
}

/*< private >
 * gsk_gradient_is_opaque:
 * @gradient: a gradient
 *
 * Returns whether @gradient completely covers
 * the plane with non-translucent color.
 *
 * Returns: true if @gradient is opaque
 */
gboolean
gsk_gradient_is_opaque (const GskGradient *gradient)
{
  return gradient->opaque &&
         gradient_stops_get_size (&gradient->stops) > 0 &&
         gradient->repeat != GSK_REPEAT_NONE;
}

/*< private >
 * gsk_gradient_check_single_color:
 * @gradient: a gradient
 *
 * Checks whether the gradient fills the entire plane
 * with a single color.
 *
 * This API is used to optimize away gradient nodes
 * that can be replaced by color nodes.
 *
 * Returns: (transfer none) (nullable): the single color
 *   that the gradient will fill the plane with, or `NULL`
 *   if the gradient will not fill the plane with a single
 *   color
 */
const GdkColor *
gsk_gradient_check_single_color (const GskGradient *gradient)
{
  GskGradientStop *first = gradient_stops_index (&gradient->stops, 0);

  if (gradient->hue_interpolation == GSK_HUE_INTERPOLATION_LONGER)
    return NULL;

  if (gradient->repeat == GSK_REPEAT_NONE)
    return NULL;

  for (unsigned int i = 1; i < gradient_stops_get_size (&gradient->stops); i++)
    {
      GskGradientStop *next = gradient_stops_index (&gradient->stops, i);
      if (!gdk_color_equal (&first->color, &next->color))
        return NULL;
    }

  return &first->color;
}

static void
add_to_average (float values[4],
                float amount,
                float hint,
                float start[4],
                float end[4])
{
  gsize i;
  float lerp;

  if (amount <= 0.0)
    return;

  if (hint == 0.5)
    lerp = 0.5f;
  else if (hint <= 0.0)
    lerp = 1.0f;
  else if (hint >= 1.0)
    lerp = 0.0f;
  else
    lerp = logf (hint) / (logf (hint) - logf (2.0f));

  for (i = 0; i < 4; i++)
    {
      values[i] += amount * ((1.0f - lerp) * start[i] + lerp * end[i]); 
    }
}

static void
color_convert (const GskGradient *self,
               const GdkColor    *color,
               float              out_values[4])
{
  GdkColor tmp;
  gsize i;

  gdk_color_convert (&tmp, self->interpolation, color);

  if (self->premultiplied)
    {
      for (i = 0; i < 3; i++)
        {
          out_values[i] = tmp.values[i] * tmp.alpha;
        }
      out_values[GDK_COLOR_CHANNEL_ALPHA] = tmp.alpha;
    }
  else
    {
      for (i = 0; i < 4; i++)
        out_values[i] = tmp.values[i];
    }
}

/**
 * gsk_gradient_get_average_color:
 * @self: a gradient
 * @out_color: (out caller-allocates): Return location to place the color
 *
 * Computes the weighted average color of all color stops, respecting
 * transition hints and premultiplication. This can be imagined as the gradient
 * line being shrunk to a single pixel. Not relevant for this computation is
 * the repeat.
 *
 * This color is used in some cases for repeating zero-length gradients.
 *
 * Note that different gradient implementations have a different meaning for
 * degenerate gradient corner cases and not all of them may trivially map to
 * this function.
 **/
void
gsk_gradient_get_average_color (const GskGradient *self,
                                GdkColor          *out_color)
{
  float values[4] = { 0, 0, 0, 0 };
  GskGradientStop *cur, *next;
  float cur_offset;
  float cur_values[4], next_values[4];
  gsize i;

  cur = gradient_stops_get (&self->stops, 0);
  color_convert (self, &cur->color, cur_values);
  cur_offset = cur->offset;
  add_to_average (values, cur->offset, 0.5, cur_values, cur_values);

  for (i = 1; i < gradient_stops_get_size (&self->stops); i++)
    {
      next = gradient_stops_get (&self->stops, i);
      color_convert (self, &next->color, next_values);
      if (self->interpolation->hue_channel != GDK_COLOR_CHANNEL_ALPHA)
        {
          next_values[self->interpolation->hue_channel] =
              gsk_hue_interpolation_fixup (self->hue_interpolation,
                                           cur_values[self->interpolation->hue_channel],
                                           next_values[self->interpolation->hue_channel]);
        }

      add_to_average (values, next->offset - cur_offset, next->transition_hint, cur_values, next_values);
      cur_values[0] = next_values[0];
      cur_values[1] = next_values[1];
      cur_values[2] = next_values[2];
      cur_values[3] = next_values[3];
      cur_offset = next->offset;
    }

  add_to_average (values, 1.0f - cur_offset, 0.5, cur_values, cur_values);
  if (self->premultiplied && values[GDK_COLOR_CHANNEL_ALPHA] > 0.0f)
    {
      for (i = 0; i < 3; i++)
        {
          values[i] /= values[GDK_COLOR_CHANNEL_ALPHA];
        }
    }
  gdk_color_init (out_color, gdk_color_state_ref (self->interpolation), values);
}

float
gsk_hue_interpolation_fixup (GskHueInterpolation  interp,
                             float                h1,
                             float                h2)
{
  float d;

  d = h2 - h1;

  while (d > 360)
    {
      h2 -= 360;
      d = h2 - h1;
    }
  while (d < -360)
    {
      h2 += 360;
      d = h2 - h1;
    }

  g_assert (fabsf (d) <= 360);

  switch (interp)
    {
    case GSK_HUE_INTERPOLATION_SHORTER:
      {
        if (d > 180)
          h2 -= 360;
        else if (d < -180)
          h2 += 360;
      }
      g_assert (fabsf (h2 - h1) <= 180);
      break;

   case GSK_HUE_INTERPOLATION_LONGER:
      {
        if (0 < d && d < 180)
          h2 -= 360;
        else if (-180 < d && d <= 0)
          h2 += 360;
      g_assert (fabsf (h2 - h1) >= 180);
      }
      break;

    case GSK_HUE_INTERPOLATION_INCREASING:
      if (h2 < h1)
        h2 += 360;
      d = h2 - h1;
      g_assert (h1 <= h2);
      break;

    case GSK_HUE_INTERPOLATION_DECREASING:
      if (h1 < h2)
        h2 -= 360;
      d = h2 - h1;
      g_assert (h1 >= h2);
      break;

    default:
      g_assert_not_reached ();
    }

  return h2;
}

cairo_extend_t
gsk_repeat_to_cairo (GskRepeat repeat)
{
  switch (repeat)
    {
    case GSK_REPEAT_NONE: return CAIRO_EXTEND_NONE;
    case GSK_REPEAT_REPEAT: return CAIRO_EXTEND_REPEAT;
    case GSK_REPEAT_REFLECT: return CAIRO_EXTEND_REFLECT;
    case GSK_REPEAT_PAD: return CAIRO_EXTEND_PAD;
    default: g_assert_not_reached ();
    }
}
