/* GTK - The GIMP Toolkit
 * Copyright © 2012 Red Hat, Inc.
 *
 * 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 2 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/>.
 *
 * Author: Cosimo Cecchi <cosimoc@gnome.org>
 *
 */

/**
 * GtkLevelBar:
 *
 * Shows a level indicator.
 *
 * Typical use cases are displaying the strength of a password, or
 * showing the charge level of a battery.
 *
 * <picture>
 *   <source srcset="levelbar-dark.png" media="(prefers-color-scheme: dark)">
 *   <img alt="An example GtkLevelBar" src="levelbar.png">
 * </picture>
 *
 * Use [method@Gtk.LevelBar.set_value] to set the current value, and
 * [method@Gtk.LevelBar.add_offset_value] to set the value offsets at which
 * the bar will be considered in a different state. GTK will add a few
 * offsets by default on the level bar: %GTK_LEVEL_BAR_OFFSET_LOW,
 * %GTK_LEVEL_BAR_OFFSET_HIGH and %GTK_LEVEL_BAR_OFFSET_FULL, with
 * values 0.25, 0.75 and 1.0 respectively.
 *
 * Note that it is your responsibility to update preexisting offsets
 * when changing the minimum or maximum value. GTK will simply clamp
 * them to the new range.
 *
 * ## Adding a custom offset on the bar
 *
 * ```c
 * static GtkWidget *
 * create_level_bar (void)
 * {
 *   GtkWidget *widget;
 *   GtkLevelBar *bar;
 *
 *   widget = gtk_level_bar_new ();
 *   bar = GTK_LEVEL_BAR (widget);
 *
 *   // This changes the value of the default low offset
 *
 *   gtk_level_bar_add_offset_value (bar,
 *                                   GTK_LEVEL_BAR_OFFSET_LOW,
 *                                   0.10);
 *
 *   // This adds a new offset to the bar; the application will
 *   // be able to change its color CSS like this:
 *   //
 *   // levelbar block.my-offset {
 *   //   background-color: magenta;
 *   //   border-style: solid;
 *   //   border-color: black;
 *   //   border-width: 1px;
 *   // }
 *
 *   gtk_level_bar_add_offset_value (bar, "my-offset", 0.60);
 *
 *   return widget;
 * }
 * ```
 *
 * The default interval of values is between zero and one, but it’s possible
 * to modify the interval using [method@Gtk.LevelBar.set_min_value] and
 * [method@Gtk.LevelBar.set_max_value]. The value will be always drawn in
 * proportion to the admissible interval, i.e. a value of 15 with a specified
 * interval between 10 and 20 is equivalent to a value of 0.5 with an interval
 * between 0 and 1. When %GTK_LEVEL_BAR_MODE_DISCRETE is used, the bar level
 * is rendered as a finite number of separated blocks instead of a single one.
 * The number of blocks that will be rendered is equal to the number of units
 * specified by the admissible interval.
 *
 * For instance, to build a bar rendered with five blocks, it’s sufficient to
 * set the minimum value to 0 and the maximum value to 5 after changing the
 * indicator mode to discrete.
 *
 * # GtkLevelBar as GtkBuildable
 *
 * The `GtkLevelBar` implementation of the `GtkBuildable` interface supports a
 * custom `<offsets>` element, which can contain any number of `<offset>` elements,
 * each of which must have "name" and "value" attributes.
 *
 * # CSS nodes
 *
 * ```
 * levelbar[.discrete]
 * ╰── trough
 *     ├── block.filled.level-name
 *     ┊
 *     ├── block.empty
 *     ┊
 * ```
 *
 * `GtkLevelBar` has a main CSS node with name levelbar and one of the style
 * classes .discrete or .continuous and a subnode with name trough. Below the
 * trough node are a number of nodes with name block and style class .filled
 * or .empty. In continuous mode, there is exactly one node of each, in discrete
 * mode, the number of filled and unfilled nodes corresponds to blocks that are
 * drawn. The block.filled nodes also get a style class .level-name corresponding
 * to the level for the current value.
 *
 * In horizontal orientation, the nodes are always arranged from left to right,
 * regardless of text direction.
 *
 * # Accessibility
 *
 * `GtkLevelBar` uses the [enum@Gtk.AccessibleRole.meter] role.
 */
#include "config.h"

#include "gtkaccessiblerange.h"
#include "gtkbinlayout.h"
#include "gtkbuildable.h"
#include "gtkbuilderprivate.h"
#include "gtkgizmoprivate.h"
#include "gtklevelbar.h"
#include "gtkmarshalers.h"
#include "gtkorientable.h"
#include "gtkcssnodeprivate.h"
#include "gtktypebuiltins.h"
#include "gtkwidgetprivate.h"
#include "gtkprivate.h"

#include <math.h>
#include <stdlib.h>

enum {
  PROP_VALUE = 1,
  PROP_MIN_VALUE,
  PROP_MAX_VALUE,
  PROP_MODE,
  PROP_INVERTED,
  LAST_PROPERTY,
  PROP_ORIENTATION /* overridden */
};

enum {
  SIGNAL_OFFSET_CHANGED,
  NUM_SIGNALS
};

static GParamSpec *properties[LAST_PROPERTY] = { NULL, };
static guint signals[NUM_SIGNALS] = { 0, };

typedef struct _GtkLevelBarClass   GtkLevelBarClass;

typedef struct {
  char *name;
  double value;
} GtkLevelBarOffset;

struct _GtkLevelBar {
  GtkWidget parent_instance;

  GtkOrientation orientation;

