/* LIBPIKA - The PIKA Library
 * Copyright (C) 1995-1997 Peter Mattis and Spencer Kimball
 *
 * pikalabelspin.c
 * Copyright (C) 2020 Jehan
 *
 * 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
 * Library 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 <gegl.h>
#include <gtk/gtk.h>

#include "libpikacolor/pikacolor.h"
#include "libpikamath/pikamath.h"
#include "libpikabase/pikabase.h"

#include "pikawidgets.h"


/**
 * SECTION: pikalabelspin
 * @title: PikaLabelSpin
 * @short_description: Widget containing a spin button and a label.
 *
 * This widget is a subclass of #PikaLabeled with a #PikaSpinButton.
 **/

enum
{
  VALUE_CHANGED,
  LAST_SIGNAL
};

enum
{
  PROP_0,
  PROP_VALUE,
  PROP_LOWER,
  PROP_UPPER,
  PROP_DIGITS,
};

typedef struct _PikaLabelSpinPrivate
{
  PikaLabeled    parent_instance;

  GtkWidget     *spinbutton;
  GtkAdjustment *spin_adjustment;

  gint           digits;
} PikaLabelSpinPrivate;

static void        pika_label_spin_constructed       (GObject       *object);
static void        pika_label_spin_set_property      (GObject       *object,
                                                      guint          property_id,
                                                      const GValue  *value,
                                                      GParamSpec    *pspec);
static void        pika_label_spin_get_property      (GObject       *object,
                                                      guint          property_id,
                                                      GValue        *value,
                                                      GParamSpec    *pspec);

static GtkWidget * pika_label_spin_populate          (PikaLabeled   *spin,
                                                      gint          *x,
                                                      gint          *y,
                                                      gint          *width,
                                                      gint          *height);

static void        pika_label_spin_update_spin_width (PikaLabelSpin *spin,
                                                      gdouble        lower,
                                                      gdouble        upper,
                                                      guint          digits);
static void        pika_label_spin_update_settings   (PikaLabelSpin *spin);

G_DEFINE_TYPE_WITH_PRIVATE (PikaLabelSpin, pika_label_spin, PIKA_TYPE_LABELED)

#define parent_class pika_label_spin_parent_class

static guint pika_label_spin_signals[LAST_SIGNAL] = { 0 };

static void
pika_label_spin_class_init (PikaLabelSpinClass *klass)
{
  GObjectClass     *object_class  = G_OBJECT_CLASS (klass);
  PikaLabeledClass *labeled_class = PIKA_LABELED_CLASS (klass);

  pika_label_spin_signals[VALUE_CHANGED] =
    g_signal_new ("value-changed",
                  G_TYPE_FROM_CLASS (klass),
                  G_SIGNAL_RUN_FIRST,
                  G_STRUCT_OFFSET (PikaLabelSpinClass, value_changed),
                  NULL, NULL, NULL,
                  G_TYPE_NONE, 0);

  object_class->constructed  = pika_label_spin_constructed;
  object_class->set_property = pika_label_spin_set_property;
  object_class->get_property = pika_label_spin_get_property;

  labeled_class->populate    = pika_label_spin_populate;

  /**
   * PikaLabelSpin:value:
   *
   * The currently set value.
   *
   * Since: 3.0
   **/
  g_object_class_install_property (object_class, PROP_VALUE,
                                   g_param_spec_double ("value", NULL,
                                                        "Current value",
                                                        -G_MAXDOUBLE, G_MAXDOUBLE, 1.0,
                                                        PIKA_PARAM_READWRITE));

  /**
   * PikaLabelSpin:lower:
   *
   * The lower bound of the spin button.
   *
   * Since: 3.0
   **/
  g_object_class_install_property (object_class, PROP_LOWER,
                                   g_param_spec_double ("lower", NULL,
                                                        "Minimum value",
                                                        -G_MAXDOUBLE, G_MAXDOUBLE, 1.0,
                                                        PIKA_PARAM_READWRITE |
                                                        G_PARAM_CONSTRUCT));

  /**
   * PikaLabelSpin:upper:
   *
   * The upper bound of the spin button.
   *
   * Since: 3.0
   **/
  g_object_class_install_property (object_class, PROP_UPPER,
                                   g_param_spec_double ("upper", NULL,
                                                        "Max value",
                                                        -G_MAXDOUBLE, G_MAXDOUBLE, 0.0,
                                                        PIKA_PARAM_READWRITE |
                                                        G_PARAM_CONSTRUCT));

  /**
   * PikaLabelSpin:digits:
   *
   * The number of decimal places to display. If -1, then the number is
   * estimated.
   *
   * Since: 3.0
   **/
  g_object_class_install_property (object_class, PROP_DIGITS,
                                   g_param_spec_int ("digits", NULL,
                                                     "The number of decimal places to display",
                                                     -1, G_MAXINT, -1,
                                                     PIKA_PARAM_READWRITE));
}

