/*
 * Copyright © 2025 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.1 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/>.
 *
 * SPDX-License-Identifier: LGPL-2.1-or-later
 *
 * Authors: Matthias Clasen <mclasen@redhat.com>
 */

#include "range-editor.h"

/* {{{ Gizmo */

#define GIZMO_TYPE (gizmo_get_type ())
G_DECLARE_FINAL_TYPE (Gizmo, gizmo, GIZ, MO, GtkWidget)

typedef void    (* GizmoMeasureFunc)   (Gizmo          *gizmo,
                                        GtkOrientation  orientation,
                                        int             for_size,
                                        int            *minimum,
                                        int            *natural,
                                        int            *minimum_baseline,
                                        int            *natural_baseline);
typedef void    (* GizmoAllocateFunc)  (Gizmo          *gizmo,
                                        int             width,
                                        int             height,
                                        int             baseline);
typedef void    (* GizmoSnapshotFunc)  (Gizmo          *gizmo,
                                        GtkSnapshot    *snapshot);

struct _Gizmo
{
  GtkWidget parent_instance;

  GizmoMeasureFunc   measure_func;
  GizmoAllocateFunc  allocate_func;
  GizmoSnapshotFunc  snapshot_func;
};

struct _GizmoClass
{
  GtkWidgetClass parent_class;
};

G_DEFINE_TYPE (Gizmo, gizmo, GTK_TYPE_WIDGET);

static void
gizmo_measure (GtkWidget      *widget,
               GtkOrientation  orientation,
               int             for_size,
               int            *minimum,
               int            *natural,
               int            *minimum_baseline,
               int            *natural_baseline)
{
  Gizmo *self = GIZ_MO (widget);

  if (self->measure_func)
    self->measure_func (self, orientation, for_size,
                        minimum, natural,
                        minimum_baseline, natural_baseline);
}

static void
gizmo_size_allocate (GtkWidget *widget,
                     int        width,
                     int        height,
                     int        baseline)
{
  Gizmo *self = GIZ_MO (widget);

  if (self->allocate_func)
    self->allocate_func (self, width, height, baseline);
}

static void
gizmo_snapshot (GtkWidget   *widget,
                GtkSnapshot *snapshot)
{
  Gizmo *self = GIZ_MO (widget);

  if (self->snapshot_func)
    self->snapshot_func (self, snapshot);
  else
    GTK_WIDGET_CLASS (gizmo_parent_class)->snapshot (widget, snapshot);
}

static void
gizmo_finalize (GObject *object)
{
  Gizmo *self = GIZ_MO (object);
  GtkWidget *child;

  while ((child = gtk_widget_get_first_child (GTK_WIDGET (self))) != NULL)
    gtk_widget_unparent (child);

  G_OBJECT_CLASS (gizmo_parent_class)->finalize (object);
}

static void
gizmo_class_init (GizmoClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);
  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);

  object_class->finalize = gizmo_finalize;

  widget_class->measure = gizmo_measure;
  widget_class->size_allocate = gizmo_size_allocate;
  widget_class->snapshot = gizmo_snapshot;
}

static void
gizmo_init (Gizmo *self)
{
}

static GtkWidget *
gizmo_new (const char        *css_name,
           GizmoMeasureFunc   measure_func,
           GizmoAllocateFunc  allocate_func,
           GizmoSnapshotFunc  snapshot_func)
{
  Gizmo *gizmo;

  gizmo = g_object_new (GIZMO_TYPE,
                        "css-name", css_name,
                        "accessible-role", GTK_ACCESSIBLE_ROLE_NONE,
                        NULL);

  gizmo->measure_func  = measure_func;
  gizmo->allocate_func = allocate_func;
  gizmo->snapshot_func = snapshot_func;

  return GTK_WIDGET (gizmo);
}

/* }}} */

struct _RangeEditor
{
  GtkWidget parent_instance;

  float lower;
  float upper;
  float value1;
  float value2;