  GtkLevelBarMode bar_mode;

  double min_value;
  double max_value;
  double cur_value;

  GList *offsets;

  GtkWidget *trough_widget;
  GtkWidget **block_widget;
  guint n_blocks;

  guint inverted : 1;
};

struct _GtkLevelBarClass {
  GtkWidgetClass parent_class;

  void (* offset_changed) (GtkLevelBar *self,
                           const char *name);
};

static void gtk_level_bar_set_value_internal (GtkLevelBar *self,
                                              double       value);

static void gtk_level_bar_buildable_init (GtkBuildableIface *iface);

G_DEFINE_TYPE_WITH_CODE (GtkLevelBar, gtk_level_bar, GTK_TYPE_WIDGET,
                         G_IMPLEMENT_INTERFACE (GTK_TYPE_ORIENTABLE, NULL)
                         G_IMPLEMENT_INTERFACE (GTK_TYPE_ACCESSIBLE_RANGE, NULL)
                         G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE,
                                                gtk_level_bar_buildable_init))

static GtkLevelBarOffset *
gtk_level_bar_offset_new (const char *name,
                          double       value)
{
  GtkLevelBarOffset *offset = g_new0 (GtkLevelBarOffset, 1);

  offset->name = g_strdup (name);
  offset->value = value;

  return offset;
}

static void
gtk_level_bar_offset_free (GtkLevelBarOffset *offset)
{
  g_free (offset->name);
  g_free (offset);
}

static int
offset_find_func (gconstpointer data,
                  gconstpointer user_data)
{
  const GtkLevelBarOffset *offset = data;
  const char *name = user_data;

  return g_strcmp0 (name, offset->name);
}

static int
offset_sort_func (gconstpointer a,
                  gconstpointer b)
{
  const GtkLevelBarOffset *offset_a = a;
  const GtkLevelBarOffset *offset_b = b;

  return (offset_a->value > offset_b->value);
}

static gboolean
gtk_level_bar_ensure_offset (GtkLevelBar *self,
                             const char *name,
                             double       value)
{
  GList *existing;
  GtkLevelBarOffset *offset = NULL;
  GtkLevelBarOffset *new_offset;

  existing = g_list_find_custom (self->offsets, name, offset_find_func);
  if (existing)
    offset = existing->data;

  if (offset && (offset->value == value))
    return FALSE;

  new_offset = gtk_level_bar_offset_new (name, value);

  if (offset)
    {
      gtk_level_bar_offset_free (offset);
      self->offsets = g_list_delete_link (self->offsets, existing);
    }

  self->offsets = g_list_insert_sorted (self->offsets, new_offset, offset_sort_func);

  return TRUE;
}

#ifndef G_DISABLE_CHECKS
static gboolean
gtk_level_bar_value_in_interval (GtkLevelBar *self,
                                 double       value)
{
  return ((value >= self->min_value) &&
          (value <= self->max_value));
}
#endif

static int
gtk_level_bar_get_num_blocks (GtkLevelBar *self)
{
  if (self->bar_mode == GTK_LEVEL_BAR_MODE_CONTINUOUS)
    return 1;
  else if (self->bar_mode == GTK_LEVEL_BAR_MODE_DISCRETE)
    return MAX (1, (int) (round (self->max_value) - round (self->min_value)));

  return 0;
}

static int
gtk_level_bar_get_num_block_nodes (GtkLevelBar *self)
{
  if (self->bar_mode == GTK_LEVEL_BAR_MODE_CONTINUOUS)
    return 2;
  else
    return gtk_level_bar_get_num_blocks (self);
}

static void
gtk_level_bar_get_min_block_size (GtkLevelBar *self,
                                  int         *block_width,
                                  int         *block_height)
{
  guint i, n_blocks;
  int width, height;

  *block_width = *block_height = 0;
  n_blocks = gtk_level_bar_get_num_block_nodes (self);

  for (i = 0; i < n_blocks; i++)
    {
      gtk_widget_measure (self->block_widget[i],
                          GTK_ORIENTATION_HORIZONTAL,
                          -1,
                          &width, NULL,
                          NULL, NULL);
      gtk_widget_measure (self->block_widget[i],
                          GTK_ORIENTATION_VERTICAL,
                          -1,
                          &height, NULL,
                          NULL, NULL);

      *block_width = MAX (width, *block_width);
      *block_height = MAX (height, *block_height);
    }
}

static gboolean
gtk_level_bar_get_real_inverted (GtkLevelBar *self)
{
  if (gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL &&
      self->orientation == GTK_ORIENTATION_HORIZONTAL)
    return !self->inverted;

  return self->inverted;
}

static void
gtk_level_bar_render_trough (GtkGizmo    *gizmo,
                             GtkSnapshot *snapshot)
{
  GtkWidget *widget = GTK_WIDGET (gizmo);
  GtkLevelBar *self = GTK_LEVEL_BAR (gtk_widget_get_parent (GTK_WIDGET (gizmo)));

  if (self->bar_mode == GTK_LEVEL_BAR_MODE_CONTINUOUS)
    {
      gboolean inverted;

      inverted = gtk_level_bar_get_real_inverted (self);

      /* render the empty (unfilled) part */
      gtk_widget_snapshot_child (widget, self->block_widget[inverted ? 0 : 1], snapshot);

      /* now render the filled part on top of it */
      if (self->cur_value != 0)
        gtk_widget_snapshot_child (widget, self->block_widget[inverted ? 1 : 0], snapshot);
    }
  else
    {
      int num_blocks, i;

      num_blocks = gtk_level_bar_get_num_blocks (self);

      for (i = 0; i < num_blocks; i++)
        gtk_widget_snapshot_child (widget, self->block_widget[i], snapshot);
    }
}