static void
pika_label_spin_init (PikaLabelSpin *spin)
{
  PikaLabelSpinPrivate *priv  = pika_label_spin_get_instance_private (spin);

  /* We want the adjustment to exist at init so that construction
   * properties can apply (default values are bogus but should be
   * properly overridden with expected values if the object was created
   * with pika_label_spin_new().
   */
  priv->spin_adjustment = gtk_adjustment_new (0.0, 0.0, 100.0, 1.0, 10.0, 0.0);

  gtk_grid_set_row_spacing (GTK_GRID (spin), 6);
  gtk_grid_set_column_spacing (GTK_GRID (spin), 6);
}

static void
pika_label_spin_constructed (GObject *object)
{
  PikaLabelSpin        *spin = PIKA_LABEL_SPIN (object);
  PikaLabelSpinPrivate *priv  = pika_label_spin_get_instance_private (spin);

  G_OBJECT_CLASS (parent_class)->constructed (object);

  /* This is important to make this object into a property widget. It
   * will allow config object to bind the "value" property of this
   * widget, and therefore be updated automatically.
   */
  g_object_bind_property (G_OBJECT (priv->spin_adjustment), "value",
                          G_OBJECT (spin),                  "value",
                          G_BINDING_BIDIRECTIONAL |
                          G_BINDING_SYNC_CREATE);

  pika_label_spin_update_settings (spin);
}