  GtkWidget *trough;
  GtkWidget *highlight;
  GtkWidget *slider1;
  GtkWidget *slider2;

  GtkGesture *drag_gesture;
  GtkWidget *mouse_location;
};

enum
{
  PROP_LOWER = 1,
  PROP_UPPER,
  PROP_VALUE1,
  PROP_VALUE2,
  NUM_PROPERTIES,
};

static GParamSpec *properties[NUM_PROPERTIES];

struct _RangeEditorClass
{
  GtkWidgetClass parent_class;
};

G_DEFINE_TYPE (RangeEditor, range_editor, GTK_TYPE_WIDGET)

/* {{{ Setters */

static void
range_editor_set_values (RangeEditor *self,
                         float        value1,
                         float        value2)
{
  g_return_if_fail (self->lower <= value1);
  g_return_if_fail (value1 <= value2);
  g_return_if_fail (value2 <= self->upper);

  if (self->value1 == value1 && self->value2 == value2)
    return;

  g_object_freeze_notify (G_OBJECT (self));

  if (self->value1 != value1)
    {
      self->value1 = value1;
      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_VALUE1]);
    }

  if (self->value2 != value2)
    {
      self->value2 = value2;
      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_VALUE2]);
    }

  g_object_thaw_notify (G_OBJECT (self));
  gtk_widget_queue_resize (self->highlight);
  gtk_widget_queue_resize (self->slider1);
  gtk_widget_queue_resize (self->slider2);
}

static void
range_editor_set_limits (RangeEditor *self,
                         float        lower,
                         float        upper)
{
  g_return_if_fail (lower <= upper);

  if (self->lower == lower && self->upper == upper)
    return;

  g_object_freeze_notify (G_OBJECT (self));

  if (self->lower != lower)
    {
      self->lower = lower;
      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_LOWER]);
    }

  if (self->upper != upper)
    {
      self->upper = upper;
      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_UPPER]);
    }

  range_editor_set_values (self,
                           CLAMP (self->value1, self->lower, self->upper),
                           CLAMP (self->value2, self->lower, self->upper));

  g_object_thaw_notify (G_OBJECT (self));
  gtk_widget_queue_resize (self->highlight);
}

/* }}} */
/* {{{ Utilities */

static void
measure_widget (GtkWidget      *widget,
                GtkOrientation  orientation,
                int            *min,
                int            *nat)
{
  gtk_widget_measure (widget, orientation, -1, min, nat, NULL, NULL);
}

/* }}} */
/* {{{ Trough */

static void
measure_trough (Gizmo          *gizmo,
                GtkOrientation  orientation,
                int             for_size,
                int            *minimum,
                int            *natural,
                int            *minimum_baseline,
                int            *natural_baseline)
{
  RangeEditor *self = RANGE_EDITOR (gtk_widget_get_parent (GTK_WIDGET (gizmo)));
  int min, nat;

  measure_widget (self->highlight, orientation, minimum, natural);
  measure_widget (self->slider1, orientation, &min, &nat);

  if (orientation == GTK_ORIENTATION_HORIZONTAL)
    {
      *minimum += min;
      *natural += nat;
    }
  else
    {
      *minimum = MAX (*minimum, min);
      *natural = MAX (*natural, nat);
    }

  measure_widget (self->slider2, orientation, &min, &nat);

  if (orientation == GTK_ORIENTATION_HORIZONTAL)
    {
      *minimum += min;
      *natural += nat;
    }
  else
    {
      *minimum = MAX (*minimum, min);
      *natural = MAX (*natural, nat);
    }
}