static void
gtk_level_bar_measure_trough (GtkGizmo       *gizmo,
                              GtkOrientation  orientation,
                              int             for_size,
                              int            *minimum,
                              int            *natural,
                              int            *minimum_baseline,
                              int            *natural_baseline)
{
  GtkWidget *widget = GTK_WIDGET (gizmo);
  GtkLevelBar *self = GTK_LEVEL_BAR (gtk_widget_get_parent (widget));
  int num_blocks, size;
  int block_width, block_height;

  num_blocks = gtk_level_bar_get_num_blocks (self);
  gtk_level_bar_get_min_block_size (self, &block_width, &block_height);

  if (orientation == GTK_ORIENTATION_HORIZONTAL)
    {
      if (self->orientation == GTK_ORIENTATION_HORIZONTAL)
        size = num_blocks * block_width;
      else
        size = block_width;
    }
  else
    {
      if (self->orientation == GTK_ORIENTATION_VERTICAL)
        size = num_blocks * block_height;
      else
        size = block_height;
    }

  *minimum = size;
  *natural = size;
}

static void
gtk_level_bar_allocate_trough_continuous (GtkLevelBar *self,
                                          int          width,
                                          int          height,
                                          int          baseline)
{
  GtkAllocation block_area;
  double fill_percentage;
  gboolean inverted;
  int block_min;

  inverted = gtk_level_bar_get_real_inverted (self);

  /* allocate the empty (unfilled) part */
  gtk_widget_size_allocate (self->block_widget[inverted ? 0 : 1],
                            &(GtkAllocation) {0, 0, width, height},
                            baseline);

  if (self->cur_value == 0)
    return;

  /* now allocate the filled part */
  block_area = (GtkAllocation) {0, 0, width, height};
  fill_percentage = (self->cur_value - self->min_value) /
    (self->max_value - self->min_value);

  gtk_widget_measure (self->block_widget[inverted ? 1 : 0],
                      self->orientation, -1,
                      &block_min, NULL,
                      NULL, NULL);

  if (self->orientation == GTK_ORIENTATION_HORIZONTAL)
    {
      block_area.width = (int) floor (block_area.width * fill_percentage);
      block_area.width = MAX (block_area.width, block_min);

      if (inverted)
        block_area.x += width - block_area.width;
    }
  else
    {
      block_area.height = (int) floor (block_area.height * fill_percentage);
      block_area.height = MAX (block_area.height, block_min);

      if (inverted)
        block_area.y += height - block_area.height;
    }

  gtk_widget_size_allocate (self->block_widget[inverted ? 1 : 0],
                            &block_area,
                            baseline);
}

static void
gtk_level_bar_allocate_trough_discrete (GtkLevelBar *self,
                                        int          width,
                                        int          height,
                                        int          baseline)
{
  GtkAllocation block_area;
  int num_blocks, i;
  int block_width, block_height;
  int extra_space;

  gtk_level_bar_get_min_block_size (self, &block_width, &block_height);
  num_blocks = gtk_level_bar_get_num_blocks (self);

  if (num_blocks == 0)
    return;

  if (self->orientation == GTK_ORIENTATION_HORIZONTAL)
    {
      block_width = MAX (block_width, (int) floor ((double) width / num_blocks));
      block_height = height;
      extra_space = width - block_width * num_blocks;

      if (extra_space > 0)
        block_width++;
    }
  else
    {
      block_width = width;
      block_height = MAX (block_height, (int) floor ((double) height / num_blocks));
      extra_space = height - block_height * num_blocks;

      if (extra_space > 0)
        block_height++;
    }

  block_area.x = 0;
  block_area.y = 0;
  block_area.width = block_width;
  block_area.height = block_height;

  for (i = 0; i < num_blocks; i++)
    {
      if (extra_space > 0 && i == extra_space)
        {
          if (self->orientation == GTK_ORIENTATION_HORIZONTAL)
            block_area.width--;
          else
            block_area.height--;
        }

      gtk_widget_size_allocate (self->block_widget[i],
                                &block_area,
                                baseline);

      if (self->orientation == GTK_ORIENTATION_HORIZONTAL)
        block_area.x += block_area.width;
      else
        block_area.y += block_area.height;
    }
}

static void
gtk_level_bar_allocate_trough (GtkGizmo *gizmo,
                               int       width,
                               int       height,
                               int       baseline)
{
  GtkWidget *widget = GTK_WIDGET (gizmo);
  GtkLevelBar *self = GTK_LEVEL_BAR (gtk_widget_get_parent (widget));

  if (self->bar_mode == GTK_LEVEL_BAR_MODE_CONTINUOUS)
    gtk_level_bar_allocate_trough_continuous (self, width, height, baseline);
  else
    gtk_level_bar_allocate_trough_discrete (self, width, height, baseline);
}

static void
update_block_nodes (GtkLevelBar *self)
{
  guint n_blocks;
  guint i;

  n_blocks = gtk_level_bar_get_num_block_nodes (self);

  if (self->n_blocks == n_blocks)
    return;
  else if (n_blocks < self->n_blocks)
    {
      for (i = n_blocks; i < self->n_blocks; i++)
        {
          gtk_widget_unparent (self->block_widget[i]);
        }
      self->block_widget = g_renew (GtkWidget*, self->block_widget, n_blocks);
      self->n_blocks = n_blocks;
    }
  else
    {
      self->block_widget = g_renew (GtkWidget*, self->block_widget, n_blocks);
      for (i = self->n_blocks; i < n_blocks; i++)
        {
          self->block_widget[i] = gtk_gizmo_new_with_role ("block",
                                                           GTK_ACCESSIBLE_ROLE_NONE,
                                                           NULL, NULL, NULL, NULL, NULL, NULL);
          gtk_widget_insert_before (self->block_widget[i], GTK_WIDGET (self->trough_widget), NULL);
        }
      self->n_blocks = n_blocks;
    }
}