static void
pika_label_spin_set_property (GObject      *object,
                              guint         property_id,
                              const GValue *value,
                              GParamSpec   *pspec)
{
  PikaLabelSpin        *spin = PIKA_LABEL_SPIN (object);
  PikaLabelSpinPrivate *priv  = pika_label_spin_get_instance_private (spin);

  switch (property_id)
    {
    case PROP_VALUE:
      /* Avoid looping forever since we have bound this widget's
       * "value" property with the spin button "value" property.
       */
      if (gtk_adjustment_get_value (priv->spin_adjustment) != g_value_get_double (value))
        gtk_adjustment_set_value (priv->spin_adjustment, g_value_get_double (value));

      g_signal_emit (object, pika_label_spin_signals[VALUE_CHANGED], 0);
      break;
    case PROP_LOWER:
      gtk_adjustment_set_lower (priv->spin_adjustment,
                                g_value_get_double (value));
      if (priv->spinbutton)
        {
          pika_label_spin_update_settings (spin);
        }
      break;
    case PROP_UPPER:
      gtk_adjustment_set_upper (priv->spin_adjustment,
                                g_value_get_double (value));
      if (priv->spinbutton)
        {
          pika_label_spin_update_settings (spin);
        }
      break;
    case PROP_DIGITS:
      if (priv->spinbutton)
        {
          priv->digits = g_value_get_int (value);
          pika_label_spin_update_settings (spin);
        }
      break;

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

static void
pika_label_spin_get_property (GObject    *object,
                              guint       property_id,
                              GValue     *value,
                              GParamSpec *pspec)
{
  PikaLabelSpin        *spin       = PIKA_LABEL_SPIN (object);
  PikaLabelSpinPrivate *priv       = pika_label_spin_get_instance_private (spin);
  GtkSpinButton        *spinbutton = GTK_SPIN_BUTTON (priv->spinbutton);

  switch (property_id)
    {
    case PROP_VALUE:
      g_value_set_double (value, gtk_adjustment_get_value (priv->spin_adjustment));
      break;
    case PROP_LOWER:
      g_value_set_double (value, gtk_adjustment_get_lower (priv->spin_adjustment));
      break;
    case PROP_UPPER:
      g_value_set_double (value, gtk_adjustment_get_upper (priv->spin_adjustment));
      break;
    case PROP_DIGITS:
      g_value_set_uint (value, gtk_spin_button_get_digits (spinbutton));
      break;

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

static GtkWidget *
pika_label_spin_populate (PikaLabeled *labeled,
                          gint        *x,
                          gint        *y,
                          gint        *width,
                          gint        *height)
{
  PikaLabelSpin        *spin = PIKA_LABEL_SPIN (labeled);
  PikaLabelSpinPrivate *priv = pika_label_spin_get_instance_private (spin);

  priv->spinbutton = pika_spin_button_new (priv->spin_adjustment, 2.0, 2.0);
  gtk_spin_button_set_numeric (GTK_SPIN_BUTTON (priv->spinbutton), TRUE);

  gtk_grid_attach (GTK_GRID (spin), priv->spinbutton, 1, 0, 1, 1);
  gtk_widget_show (priv->spinbutton);

  return priv->spinbutton;
}

static void
pika_label_spin_update_spin_width (PikaLabelSpin *spin,
                                   gdouble        lower,
                                   gdouble        upper,
                                   guint          digits)
{
  PikaLabelSpinPrivate *priv = pika_label_spin_get_instance_private (spin);
  gint                  width = 0;

  g_return_if_fail (PIKA_IS_LABEL_SPIN (spin));

  /* Necessary size to display the max/min integer values, with optional
   * negative sign.
   */
  width = (gint) floor (log10 (upper) + 1) + (upper < 0.0 ? 1 : 0);
  width = MAX (width, (gint) floor (log10 (lower) + 1) + (lower < 0.0 ? 1 : 0));

  /* Adding decimal digits and separator. */
  width += (digits > 0 ? 1 + digits : 0);

  /* Overlong spin button are useless. */
  width = MIN (10, width);

  gtk_entry_set_width_chars (GTK_ENTRY (priv->spinbutton), width);
}

static void
pika_label_spin_update_settings (PikaLabelSpin *spin)
{
  PikaLabelSpinPrivate *priv = pika_label_spin_get_instance_private (spin);
  gdouble               lower;
  gdouble               upper;
  gdouble               step;
  gdouble               page;
  gint                  digits = priv->digits;

  g_return_if_fail (PIKA_IS_LABEL_SPIN (spin));

  g_object_get (spin,
                "lower",  &lower,
                "upper",  &upper,
                NULL);

  g_return_if_fail (upper >= lower);

  pika_range_estimate_settings (lower, upper, &step, &page,
                                digits < 0 ? &digits: NULL);
  pika_label_spin_set_increments (spin, step, page);
  gtk_spin_button_set_digits (GTK_SPIN_BUTTON (priv->spinbutton), (guint) digits);
  pika_label_spin_update_spin_width (spin, lower, upper, (guint) digits);
}


/* Public Functions */


/**
 * pika_label_spin_new:
 * @text:   The text for the #GtkLabel.
 * @value:  The initial value.
 * @lower:  The lower boundary.
 * @upper:  The upper boundary.
 * @digits: The number of decimal digits.
 *
 * Suitable increment values are estimated based on the [@lower, @upper]
 * range.
 * If @digits is -1, then it will also be estimated based on the same
 * range. Digits estimation will always be at least 1, so if you want to
 * show integer values only, set 0 explicitly.
 *
 * Returns: (transfer full): The new #PikaLabelSpin widget.
 **/
GtkWidget *
pika_label_spin_new (const gchar *text,
                     gdouble      value,
                     gdouble      lower,
                     gdouble      upper,
                     gint         digits)
{
  GtkWidget *labeled;

  g_return_val_if_fail (upper >= lower, NULL);
  g_return_val_if_fail (digits >= -1, NULL);

  labeled = g_object_new (PIKA_TYPE_LABEL_SPIN,
                          "label",  text,
                          "value",  value,
                          "lower",  lower,
                          "upper",  upper,
                          "digits", digits,
                          NULL);

  return labeled;
}

/**
 * pika_label_spin_set_value:
 * @spin: The #GtkLabelSpin.
 * @value: A new value.
 *
 * This function sets the value shown by @spin.
 **/
void
pika_label_spin_set_value (PikaLabelSpin *spin,
                           gdouble         value)
{
  g_return_if_fail (PIKA_IS_LABEL_SPIN (spin));

  g_object_set (spin,
                "value", value,
                NULL);
}

/**
 * pika_label_spin_get_value:
 * @spin: The #GtkLabelSpin.
 *
 * This function returns the value shown by @spin.
 *
 * Returns: The value currently set.
 **/
gdouble
pika_label_spin_get_value (PikaLabelSpin *spin)
{
  gdouble value;

  g_return_val_if_fail (PIKA_IS_LABEL_SPIN (spin), 0.0);

  g_object_get (spin,
                "value", &value,
                NULL);
  return value;
}

/**
 * pika_label_spin_set_increments:
 * @spin: the #PikaLabelSpin.
 * @step: the step increment.
 * @page: the page increment.
 *
 * Set the step and page increments of the spin button.
 * By default, these increment values are automatically computed
 * depending on the range based on common usage. So you will likely not
 * need to run this for most case. Yet if you want specific increments
 * (which the widget cannot guess), you can call this function.
 */
void
pika_label_spin_set_increments (PikaLabelSpin *spin,
                                gdouble        step,
                                gdouble        page)
{
  PikaLabelSpinPrivate *priv = pika_label_spin_get_instance_private (spin);
  GtkSpinButton        *spinbutton;
  gdouble               lower;
  gdouble               upper;

  g_return_if_fail (PIKA_IS_LABEL_SPIN (spin));
  g_return_if_fail (step <= page);

  spinbutton = GTK_SPIN_BUTTON (priv->spinbutton);

  gtk_spin_button_get_range (spinbutton, &lower, &upper);
  g_return_if_fail (upper >= lower);
  g_return_if_fail ((upper == lower || step <= upper - lower) &&
                    (upper == lower || page <= upper - lower));

  g_object_freeze_notify (G_OBJECT (spinbutton));
  gtk_adjustment_set_step_increment (gtk_spin_button_get_adjustment (spinbutton), step);
  gtk_adjustment_set_page_increment (gtk_spin_button_get_adjustment (spinbutton), page);
  g_object_thaw_notify (G_OBJECT (spinbutton));

  g_object_set (priv->spinbutton,
                "climb-rate", step,
                NULL);
}

/**
 * pika_label_spin_set_digits:
 * @spin: the #PikaLabelSpin.
 * @digits: the number of decimal places to display.
 *
 * Set the number of decimal place to display in the @spin's entry.
 * If @digits is -1, then it will also be estimated based on @spin's
 * range. Digits estimation will always be at least 1, so if you want to
 * show integer values only, set 0 explicitly.
 */
void
pika_label_spin_set_digits (PikaLabelSpin *spin,
                            gint           digits)
{
  g_return_if_fail (PIKA_IS_LABEL_SPIN (spin));

  g_object_set (spin,
                "digits", digits,
                NULL);
}

/**
 * pika_label_spin_get_spin_button:
 * @spin: The #PikaLabelSpin
 *
 * This function returns the #PikaSpinButton packed in @spin.
 *
 * Returns: (transfer none): The #PikaSpinButton contained in @spin.
 **/
GtkWidget *
pika_label_spin_get_spin_button (PikaLabelSpin *spin)
{
  PikaLabelSpinPrivate *priv = pika_label_spin_get_instance_private (spin);

  g_return_val_if_fail (PIKA_IS_LABEL_SPIN (spin), NULL);

  return priv->spinbutton;
}