static void
allocate_trough (Gizmo *gizmo,
                 int    width,
                 int    height,
                 int    baseline)
{
  RangeEditor *self = RANGE_EDITOR (gtk_widget_get_parent (GTK_WIDGET (gizmo)));
  GtkAllocation alloc;
  float x1, x2;
  int slider_width, slider_height;

  x1 = width * (self->value1 - self->lower) / (self->upper - self->lower);
  x2 = width * (self->value2 - self->lower) / (self->upper - self->lower);

  alloc.x = x1;
  alloc.y = 0;
  alloc.width = x2 - x1;
  alloc.height = height;

  gtk_widget_size_allocate (self->highlight, &alloc, -1);

  measure_widget (self->slider1, GTK_ORIENTATION_HORIZONTAL, &slider_width, NULL);
  measure_widget (self->slider1, GTK_ORIENTATION_VERTICAL, &slider_height, NULL);

  alloc.x = x1 - slider_width;
  alloc.y = (height - slider_height) / 2;
  alloc.width = slider_width;
  alloc.height = slider_height;

  gtk_widget_size_allocate (self->slider1, &alloc, -1);

  measure_widget (self->slider2, GTK_ORIENTATION_HORIZONTAL, &slider_width, NULL);
  measure_widget (self->slider2, GTK_ORIENTATION_VERTICAL, &slider_height, NULL);

  alloc.x = x2;
  alloc.y = (height - slider_height) / 2;
  alloc.width = slider_width;
  alloc.height = slider_height;

  gtk_widget_size_allocate (self->slider2, &alloc, -1);
}

static void
render_trough (Gizmo       *gizmo,
               GtkSnapshot *snapshot)
{
  RangeEditor *self = RANGE_EDITOR (gtk_widget_get_parent (GTK_WIDGET (gizmo)));

  gtk_widget_snapshot_child (self->trough, self->highlight, snapshot);
  gtk_widget_snapshot_child (self->trough, self->slider1, snapshot);
  gtk_widget_snapshot_child (self->trough, self->slider2, snapshot);
}

/* }}} */
/* {{{ Input */

static void
drag_gesture_begin (GtkGestureDrag *gesture,
                    double          offset_x,
                    double          offset_y,
                    RangeEditor    *self)
{
  self->mouse_location = gtk_widget_pick (GTK_WIDGET (self), offset_x, offset_y, 0);

  if (self->mouse_location == self->slider1 ||
      self->mouse_location == self->slider2)
    gtk_gesture_set_state (self->drag_gesture, GTK_EVENT_SEQUENCE_CLAIMED);
}

static void
drag_gesture_end (GtkGestureDrag *gesture,
                  double          offset_x,
                  double          offset_y,
                  RangeEditor    *self)
{
  self->mouse_location = NULL;
}

static void
drag_gesture_update (GtkGestureDrag *gesture,
                     double          offset_x,
                     double          offset_y,
                     RangeEditor    *self)
{
  double start_x, start_y;

  if (self->mouse_location == self->slider1 ||
      self->mouse_location == self->slider2)
    {
      int mouse_x;
      float v;
      graphene_rect_t bounds;

      gtk_gesture_drag_get_start_point (gesture, &start_x, &start_y);
      mouse_x = start_x + offset_x;

      if (!gtk_widget_compute_bounds (self->trough, GTK_WIDGET (self), &bounds))
        g_assert_not_reached ();

      v = self->lower + (mouse_x - bounds.origin.x) / bounds.size.width * (self->upper - self->lower);

      if (self->mouse_location == self->slider1)
        {
          v = CLAMP (v, self->lower, self->upper);
          range_editor_set_values (self, MIN (v, self->value2), self->value2);
        }
      else if (self->mouse_location == self->slider2)
        {
          v = CLAMP (v, self->lower, self->upper);
          range_editor_set_values (self, self->value1, MAX (self->value1, v));
        }
    }
}

/* }}} */
/* {{{ GtkWidget implementation */

static void
range_editor_measure (GtkWidget      *widget,
                      GtkOrientation  orientation,
                      int             for_size,
                      int            *minimum,
                      int            *natural,
                      int            *minimum_baseline,
                      int            *natural_baseline)
{
  RangeEditor *self = RANGE_EDITOR (widget);

  measure_widget (self->trough, orientation, minimum, natural);
}