static void
update_mode_style_classes (GtkLevelBar *self)
{
  GtkCssNode *widget_node;

  widget_node = gtk_widget_get_css_node (GTK_WIDGET (self));
  if (self->bar_mode == GTK_LEVEL_BAR_MODE_CONTINUOUS)
    {
      gtk_css_node_remove_class (widget_node, g_quark_from_static_string ("discrete"));
      gtk_css_node_add_class (widget_node, g_quark_from_static_string ("continuous"));
    }
  else if (self->bar_mode == GTK_LEVEL_BAR_MODE_DISCRETE)
    {
      gtk_css_node_add_class (widget_node, g_quark_from_static_string ("discrete"));
      gtk_css_node_remove_class (widget_node, g_quark_from_static_string ("continuous"));
    }
}

static void
update_level_style_classes (GtkLevelBar *self)
{
  double value;
  const char *value_class = NULL;
  GtkLevelBarOffset *offset, *prev_offset;
  GList *l;
  int num_filled, num_blocks, i;
  gboolean inverted;

  value = gtk_level_bar_get_value (self);

  for (l = self->offsets; l != NULL; l = l->next)
    {
      offset = l->data;

      /* find the right offset for our style class */
      if (value <= offset->value)
        {
          if (l->prev == NULL)
            {
              value_class = offset->name;
            }
          else
            {
              prev_offset = l->prev->data;
              if (prev_offset->value < value)
                value_class = offset->name;
            }
        }

      if (value_class)
        break;
    }

  inverted = gtk_level_bar_get_real_inverted (self);
  num_blocks = gtk_level_bar_get_num_block_nodes (self);

  if (self->bar_mode == GTK_LEVEL_BAR_MODE_CONTINUOUS)
    num_filled = 1;
  else
    num_filled = MIN (num_blocks, (int) round (self->cur_value) - (int) round (self->min_value));

  for (i = 0; i < num_filled; i++)
    {
      GtkCssNode *node = gtk_widget_get_css_node (self->block_widget[inverted ? num_blocks - 1 - i : i]);

      gtk_css_node_set_classes (node, NULL);
      gtk_css_node_add_class (node, g_quark_from_static_string ("filled"));

      if (value_class)
        gtk_css_node_add_class (node, g_quark_from_string (value_class));
    }

  for (; i < num_blocks; i++)
    {
      GtkCssNode *node = gtk_widget_get_css_node (self->block_widget[inverted ? num_blocks - 1 - i : i]);

      gtk_css_node_set_classes (node, NULL);
      gtk_css_node_add_class (node, g_quark_from_static_string ("empty"));
    }
}

static void
gtk_level_bar_direction_changed (GtkWidget        *widget,
                                 GtkTextDirection  previous_dir)
{
  GtkLevelBar *self = GTK_LEVEL_BAR (widget);

  update_level_style_classes (self);

  GTK_WIDGET_CLASS (gtk_level_bar_parent_class)->direction_changed (widget, previous_dir);
}

static void
gtk_level_bar_ensure_offsets_in_range (GtkLevelBar *self)
{
  GtkLevelBarOffset *offset;
  GList *l = self->offsets;

  while (l != NULL)
    {
      offset = l->data;
      l = l->next;

      if (offset->value < self->min_value)
        gtk_level_bar_ensure_offset (self, offset->name, self->min_value);
      else if (offset->value > self->max_value)
        gtk_level_bar_ensure_offset (self, offset->name, self->max_value);
    }
}

typedef struct {
  GtkLevelBar *self;
  GtkBuilder *builder;
  GList *offsets;
} OffsetsParserData;

static void
offset_start_element (GtkBuildableParseContext *context,
                      const char               *element_name,
                      const char              **names,
                      const char              **values,
                      gpointer                  user_data,
                      GError                  **error)
{
  OffsetsParserData *data = user_data;

  if (strcmp (element_name, "offsets") == 0)
    {
      if (!_gtk_builder_check_parent (data->builder, context, "object", error))
        return;

      if (!g_markup_collect_attributes (element_name, names, values, error,
                                        G_MARKUP_COLLECT_INVALID, NULL, NULL,
                                        G_MARKUP_COLLECT_INVALID))
        _gtk_builder_prefix_error (data->builder, context, error);
    }
  else if (strcmp (element_name, "offset") == 0)
    {
      const char *name;
      const char *value;
      GValue gvalue = G_VALUE_INIT;
      GtkLevelBarOffset *offset;

      if (!_gtk_builder_check_parent (data->builder, context, "offsets", error))
        return;

      if (!g_markup_collect_attributes (element_name, names, values, error,
                                        G_MARKUP_COLLECT_STRING, "name", &name,
                                        G_MARKUP_COLLECT_STRING, "value", &value,
                                        G_MARKUP_COLLECT_INVALID))
        {
          _gtk_builder_prefix_error (data->builder, context, error);
          return;
        }

      if (!gtk_builder_value_from_string_type (data->builder, G_TYPE_DOUBLE, value, &gvalue, error))
        {
          _gtk_builder_prefix_error (data->builder, context, error);
          return;
        }

      offset = gtk_level_bar_offset_new (name, g_value_get_double (&gvalue));
      data->offsets = g_list_prepend (data->offsets, offset);
    }
  else
    {
      _gtk_builder_error_unhandled_tag (data->builder, context,
                                        "GtkLevelBar", element_name,
                                        error);
    }
}

