/* LIBGIMP - The GIMP Library
 * Copyright (C) 1995-1997 Peter Mattis and Spencer Kimball
 *
 * gimpzoommodel.c
 * Copyright (C) 2005  David Odin <dindinx@gimp.org>
 *
 * This library is free software: you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library.  If not, see
 * <http://www.gnu.org/licenses/>.
 */

#include "config.h"

#include <gtk/gtk.h>

#include "gimpwidgetstypes.h"

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

#include "gimphelpui.h"
#include "gimpwidgetsmarshal.h"
#include "gimpzoommodel.h"


/**
 * SECTION: gimpzoommodel
 * @title: GimpZoomModel
 * @short_description: A model for zoom values.
 *
 * A model for zoom values.
 **/


#define ZOOM_MIN  (1.0 / 256.0)
#define ZOOM_MAX  (256.0)

enum
{
  ZOOMED,
  LAST_SIGNAL
};

enum
{
  PROP_0,
  PROP_VALUE,
  PROP_MINIMUM,
  PROP_MAXIMUM,
  PROP_FRACTION,
  PROP_PERCENTAGE
};


typedef struct
{
  gdouble  value;
  gdouble  minimum;
  gdouble  maximum;
} GimpZoomModelPrivate;

#define GIMP_ZOOM_MODEL_GET_PRIVATE(obj) \
  ((GimpZoomModelPrivate *) ((GimpZoomModel *) (obj))->priv)


static void  gimp_zoom_model_set_property (GObject      *object,
                                           guint         property_id,
                                           const GValue *value,
                                           GParamSpec   *pspec);
static void  gimp_zoom_model_get_property (GObject      *object,
                                           guint         property_id,
                                           GValue       *value,
                                           GParamSpec   *pspec);


static guint zoom_model_signals[LAST_SIGNAL] = { 0, };

G_DEFINE_TYPE (GimpZoomModel, gimp_zoom_model, G_TYPE_OBJECT)

#define parent_class gimp_zoom_model_parent_class


static void
gimp_zoom_model_class_init (GimpZoomModelClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  /**
   * GimpZoomModel::zoomed:
   * @model: the object that received the signal
   * @old_factor: the zoom factor before it changes
   * @new_factor: the zoom factor after it has changed.
   *
   * Emitted when the zoom factor of the zoom model changes.
   */
  zoom_model_signals[ZOOMED] =
      g_signal_new ("zoomed",
                    G_TYPE_FROM_CLASS (klass),
                    G_SIGNAL_RUN_LAST,
                    G_STRUCT_OFFSET (GimpZoomModelClass,
                                     zoomed),
                    NULL, NULL,
                    _gimp_widgets_marshal_VOID__DOUBLE_DOUBLE,
                    G_TYPE_NONE, 2,
                    G_TYPE_DOUBLE, G_TYPE_DOUBLE);

  object_class->set_property = gimp_zoom_model_set_property;
  object_class->get_property = gimp_zoom_model_get_property;

  g_object_class_install_property (object_class, PROP_VALUE,
                                   g_param_spec_double ("value",
                                                        "Zoom factor", NULL,
                                                        ZOOM_MIN, ZOOM_MAX,
                                                        1.0,
                                                        GIMP_PARAM_READWRITE));
  g_object_class_install_property (object_class, PROP_MINIMUM,
                                   g_param_spec_double ("minimum",
                                                        "Lower limit for the zoom factor", NULL,
                                                        ZOOM_MIN, ZOOM_MAX,
                                                        ZOOM_MIN,
                                                        GIMP_PARAM_READWRITE));
  g_object_class_install_property (object_class, PROP_MAXIMUM,
                                   g_param_spec_double ("maximum",
                                                        "Upper limit for the zoom factor", NULL,
                                                        ZOOM_MIN, ZOOM_MAX,
                                                        ZOOM_MAX,
                                                        GIMP_PARAM_READWRITE));

  g_object_class_install_property (object_class, PROP_FRACTION,
                                   g_param_spec_string ("fraction",
                                                        "The zoom factor expressed as a fraction", NULL,
                                                        "1:1",
                                                        GIMP_PARAM_READABLE));
  g_object_class_install_property (object_class, PROP_PERCENTAGE,
                                   g_param_spec_string ("percentage",
                                                        "The zoom factor expressed as a percentage", NULL,
                                                        "100%",
                                                        GIMP_PARAM_READABLE));

  g_type_class_add_private (object_class, sizeof (GimpZoomModelPrivate));
}