static void
range_editor_size_allocate (GtkWidget *widget,
                            int        width,
                            int        height,
                            int        baseline)
{
  RangeEditor *self = RANGE_EDITOR (widget);
  GtkAllocation alloc;
  int min_height;
  int slider1_width, slider2_width;

  measure_widget (self->slider1, GTK_ORIENTATION_HORIZONTAL, &slider1_width, NULL);
  measure_widget (self->slider2, GTK_ORIENTATION_HORIZONTAL, &slider2_width, NULL);
  measure_widget (self->trough, GTK_ORIENTATION_VERTICAL, &min_height, NULL);

  alloc.x = slider1_width;
  alloc.y = (height - min_height) / 2;
  alloc.width = width - slider1_width - slider2_width;
  alloc.height = min_height;

  gtk_widget_size_allocate (self->trough, &alloc, -1);
}

/* }}} */
/* {{{ GObject boilerplate */

static void
range_editor_init (RangeEditor *self)
{
  self->lower = 0;
  self->upper = 0;
  self->value1 = 0;
  self->value2 = 0;

  self->trough = gizmo_new ("trough",
                            measure_trough,
                            allocate_trough,
                            render_trough);
  gtk_widget_set_parent (self->trough, GTK_WIDGET (self));

  self->highlight = gizmo_new ("highlight", NULL, NULL, NULL);
  gtk_widget_set_parent (self->highlight, self->trough);

  self->slider1 = gizmo_new ("slider", NULL, NULL, NULL);
  gtk_widget_set_parent (self->slider1, self->trough);
  gtk_widget_add_css_class (self->slider1, "left");

  self->slider2 = gizmo_new ("slider", NULL, NULL, NULL);
  gtk_widget_set_parent (self->slider2, self->trough);
  gtk_widget_add_css_class (self->slider2, "right");

  self->drag_gesture = gtk_gesture_drag_new ();
  gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (self->drag_gesture), 0);
  g_signal_connect (self->drag_gesture, "drag-begin",
                    G_CALLBACK (drag_gesture_begin), self);
  g_signal_connect (self->drag_gesture, "drag-update",
                    G_CALLBACK (drag_gesture_update), self);
  g_signal_connect (self->drag_gesture, "drag-end",
                    G_CALLBACK (drag_gesture_end), self);

  gtk_widget_add_controller (GTK_WIDGET (self), GTK_EVENT_CONTROLLER (self->drag_gesture));
}