static const GtkBuildableParser offset_parser =
{
  offset_start_element
};

static GtkBuildableIface *parent_buildable_iface;

static gboolean
gtk_level_bar_buildable_custom_tag_start (GtkBuildable       *buildable,
                                          GtkBuilder         *builder,
                                          GObject            *child,
                                          const char         *tagname,
                                          GtkBuildableParser *parser,
                                          gpointer           *parser_data)
{
  OffsetsParserData *data;

  if (parent_buildable_iface->custom_tag_start (buildable, builder, child,
                                                tagname, parser, parser_data))
    return TRUE;

  if (child)
    return FALSE;

  if (strcmp (tagname, "offsets") != 0)
    return FALSE;

  data = g_new0 (OffsetsParserData, 1);
  data->self = GTK_LEVEL_BAR (buildable);
  data->builder = builder;
  data->offsets = NULL;

  *parser = offset_parser;
  *parser_data = data;

  return TRUE;
}

static void
gtk_level_bar_buildable_custom_finished (GtkBuildable *buildable,
                                         GtkBuilder   *builder,
                                         GObject      *child,
                                         const char   *tagname,
                                         gpointer      user_data)
{
  OffsetsParserData *data = user_data;
  GtkLevelBar *self;
  GtkLevelBarOffset *offset;
  GList *l;

  self = data->self;

  if (strcmp (tagname, "offsets") != 0)
    {
      parent_buildable_iface->custom_finished (buildable, builder, child,
                                               tagname, user_data);
      return;
    }

  for (l = data->offsets; l != NULL; l = l->next)
    {
      offset = l->data;
      gtk_level_bar_add_offset_value (self, offset->name, offset->value);
    }

  g_list_free_full (data->offsets, (GDestroyNotify) gtk_level_bar_offset_free);
  g_free (data);
}

static void
gtk_level_bar_buildable_init (GtkBuildableIface *iface)
{
  parent_buildable_iface = g_type_interface_peek_parent (iface);

  iface->custom_tag_start = gtk_level_bar_buildable_custom_tag_start;
  iface->custom_finished = gtk_level_bar_buildable_custom_finished;
}

static void
gtk_level_bar_set_orientation (GtkLevelBar    *self,
                               GtkOrientation  orientation)
{
  if (self->orientation != orientation)
    {
      self->orientation = orientation;
      gtk_widget_update_orientation (GTK_WIDGET (self), self->orientation);
      gtk_widget_queue_resize (GTK_WIDGET (self));
      g_object_notify (G_OBJECT (self), "orientation");
    }
}

static void
gtk_level_bar_get_property (GObject    *obj,
                            guint       property_id,
                            GValue     *value,
                            GParamSpec *pspec)
{
  GtkLevelBar *self = GTK_LEVEL_BAR (obj);

  switch (property_id)
    {
    case PROP_VALUE:
      g_value_set_double (value, gtk_level_bar_get_value (self));
      break;
    case PROP_MIN_VALUE:
      g_value_set_double (value, gtk_level_bar_get_min_value (self));
      break;
    case PROP_MAX_VALUE:
      g_value_set_double (value, gtk_level_bar_get_max_value (self));
      break;
    case PROP_MODE:
      g_value_set_enum (value, gtk_level_bar_get_mode (self));
      break;
    case PROP_INVERTED:
      g_value_set_boolean (value, gtk_level_bar_get_inverted (self));
      break;
    case PROP_ORIENTATION:
      g_value_set_enum (value, self->orientation);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, property_id, pspec);
      break;
    }
}

static void
gtk_level_bar_set_property (GObject      *obj,
                            guint         property_id,
                            const GValue *value,
                            GParamSpec   *pspec)
{
  GtkLevelBar *self = GTK_LEVEL_BAR (obj);

  switch (property_id)
    {
    case PROP_VALUE:
      gtk_level_bar_set_value (self, g_value_get_double (value));
      break;
    case PROP_MIN_VALUE:
      gtk_level_bar_set_min_value (self, g_value_get_double (value));
      break;
    case PROP_MAX_VALUE:
      gtk_level_bar_set_max_value (self, g_value_get_double (value));
      break;
    case PROP_MODE:
      gtk_level_bar_set_mode (self, g_value_get_enum (value));
      break;
    case PROP_INVERTED:
      gtk_level_bar_set_inverted (self, g_value_get_boolean (value));
      break;
    case PROP_ORIENTATION:
      gtk_level_bar_set_orientation (self, g_value_get_enum (value));
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, property_id, pspec);
      break;
    }
}

static void
gtk_level_bar_finalize (GObject *obj)
{
  GtkLevelBar *self = GTK_LEVEL_BAR (obj);
  int i;

  g_list_free_full (self->offsets, (GDestroyNotify) gtk_level_bar_offset_free);

  for (i = 0; i < self->n_blocks; i++)
    gtk_widget_unparent (self->block_widget[i]);

  g_free (self->block_widget);

  gtk_widget_unparent (self->trough_widget);

  G_OBJECT_CLASS (gtk_level_bar_parent_class)->finalize (obj);
}