static void
gimp_zoom_model_init (GimpZoomModel *model)
{
  GimpZoomModelPrivate *priv;

  model->priv = G_TYPE_INSTANCE_GET_PRIVATE (model,
                                             GIMP_TYPE_ZOOM_MODEL,
                                             GimpZoomModelPrivate);

  priv = GIMP_ZOOM_MODEL_GET_PRIVATE (model);

  priv->value   = 1.0;
  priv->minimum = ZOOM_MIN;
  priv->maximum = ZOOM_MAX;
}

static void
gimp_zoom_model_set_property (GObject      *object,
                              guint         property_id,
                              const GValue *value,
                              GParamSpec   *pspec)
{
  GimpZoomModelPrivate *priv  = GIMP_ZOOM_MODEL_GET_PRIVATE (object);
  gdouble               previous_value;

  previous_value = priv->value;
  g_object_freeze_notify (object);

  switch (property_id)
    {
    case PROP_VALUE:
      priv->value = g_value_get_double (value);

      g_object_notify (object, "value");
      g_object_notify (object, "fraction");
      g_object_notify (object, "percentage");
      break;

    case PROP_MINIMUM:
      priv->minimum = MIN (g_value_get_double (value), priv->maximum);
      break;

    case PROP_MAXIMUM:
      priv->maximum = MAX (g_value_get_double (value), priv->minimum);
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
      break;
    }

  if (priv->value > priv->maximum || priv->value < priv->minimum)
    {
      priv->value = CLAMP (priv->value, priv->minimum, priv->maximum);

      g_object_notify (object, "value");
      g_object_notify (object, "fraction");
      g_object_notify (object, "percentage");
    }

  g_object_thaw_notify (object);

  if (priv->value != previous_value)
    {
      g_signal_emit (object, zoom_model_signals[ZOOMED],
                     0, previous_value, priv->value);
    }
}

static void
gimp_zoom_model_get_property (GObject    *object,
                              guint       property_id,
                              GValue     *value,
                              GParamSpec *pspec)
{
  GimpZoomModelPrivate *priv  = GIMP_ZOOM_MODEL_GET_PRIVATE (object);
  gchar                *tmp;

  switch (property_id)
    {
    case PROP_VALUE:
      g_value_set_double (value, priv->value);
      break;

    case PROP_MINIMUM:
      g_value_set_double (value, priv->minimum);
      break;

    case PROP_MAXIMUM:
      g_value_set_double (value, priv->maximum);
      break;

    case PROP_FRACTION:
      {
        gint  numerator;
        gint  denominator;

        gimp_zoom_model_get_fraction (GIMP_ZOOM_MODEL (object),
                                      &numerator, &denominator);

        tmp = g_strdup_printf ("%d:%d", numerator, denominator);
        g_value_set_string (value, tmp);
        g_free (tmp);
      }
      break;

    case PROP_PERCENTAGE:
      tmp = g_strdup_printf (priv->value >= 0.15 ? "%.0f%%" : "%.2f%%",
                             priv->value * 100.0);
      g_value_set_string (value, tmp);
      g_free (tmp);
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
      break;
    }
}

static void
gimp_zoom_model_zoom_in (GimpZoomModel *model)
{
  GimpZoomModelPrivate *priv = GIMP_ZOOM_MODEL_GET_PRIVATE (model);

  if (priv->value < priv->maximum)
    gimp_zoom_model_zoom (model, GIMP_ZOOM_IN, 0.0);
}

