/* 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
 * <https://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,
  N_PROPS
};


struct _GimpZoomModel
{
  GObject  parent_instance;

  gdouble  value;
  gdouble  minimum;
  gdouble  maximum;
};


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, };
static GParamSpec *object_props[N_PROPS] = { NULL, };

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,
                    0,
                    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;

  /**
   * GimpZoomModel:value:
   *
   * The zoom factor.
   */
  object_props[PROP_VALUE] = g_param_spec_double ("value",
                                                  "Value",
                                                  "Zoom factor",
                                                  ZOOM_MIN, ZOOM_MAX,
                                                  1.0,
                                                  GIMP_PARAM_READWRITE);
  /**
   * GimpZoomModel:minimum:
   *
   * The minimum zoom factor.
   */
  object_props[PROP_MINIMUM] = g_param_spec_double ("minimum",
                                                    "Minimum",
                                                    "Lower limit for the zoom factor",
                                                    ZOOM_MIN, ZOOM_MAX,
                                                    ZOOM_MIN,
                                                    GIMP_PARAM_READWRITE);
  /**
   * GimpZoomModel:maximum:
   *
   * The maximum zoom factor.
   */
  object_props[PROP_MAXIMUM] = g_param_spec_double ("maximum",
                                                    "Maximum",
                                                    "Upper limit for the zoom factor",
                                                    ZOOM_MIN, ZOOM_MAX,
                                                    ZOOM_MAX,
                                                    GIMP_PARAM_READWRITE);

  /**
   * GimpZoomModel:fraction:
   *
   * The zoom factor expressed as a fraction.
   */
  object_props[PROP_FRACTION] = g_param_spec_string ("fraction",
                                                     "Fraction",
                                                     "The zoom factor expressed as a fraction",
                                                     "1:1",
                                                     GIMP_PARAM_READABLE);
  /**
   * GimpZoomModel:percentage:
   *
   * The zoom factor expressed as percentage.
   */
  object_props[PROP_PERCENTAGE] = g_param_spec_string ("percentage",
                                                       "Percentage",
                                                       "The zoom factor expressed as a percentage",
                                                       "100%",
                                                       GIMP_PARAM_READABLE);

  g_object_class_install_properties (object_class, N_PROPS, object_props);
}

static void
gimp_zoom_model_init (GimpZoomModel *model)
{
  model->value   = 1.0;
  model->minimum = ZOOM_MIN;
  model->maximum = ZOOM_MAX;
}

static void
gimp_zoom_model_set_property (GObject      *object,
                              guint         property_id,
                              const GValue *value,
                              GParamSpec   *pspec)
{
  GimpZoomModel *model = GIMP_ZOOM_MODEL (object);
  gdouble        previous_value;

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

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

      g_object_notify_by_pspec (object, object_props[PROP_VALUE]);
      g_object_notify_by_pspec (object, object_props[PROP_FRACTION]);
      g_object_notify_by_pspec (object, object_props[PROP_PERCENTAGE]);
      break;

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

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

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

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

      g_object_notify_by_pspec (object, object_props[PROP_VALUE]);
      g_object_notify_by_pspec (object, object_props[PROP_FRACTION]);
      g_object_notify_by_pspec (object, object_props[PROP_PERCENTAGE]);
    }

  g_object_thaw_notify (object);

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

static void
gimp_zoom_model_get_property (GObject    *object,
                              guint       property_id,
                              GValue     *value,
                              GParamSpec *pspec)
{
  GimpZoomModel *model = GIMP_ZOOM_MODEL (object);
  gchar         *tmp;

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

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

    case PROP_MAXIMUM:
      g_value_set_double (value, model->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 (model->value >= 0.15 ? "%.0f%%" : "%.2f%%",
                             model->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)
{
  if (model->value < model->maximum)
    gimp_zoom_model_zoom (model, GIMP_ZOOM_IN, 0.0);
}

static void
gimp_zoom_model_zoom_out (GimpZoomModel *model)
{
  if (model->value > model->minimum)
    gimp_zoom_model_zoom (model, GIMP_ZOOM_OUT, 0.0);
}

/**
 * gimp_zoom_model_new:
 *
 * Creates a new #GimpZoomModel.
 *
 * Returns: 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)
{
  gdouble delta = 0.0;

  g_return_if_fail (GIMP_IS_ZOOM_MODEL (model));

  if (zoom_type == GIMP_ZOOM_SMOOTH)
    delta = scale;

  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, delta),
                NULL);
}

/**
 * gimp_zoom_model_get_factor:
 * @model: a #GimpZoomModel
 *
 * Retrieves the current zoom factor of @model.
 *
 * Returns: 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 model->value;
}


/**
 * gimp_zoom_model_get_fraction
 * @model:       a #GimpZoomModel
 * @numerator: (out): return location for numerator
 * @denominator: (out): 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 *icon_name,
                 GtkIconSize  icon_size)
{
  GtkWidget *button;
  GtkWidget *image;

  image = gtk_image_new_from_icon_name (icon_name,
                                        icon_size > 0 ?
                                        icon_size : GTK_ICON_SIZE_BUTTON);

  button = gtk_button_new ();
  gtk_container_add (GTK_CONTAINER (button), image);
  gtk_widget_show (image);

  return button;
}

static void
zoom_in_button_callback (GimpZoomModel *model,
                         gdouble        old,
                         gdouble        new,
                         GtkWidget     *button)
{
  gtk_widget_set_sensitive (button, model->value != model->maximum);
}

static void
zoom_out_button_callback (GimpZoomModel *model,
                          gdouble        old,
                          gdouble        new,
                          GtkWidget     *button)
{
  gtk_widget_set_sensitive (button, model->value != model->minimum);
}

/**
 * gimp_zoom_button_new:
 * @model:     a #GimpZoomModel
 * @zoom_type:
 * @icon_size: use 0 for a button with text labels
 *
 * Returns: (transfer full): 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 ("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 ("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
 * @delta:     the delta from a smooth zoom event
 *
 * Utility function to calculate a new scale factor.
 *
 * Returns: the new scale factor
 *
 * Since GIMP 2.4
 **/
gdouble
gimp_zoom_model_zoom_step (GimpZoomType zoom_type,
                           gdouble      scale,
                           gdouble      delta)
{
  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, 0.0);
      scale = gimp_zoom_model_zoom_step (GIMP_ZOOM_IN, scale, 0.0);
      scale = gimp_zoom_model_zoom_step (GIMP_ZOOM_IN, scale, 0.0);
      new_scale = scale;
      break;

    case GIMP_ZOOM_OUT_MORE:
      scale = gimp_zoom_model_zoom_step (GIMP_ZOOM_OUT, scale, 0.0);
      scale = gimp_zoom_model_zoom_step (GIMP_ZOOM_OUT, scale, 0.0);
      scale = gimp_zoom_model_zoom_step (GIMP_ZOOM_OUT, scale, 0.0);
      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;

    case GIMP_ZOOM_SMOOTH:
      if (delta > 0.0)
        new_scale = scale * (1.0 + 0.1 * delta);
      else if (delta < 0.0)
        new_scale = scale / (1.0 + 0.1 * -delta);
      else
        new_scale = scale;
      break;

    case GIMP_ZOOM_PINCH:
      if (delta > 0.0)
        new_scale = scale * (1.0 + delta);
      else if (delta < 0.0)
        new_scale = scale / (1.0 + -delta);
      else
        new_scale = scale;
      break;
    }

  return CLAMP (new_scale, ZOOM_MIN, ZOOM_MAX);

#undef ZOOM_MIN_STEP
}