static void
gtk_level_bar_class_init (GtkLevelBarClass *klass)
{
  GObjectClass *oclass = G_OBJECT_CLASS (klass);
  GtkWidgetClass *wclass = GTK_WIDGET_CLASS (klass);

  oclass->get_property = gtk_level_bar_get_property;
  oclass->set_property = gtk_level_bar_set_property;
  oclass->finalize = gtk_level_bar_finalize;

  wclass->direction_changed = gtk_level_bar_direction_changed;

  g_object_class_override_property (oclass, PROP_ORIENTATION, "orientation");

  /**
   * GtkLevelBar::offset-changed:
   * @self: a `GtkLevelBar`
   * @name: the name of the offset that changed value
   *
   * Emitted when an offset specified on the bar changes value.
   *
   * This typically is the result of a [method@Gtk.LevelBar.add_offset_value]
   * call.
   *
   * The signal supports detailed connections; you can connect to the
   * detailed signal "changed::x" in order to only receive callbacks when
   * the value of offset "x" changes.
   */
  signals[SIGNAL_OFFSET_CHANGED] =
    g_signal_new (I_("offset-changed"),
                  GTK_TYPE_LEVEL_BAR,
                  G_SIGNAL_RUN_FIRST | G_SIGNAL_DETAILED,
                  G_STRUCT_OFFSET (GtkLevelBarClass, offset_changed),
                  NULL, NULL,
                  NULL,
                  G_TYPE_NONE,
                  1, G_TYPE_STRING);

  /**
   * GtkLevelBar:value:
   *
   * Determines the currently filled value of the level bar.
   */
  properties[PROP_VALUE] =
    g_param_spec_double ("value", NULL, NULL,
                         0.0, G_MAXDOUBLE, 0.0,
                         G_PARAM_READWRITE|G_PARAM_STATIC_STRINGS|G_PARAM_EXPLICIT_NOTIFY);

  /**
   * GtkLevelBar:min-value:
   *
   * Determines the minimum value of the interval that can be displayed by the bar.
   */
  properties[PROP_MIN_VALUE] =
    g_param_spec_double ("min-value", NULL, NULL,
                         0.0, G_MAXDOUBLE, 0.0,
                         G_PARAM_READWRITE|G_PARAM_STATIC_STRINGS|G_PARAM_EXPLICIT_NOTIFY);

  /**
   * GtkLevelBar:max-value:
   *
   * Determines the maximum value of the interval that can be displayed by the bar.
   */
  properties[PROP_MAX_VALUE] =
    g_param_spec_double ("max-value", NULL, NULL,
                         0.0, G_MAXDOUBLE, 1.0,
                         G_PARAM_READWRITE|G_PARAM_STATIC_STRINGS|G_PARAM_EXPLICIT_NOTIFY);

  /**
   * GtkLevelBar:mode:
   *
   * Determines the way `GtkLevelBar` interprets the value properties to draw the
   * level fill area.
   *
   * Specifically, when the value is %GTK_LEVEL_BAR_MODE_CONTINUOUS,
   * `GtkLevelBar` will draw a single block representing the current value in
   * that area; when the value is %GTK_LEVEL_BAR_MODE_DISCRETE,
   * the widget will draw a succession of separate blocks filling the
   * draw area, with the number of blocks being equal to the units separating
   * the integral roundings of [property@Gtk.LevelBar:min-value] and
   * [property@Gtk.LevelBar:max-value].
   */
  properties[PROP_MODE] =
    g_param_spec_enum ("mode", NULL, NULL,
                       GTK_TYPE_LEVEL_BAR_MODE,
                       GTK_LEVEL_BAR_MODE_CONTINUOUS,
                       G_PARAM_READWRITE|G_PARAM_STATIC_STRINGS|G_PARAM_EXPLICIT_NOTIFY);

  /**
   * GtkLevelBar:inverted:
   *
   * Whether the `GtkLeveBar` is inverted.
   *
   * Level bars normally grow from top to bottom or left to right.
   * Inverted level bars grow in the opposite direction.
   */
  properties[PROP_INVERTED] =
    g_param_spec_boolean ("inverted", NULL, NULL,
                          FALSE,
                          G_PARAM_READWRITE|G_PARAM_STATIC_STRINGS|G_PARAM_EXPLICIT_NOTIFY);

  g_object_class_install_properties (oclass, LAST_PROPERTY, properties);

  gtk_widget_class_set_layout_manager_type (wclass, GTK_TYPE_BIN_LAYOUT);
  gtk_widget_class_set_css_name (wclass, I_("levelbar"));
  gtk_widget_class_set_accessible_role (wclass, GTK_ACCESSIBLE_ROLE_METER);
}

static void
gtk_level_bar_init (GtkLevelBar *self)
{
  self->cur_value = 0.0;
  self->min_value = 0.0;
  self->max_value = 1.0;

  /* set initial orientation and style classes */
  self->orientation = GTK_ORIENTATION_HORIZONTAL;
  gtk_widget_update_orientation (GTK_WIDGET (self), self->orientation);

  self->inverted = FALSE;

  self->trough_widget = gtk_gizmo_new_with_role ("trough",
                                                 GTK_ACCESSIBLE_ROLE_NONE,
                                                 gtk_level_bar_measure_trough,
                                                 gtk_level_bar_allocate_trough,
                                                 gtk_level_bar_render_trough,
                                                 NULL,
                                                 NULL, NULL);
  gtk_widget_set_parent (self->trough_widget, GTK_WIDGET (self));

  gtk_level_bar_ensure_offset (self, GTK_LEVEL_BAR_OFFSET_LOW, 0.25);
  gtk_level_bar_ensure_offset (self, GTK_LEVEL_BAR_OFFSET_HIGH, 0.75);
  gtk_level_bar_ensure_offset (self, GTK_LEVEL_BAR_OFFSET_FULL, 1.0);

  self->block_widget = NULL;
  self->n_blocks = 0;

  self->bar_mode = GTK_LEVEL_BAR_MODE_CONTINUOUS;
  update_mode_style_classes (self);
  update_block_nodes (self);
  update_level_style_classes (self);

  gtk_accessible_update_property (GTK_ACCESSIBLE (self),
                                  GTK_ACCESSIBLE_PROPERTY_VALUE_MAX, 1.0,
                                  GTK_ACCESSIBLE_PROPERTY_VALUE_MIN, 0.0,
                                  GTK_ACCESSIBLE_PROPERTY_VALUE_NOW, 0.0,
                                  -1);
}