static void
gimp_zoom_model_zoom_out (GimpZoomModel *model)
{
  GimpZoomModelPrivate *priv = GIMP_ZOOM_MODEL_GET_PRIVATE (model);

  if (priv->value > priv->minimum)
    gimp_zoom_model_zoom (model, GIMP_ZOOM_OUT, 0.0);
}

/**
 * gimp_zoom_model_new:
 *
 * Creates a new #GimpZoomModel.
 *
 * Return value: a new #GimpZoomModel.
 *
 * Since GIMP 2.4
 **/
GimpZoomModel *
gimp_zoom_model_new (void)
{
  return g_object_new (GIMP_TYPE_ZOOM_MODEL, NULL);
}


/**
 * gimp_zoom_model_set_range:
 * @model: a #GimpZoomModel
 * @min: new lower limit for zoom factor
 * @max: new upper limit for zoom factor
 *
 * Sets the allowed range of the @model.
 *
 * Since GIMP 2.4
 **/
void
gimp_zoom_model_set_range (GimpZoomModel *model,
                           gdouble        min,
                           gdouble        max)
{
  g_return_if_fail (GIMP_IS_ZOOM_MODEL (model));
  g_return_if_fail (min < max);
  g_return_if_fail (min >= ZOOM_MIN);
  g_return_if_fail (max <= ZOOM_MAX);

  g_object_set (model,
                "minimum", min,
                "maximum", max,
                NULL);
}

/**
 * gimp_zoom_model_zoom:
 * @model:     a #GimpZoomModel
 * @zoom_type: the #GimpZoomType
 * @scale:     ignored unless @zoom_type == %GIMP_ZOOM_TO
 *
 * Since GIMP 2.4
 **/
void
gimp_zoom_model_zoom (GimpZoomModel *model,
                      GimpZoomType   zoom_type,
                      gdouble        scale)
{
  g_return_if_fail (GIMP_IS_ZOOM_MODEL (model));

  if (zoom_type != GIMP_ZOOM_TO)
    scale = gimp_zoom_model_get_factor (model);

  g_object_set (model,
                "value", gimp_zoom_model_zoom_step (zoom_type, scale),
                NULL);
}

/**
 * gimp_zoom_model_get_factor:
 * @model: a #GimpZoomModel
 *
 * Retrieves the current zoom factor of @model.
 *
 * Return value: the current scale factor
 *
 * Since GIMP 2.4
 **/
gdouble
gimp_zoom_model_get_factor (GimpZoomModel *model)
{
  g_return_val_if_fail (GIMP_IS_ZOOM_MODEL (model), 1.0);

  return GIMP_ZOOM_MODEL_GET_PRIVATE (model)->value;
}


/**
 * gimp_zoom_model_get_fraction
 * @model:       a #GimpZoomModel
 * @numerator:   return location for numerator
 * @denominator: return location for denominator
 *
 * Retrieves the current zoom factor of @model as a fraction.
 *
 * Since GIMP 2.4
 **/