static void
range_editor_set_property (GObject      *object,
                          guint         prop_id,
                          const GValue *value,
                          GParamSpec   *pspec)
{
  RangeEditor *self = RANGE_EDITOR (object);
  float v;

  switch (prop_id)
    {
    case PROP_LOWER:
      v = g_value_get_float (value);
      range_editor_set_limits (self, v, MAX (v, self->upper));
      break;

    case PROP_UPPER:
      v = g_value_get_float (value);
      range_editor_set_limits (self, MIN (v, self->lower), v);
      break;

    case PROP_VALUE1:
      v = g_value_get_float (value);
      range_editor_set_values (self, v, MAX (v, self->value2));
      break;

    case PROP_VALUE2:
      v = g_value_get_float (value);
      range_editor_set_values (self, MIN (v, self->value1), v);
      break;

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

static void
range_editor_get_property (GObject    *object,
                           guint       prop_id,
                           GValue     *value,
                           GParamSpec *pspec)
{
  RangeEditor *self = RANGE_EDITOR (object);

  switch (prop_id)
    {
    case PROP_LOWER:
      g_value_set_float (value, self->lower);
      break;

    case PROP_UPPER:
      g_value_set_float (value, self->upper);
      break;

    case PROP_VALUE1:
      g_value_set_float (value, self->value1);
      break;

    case PROP_VALUE2:
      g_value_set_float (value, self->value2);
      break;

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

static void
range_editor_dispose (GObject *object)
{
  RangeEditor *self = RANGE_EDITOR (object);

  g_clear_pointer (&self->highlight, gtk_widget_unparent);
  g_clear_pointer (&self->slider1, gtk_widget_unparent);
  g_clear_pointer (&self->slider2, gtk_widget_unparent);
  g_clear_pointer (&self->trough, gtk_widget_unparent);

  G_OBJECT_CLASS (range_editor_parent_class)->dispose (object);
}

static void
range_editor_finalize (GObject *object)
{
//  RangeEditor *self = RANGE_EDITOR (object);

  G_OBJECT_CLASS (range_editor_parent_class)->finalize (object);
}

static void
range_editor_class_init (RangeEditorClass *class)
{
  GObjectClass *object_class = G_OBJECT_CLASS (class);
  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (class);

  object_class->set_property = range_editor_set_property;
  object_class->get_property = range_editor_get_property;
  object_class->dispose = range_editor_dispose;
  object_class->finalize = range_editor_finalize;

  widget_class->measure = range_editor_measure;
  widget_class->size_allocate = range_editor_size_allocate;

  properties[PROP_LOWER] =
    g_param_spec_float ("lower", NULL, NULL,
                        -G_MAXFLOAT, G_MAXFLOAT, 0,
                        G_PARAM_READWRITE | G_PARAM_STATIC_NAME | G_PARAM_EXPLICIT_NOTIFY);

  properties[PROP_UPPER] =
    g_param_spec_float ("upper", NULL, NULL,
                        -G_MAXFLOAT, G_MAXFLOAT, 0,
                        G_PARAM_READWRITE | G_PARAM_STATIC_NAME | G_PARAM_EXPLICIT_NOTIFY);

  properties[PROP_VALUE1] =
    g_param_spec_float ("value1", NULL, NULL,
                        -G_MAXFLOAT, G_MAXFLOAT, 0,
                        G_PARAM_READWRITE | G_PARAM_STATIC_NAME | G_PARAM_EXPLICIT_NOTIFY);

  properties[PROP_VALUE2] =
    g_param_spec_float ("value2", NULL, NULL,
                        -G_MAXFLOAT, G_MAXFLOAT, 0,
                        G_PARAM_READWRITE | G_PARAM_STATIC_NAME | G_PARAM_EXPLICIT_NOTIFY);

  g_object_class_install_properties (object_class, NUM_PROPERTIES, properties);

  gtk_widget_class_set_css_name (widget_class, "rangeeditor");
}

/* }}} */
/* {{{ Public API */

RangeEditor *
range_editor_new (void)
{
  return g_object_new (RANGE_EDITOR_TYPE, NULL);
}

void
range_editor_get_limits (RangeEditor *self,
                         float       *lower,
                         float       *upper)
{
  if (lower)
    *lower = self->lower;

  if (upper)
    *upper = self->upper;
}

void
range_editor_get_values (RangeEditor *self,
                         float       *value1,
                         float       *value2)
{
  if (value1)
    *value1 = self->value1;

  if (value2)
    *value2 = self->value2;
}

void
range_editor_configure (RangeEditor *self,
                        float        lower,
                        float        upper,
                        float        value1,
                        float        value2)
{
  g_return_if_fail (lower <= value1);
  g_return_if_fail (value1 <= value2);
  g_return_if_fail (value2 <= upper);

  g_object_freeze_notify (G_OBJECT (self));

  if (self->lower != lower)
    {
      self->lower = lower;
      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_LOWER]);
    }

  if (self->upper != upper)
    {
      self->upper = upper;
      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_UPPER]);
    }

  if (self->value1 != value1)
    {
      self->value1 = value1;
      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_VALUE1]);
    }

  if (self->value2 != value2)
    {
      self->value2 = value2;
      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_VALUE2]);
    }

  g_object_thaw_notify (G_OBJECT (self));

  gtk_widget_queue_resize (self->highlight);
  gtk_widget_queue_resize (self->slider1);
  gtk_widget_queue_resize (self->slider2);
}

/* }}} */

/* vim:set foldmethod=marker: */