/**
 * gtk_level_bar_new:
 *
 * Creates a new `GtkLevelBar`.
 *
 * Returns: a `GtkLevelBar`.
 */
GtkWidget *
gtk_level_bar_new (void)
{
  return g_object_new (GTK_TYPE_LEVEL_BAR, NULL);
}

/**
 * gtk_level_bar_new_for_interval:
 * @min_value: a positive value
 * @max_value: a positive value
 *
 * Creates a new `GtkLevelBar` for the specified interval.
 *
 * Returns: a `GtkLevelBar`
 */
GtkWidget *
gtk_level_bar_new_for_interval (double min_value,
                                double max_value)
{
  return g_object_new (GTK_TYPE_LEVEL_BAR,
                       "min-value", min_value,
                       "max-value", max_value,
                       NULL);
}

/**
 * gtk_level_bar_get_min_value:
 * @self: a `GtkLevelBar`
 *
 * Returns the `min-value` of the `GtkLevelBar`.
 *
 * Returns: a positive value
 */
double
gtk_level_bar_get_min_value (GtkLevelBar *self)
{
  g_return_val_if_fail (GTK_IS_LEVEL_BAR (self), 0.0);

  return self->min_value;
}

/**
 * gtk_level_bar_get_max_value:
 * @self: a `GtkLevelBar`
 *
 * Returns the `max-value` of the `GtkLevelBar`.
 *
 * Returns: a positive value
 */
double
gtk_level_bar_get_max_value (GtkLevelBar *self)
{
  g_return_val_if_fail (GTK_IS_LEVEL_BAR (self), 0.0);

  return self->max_value;
}

/**
 * gtk_level_bar_get_value:
 * @self: a `GtkLevelBar`
 *
 * Returns the `value` of the `GtkLevelBar`.
 *
 * Returns: a value in the interval between
 *   [property@Gtk.LevelBar:min-value] and [property@Gtk.LevelBar:max-value]
 */
double
gtk_level_bar_get_value (GtkLevelBar *self)
{
  g_return_val_if_fail (GTK_IS_LEVEL_BAR (self), 0.0);

  return self->cur_value;
}

static void
gtk_level_bar_set_value_internal (GtkLevelBar *self,
                                  double       value)
{
  self->cur_value = value;

  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_VALUE]);

  gtk_widget_queue_allocate (GTK_WIDGET (self->trough_widget));
}

/**
 * gtk_level_bar_set_min_value:
 * @self: a `GtkLevelBar`
 * @value: a positive value
 *
 * Sets the `min-value` of the `GtkLevelBar`.
 *
 * You probably want to update preexisting level offsets after calling
 * this function.
 */
void
gtk_level_bar_set_min_value (GtkLevelBar *self,
                             double       value)
{
  g_return_if_fail (GTK_IS_LEVEL_BAR (self));
  g_return_if_fail (value >= 0.0);

  if (value == self->min_value)
    return;

  self->min_value = value;

  if (self->min_value > self->cur_value)
    gtk_level_bar_set_value_internal (self, self->min_value);

  update_block_nodes (self);
  update_level_style_classes (self);

  gtk_accessible_update_property (GTK_ACCESSIBLE (self),
                                  GTK_ACCESSIBLE_PROPERTY_VALUE_MIN, self->min_value,
                                  GTK_ACCESSIBLE_PROPERTY_VALUE_NOW, self->cur_value,
                                  -1);

  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_MIN_VALUE]);
}

/**
 * gtk_level_bar_set_max_value:
 * @self: a `GtkLevelBar`
 * @value: a positive value
 *
 * Sets the `max-value` of the `GtkLevelBar`.
 *
 * You probably want to update preexisting level offsets after calling
 * this function.
 */
void
gtk_level_bar_set_max_value (GtkLevelBar *self,
                             double       value)
{
  g_return_if_fail (GTK_IS_LEVEL_BAR (self));
  g_return_if_fail (value >= 0.0);

  if (value == self->max_value)
    return;

  self->max_value = value;

  if (self->max_value < self->cur_value)
    gtk_level_bar_set_value_internal (self, self->max_value);

  gtk_level_bar_ensure_offsets_in_range (self);
  update_block_nodes (self);
  update_level_style_classes (self);

  gtk_accessible_update_property (GTK_ACCESSIBLE (self),
                                  GTK_ACCESSIBLE_PROPERTY_VALUE_MAX, self->max_value,
                                  GTK_ACCESSIBLE_PROPERTY_VALUE_NOW, self->cur_value,
                                  -1);

  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_MAX_VALUE]);
}