void
gimp_zoom_model_get_fraction (GimpZoomModel *model,
                              gint          *numerator,
                              gint          *denominator)
{
  gint     p0, p1, p2;
  gint     q0, q1, q2;
  gdouble  zoom_factor;
  gdouble  remainder, next_cf;
  gboolean swapped = FALSE;

  g_return_if_fail (GIMP_IS_ZOOM_MODEL (model));
  g_return_if_fail (numerator != NULL && denominator != NULL);

  zoom_factor = gimp_zoom_model_get_factor (model);

  /* make sure that zooming behaves symmetrically */
  if (zoom_factor < 1.0)
    {
      zoom_factor = 1.0 / zoom_factor;
      swapped = TRUE;
    }

  /* calculate the continued fraction for the desired zoom factor */

  p0 = 1;
  q0 = 0;
  p1 = floor (zoom_factor);
  q1 = 1;

  remainder = zoom_factor - p1;

  while (fabs (remainder) >= 0.0001 &&
         fabs (((gdouble) p1 / q1) - zoom_factor) > 0.0001)
    {
      remainder = 1.0 / remainder;

      next_cf = floor (remainder);

      p2 = next_cf * p1 + p0;
      q2 = next_cf * q1 + q0;

      /* Numerator and Denominator are limited by 256 */
      /* also absurd ratios like 170:171 are excluded */
      if (p2 > 256 || q2 > 256 || (p2 > 1 && q2 > 1 && p2 * q2 > 200))
        break;

      /* remember the last two fractions */
      p0 = p1;
      p1 = p2;
      q0 = q1;
      q1 = q2;

      remainder = remainder - next_cf;
    }

  zoom_factor = (gdouble) p1 / q1;

  /* hard upper and lower bounds for zoom ratio */

  if (zoom_factor > 256.0)
    {
      p1 = 256;
      q1 = 1;
    }
  else if (zoom_factor < 1.0 / 256.0)
    {
      p1 = 1;
      q1 = 256;
    }

  if (swapped)
    {
      *numerator = q1;
      *denominator = p1;
    }
  else
    {
      *numerator = p1;
      *denominator = q1;
    }
}

static GtkWidget *
zoom_button_new (const gchar *stock_id,
                 GtkIconSize  icon_size)
{
  GtkWidget *button;

  if (icon_size > 0)
    {
      GtkWidget *image = gtk_image_new_from_stock (stock_id, icon_size);

      button = gtk_button_new ();
      gtk_container_add (GTK_CONTAINER (button), image);
      gtk_widget_show (image);
    }
  else
    {
      button = gtk_button_new_from_stock (stock_id);
    }

  return button;
}

static void
zoom_in_button_callback (GimpZoomModel *model,
                         gdouble        old,
                         gdouble        new,
                         GtkWidget     *button)
{
  GimpZoomModelPrivate *priv = GIMP_ZOOM_MODEL_GET_PRIVATE (model);

  gtk_widget_set_sensitive (button, priv->value != priv->maximum);
}

static void
zoom_out_button_callback (GimpZoomModel *model,
                          gdouble        old,
                          gdouble        new,
                          GtkWidget     *button)
{
  GimpZoomModelPrivate *priv = GIMP_ZOOM_MODEL_GET_PRIVATE (model);

  gtk_widget_set_sensitive (button, priv->value != priv->minimum);
}

/**
 * gimp_zoom_button_new:
 * @model:     a #GimpZoomModel
 * @zoom_type:
 * @icon_size: use 0 for a button with text labels
 *
 * Return value: a newly created GtkButton
 *
 * Since GIMP 2.4
 **/
GtkWidget *
gimp_zoom_button_new (GimpZoomModel *model,
                      GimpZoomType   zoom_type,
                      GtkIconSize    icon_size)
{
  GtkWidget *button = NULL;

  g_return_val_if_fail (GIMP_IS_ZOOM_MODEL (model), NULL);

  switch (zoom_type)
    {
    case GIMP_ZOOM_IN:
      button = zoom_button_new (GTK_STOCK_ZOOM_IN, icon_size);
      g_signal_connect_swapped (button, "clicked",
                                G_CALLBACK (gimp_zoom_model_zoom_in),
                                model);
      g_signal_connect_object (model, "zoomed",
                               G_CALLBACK (zoom_in_button_callback),
                               button, 0);
      break;

    case GIMP_ZOOM_OUT:
      button = zoom_button_new (GTK_STOCK_ZOOM_OUT, icon_size);
      g_signal_connect_swapped (button, "clicked",
                                G_CALLBACK (gimp_zoom_model_zoom_out),
                                model);
      g_signal_connect_object (model, "zoomed",
                               G_CALLBACK (zoom_out_button_callback),
                               button, 0);
      break;

    default:
      g_warning ("sorry, no button for this zoom type (%d)", zoom_type);
      break;
    }

  if (button)
    {
      gdouble zoom = gimp_zoom_model_get_factor (model);

      /*  set initial button sensitivity  */
      g_signal_emit (model, zoom_model_signals[ZOOMED], 0, zoom, zoom);

      if (icon_size > 0)
        {
          const gchar *desc;

          if (gimp_enum_get_value (GIMP_TYPE_ZOOM_TYPE, zoom_type,
                                   NULL, NULL, &desc, NULL))
            {
              gimp_help_set_help_data (button, desc, NULL);
            }
        }
    }

  return button;
}

/**
 * gimp_zoom_model_zoom_step:
 * @zoom_type: the zoom type
 * @scale:     ignored unless @zoom_type == %GIMP_ZOOM_TO
 *
 * Utility function to calculate a new scale factor.
 *
 * Return value: the new scale factor
 *
 * Since GIMP 2.4
 **/
gdouble
gimp_zoom_model_zoom_step (GimpZoomType zoom_type,
                           gdouble      scale)
{
  gint    i, n_presets;
  gdouble new_scale = 1.0;

  /* This table is constructed to have fractions, that approximate
   * sqrt(2)^k. This gives a smooth feeling regardless of the starting
   * zoom level.
   *
   * Zooming in/out always jumps to a zoom step from the list below.
   * However, we try to guarantee a certain size of the step, to
   * avoid silly jumps from 101% to 100%.
   *
   * The factor 1.1 is chosen a bit arbitrary, but feels better
   * than the geometric median of the zoom steps (2^(1/4)).
   */

#define ZOOM_MIN_STEP 1.1

  const gdouble presets[] = {
    1.0 / 256, 1.0 / 180, 1.0 / 128, 1.0 / 90,
    1.0 / 64,  1.0 / 45,  1.0 / 32,  1.0 / 23,
    1.0 / 16,  1.0 / 11,  1.0 / 8,   2.0 / 11,
    1.0 / 4,   1.0 / 3,   1.0 / 2,   2.0 / 3,
      1.0,
               3.0 / 2,      2.0,      3.0,
      4.0,    11.0 / 2,      8.0,     11.0,
      16.0,     23.0,       32.0,     45.0,
      64.0,     90.0,      128.0,    180.0,
      256.0,
  };

  n_presets = G_N_ELEMENTS (presets);

  switch (zoom_type)
    {
    case GIMP_ZOOM_IN:
      scale *= ZOOM_MIN_STEP;

      new_scale = presets[n_presets - 1];
      for (i = n_presets - 1; i >= 0 && presets[i] > scale; i--)
        new_scale = presets[i];

      break;

    case GIMP_ZOOM_OUT:
      scale /= ZOOM_MIN_STEP;

      new_scale = presets[0];
      for (i = 0; i < n_presets && presets[i] < scale; i++)
        new_scale = presets[i];

      break;

    case GIMP_ZOOM_IN_MORE:
      scale = gimp_zoom_model_zoom_step (GIMP_ZOOM_IN, scale);
      scale = gimp_zoom_model_zoom_step (GIMP_ZOOM_IN, scale);
      scale = gimp_zoom_model_zoom_step (GIMP_ZOOM_IN, scale);
      new_scale = scale;
      break;

    case GIMP_ZOOM_OUT_MORE:
      scale = gimp_zoom_model_zoom_step (GIMP_ZOOM_OUT, scale);
      scale = gimp_zoom_model_zoom_step (GIMP_ZOOM_OUT, scale);
      scale = gimp_zoom_model_zoom_step (GIMP_ZOOM_OUT, scale);
      new_scale = scale;
      break;

    case GIMP_ZOOM_IN_MAX:
      new_scale = ZOOM_MAX;
      break;

    case GIMP_ZOOM_OUT_MAX:
      new_scale = ZOOM_MIN;
      break;

    case GIMP_ZOOM_TO:
      new_scale = scale;
      break;
    }

  return CLAMP (new_scale, ZOOM_MIN, ZOOM_MAX);

#undef ZOOM_MIN_STEP
}