/**
 * gtk_level_bar_set_value:
 * @self: a `GtkLevelBar`
 * @value: a value in the interval between
 *   [property@Gtk.LevelBar:min-value] and [property@Gtk.LevelBar:max-value]
 *
 * Sets the value of the `GtkLevelBar`.
 */
void
gtk_level_bar_set_value (GtkLevelBar *self,
                         double       value)
{
  g_return_if_fail (GTK_IS_LEVEL_BAR (self));

  if (value == self->cur_value)
    return;

  gtk_level_bar_set_value_internal (self, value);
  update_level_style_classes (self);

  gtk_accessible_update_property (GTK_ACCESSIBLE (self),
                                  GTK_ACCESSIBLE_PROPERTY_VALUE_NOW, self->cur_value,
                                  -1);
}

/**
 * gtk_level_bar_get_mode:
 * @self: a `GtkLevelBar`
 *
 * Returns the `mode` of the `GtkLevelBar`.
 *
 * Returns: a `GtkLevelBarMode`
 */
GtkLevelBarMode
gtk_level_bar_get_mode (GtkLevelBar *self)
{
  g_return_val_if_fail (GTK_IS_LEVEL_BAR (self), 0);

  return self->bar_mode;
}

/**
 * gtk_level_bar_set_mode:
 * @self: a `GtkLevelBar`
 * @mode: a `GtkLevelBarMode`
 *
 * Sets the `mode` of the `GtkLevelBar`.
 */
void
gtk_level_bar_set_mode (GtkLevelBar     *self,
                        GtkLevelBarMode  mode)
{
  g_return_if_fail (GTK_IS_LEVEL_BAR (self));

  if (self->bar_mode == mode)
    return;

  self->bar_mode = mode;

  update_mode_style_classes (self);
  update_block_nodes (self);
  update_level_style_classes (self);
  gtk_widget_queue_resize (GTK_WIDGET (self));
  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_MODE]);

}

/**
 * gtk_level_bar_get_inverted:
 * @self: a `GtkLevelBar`
 *
 * Returns whether the levelbar is inverted.
 *
 * Returns: %TRUE if the level bar is inverted
 */
gboolean
gtk_level_bar_get_inverted (GtkLevelBar *self)
{
  g_return_val_if_fail (GTK_IS_LEVEL_BAR (self), FALSE);

  return self->inverted;
}

/**
 * gtk_level_bar_set_inverted:
 * @self: a `GtkLevelBar`
 * @inverted: %TRUE to invert the level bar
 *
 * Sets whether the `GtkLevelBar` is inverted.
 */
void
gtk_level_bar_set_inverted (GtkLevelBar *self,
                            gboolean     inverted)
{
  g_return_if_fail (GTK_IS_LEVEL_BAR (self));

  if (self->inverted == inverted)
    return;

  self->inverted = inverted;
  gtk_widget_queue_resize (GTK_WIDGET (self));
  update_level_style_classes (self);
  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_INVERTED]);
}

/**
 * gtk_level_bar_remove_offset_value:
 * @self: a `GtkLevelBar`
 * @name: (nullable): the name of an offset in the bar
 *
 * Removes an offset marker from a `GtkLevelBar`.
 *
 * The marker must have been previously added with
 * [method@Gtk.LevelBar.add_offset_value].
 */
void
gtk_level_bar_remove_offset_value (GtkLevelBar *self,
                                   const char *name)
{
  GList *existing;

  g_return_if_fail (GTK_IS_LEVEL_BAR (self));

  existing = g_list_find_custom (self->offsets, name, offset_find_func);
  if (existing)
    {
      gtk_level_bar_offset_free (existing->data);
      self->offsets = g_list_delete_link (self->offsets, existing);

      update_level_style_classes (self);
    }
}

/**
 * gtk_level_bar_add_offset_value:
 * @self: a `GtkLevelBar`
 * @name: the name of the new offset
 * @value: the value for the new offset
 *
 * Adds a new offset marker on @self at the position specified by @value.
 *
 * When the bar value is in the interval topped by @value (or between @value
 * and [property@Gtk.LevelBar:max-value] in case the offset is the last one
 * on the bar) a style class named `level-`@name will be applied
 * when rendering the level bar fill.
 *
 * If another offset marker named @name exists, its value will be
 * replaced by @value.
 */
void
gtk_level_bar_add_offset_value (GtkLevelBar *self,
                                const char *name,
                                double       value)
{
  GQuark name_quark;

  g_return_if_fail (GTK_IS_LEVEL_BAR (self));
  g_return_if_fail (gtk_level_bar_value_in_interval (self, value));

  if (!gtk_level_bar_ensure_offset (self, name, value))
    return;

  update_level_style_classes (self);
  name_quark = g_quark_from_string (name);
  g_signal_emit (self, signals[SIGNAL_OFFSET_CHANGED], name_quark, name);
}

/**
 * gtk_level_bar_get_offset_value:
 * @self: a `GtkLevelBar`
 * @name: (nullable): the name of an offset in the bar
 * @value: (out): location where to store the value
 *
 * Fetches the value specified for the offset marker @name in @self.
 *
 * Returns: %TRUE if the specified offset is found
 */
gboolean
gtk_level_bar_get_offset_value (GtkLevelBar *self,
                                const char *name,
                                double      *value)
{
  GList *existing;
  GtkLevelBarOffset *offset = NULL;

  g_return_val_if_fail (GTK_IS_LEVEL_BAR (self), FALSE);

  existing = g_list_find_custom (self->offsets, name, offset_find_func);
  if (existing)
    offset = existing->data;

  if (!offset)
    return FALSE;

  if (value)
    *value = offset->value;

  return TRUE;
}
