/*
 *  $Id: graph-area.c 29754 2026-04-09 09:35:42Z yeti-dn $
 *  Copyright (C) 2003-2024 David Necas (Yeti), Petr Klapetek.
 *  E-mail: yeti@gwyddion.net, klapetek@gwyddion.net.
 *
 *  This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public
 *  License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any
 *  later version.
 *
 *  This program 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 General Public License for more
 *  details.
 *
 *  You should have received a copy of the GNU General Public License along with this program; if not, write to the
 *  Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */

#include "config.h"
#include <string.h>
#include <gtk/gtk.h>

#include "libgwyddion/macros.h"
#include "libgwyddion/math.h"

#include "libgwyui/graph.h"
#include "libgwyui/types.h"
#include "libgwyui/graph-curve-dialog.h"
#include "libgwyui/graph-key-dialog.h"
#include "libgwyui/graph-utils.h"
#include "libgwyui/graph-internal.h"
#include "libgwyui/widget-impl-utils.h"

enum {
    SGNL_EDIT_CURVE,
    NUM_SIGNALS
};

enum {
    PROP_0,
    PROP_STATUS,
    NUM_PROPERTIES
};

struct _GwyGraphAreaPrivate {
    GwyGraphModel *graph_model;

    GdkWindow *event_window;
    const gchar *cursor_name;

    gulong curve_notify_id;
    gulong curve_data_changed_id;
    gulong model_notify_id;

    gint natural_width;
    gint natural_height;

    /* Selections. */
    GwyGraphStatusType status;
    GwySelection *pointsel;
    GwySelection *xsel_area;
    GwySelection *ysel_area;
    GwySelection *xsel_lines;
    GwySelection *ysel_lines;
    GwySelection *zoomsel;

    gulong pointsel_id;
    gulong xsel_area_id;
    gulong ysel_area_id;
    gulong xsel_lines_id;
    gulong ysel_lines_id;
    gulong zoomsel_id;

    /* Selection drawing. */
    gboolean selecting;
    gint selected_object_index;
    gint selected_border;

    gboolean enable_user_input;
    gint selection_limit;
    gboolean selection_is_editable;

    /* Grid lines. */
    GArray *x_grid_data;
    GArray *y_grid_data;

    /* Area boundaries, in real coodinates. */
    gdouble x_max;
    gdouble x_min;
    gdouble y_max;
    gdouble y_min;

    /* Dialogs. */
    GtkWidget *curve_dialog;
    GtkWidget *label_dialog;
    gboolean switching_curves;

    /* Label */
    GtkWidget *label;
    gboolean moving_label;
    gint xoff;
    gint yoff;
    gdouble rx0;
    gdouble ry0;
};

static void     finalize             (GObject *object);
static void     dispose              (GObject *object);
static void     set_property         (GObject *object,
                                      guint prop_id,
                                      const GValue *value,
                                      GParamSpec *pspec);
static void     get_property         (GObject *object,
                                      guint prop_id,
                                      GValue *value,
                                      GParamSpec *pspec);
static GType    child_type           (GtkContainer *container);
static void     destroy              (GtkWidget *widget);
static void     realize              (GtkWidget *widget);
static void     unrealize            (GtkWidget *widget);
static void     map                  (GtkWidget *widget);
static void     unmap                (GtkWidget *widget);
static void     get_preferred_width  (GtkWidget *widget,
                                      gint *minimum,
                                      gint *natural);
static void     get_preferred_height (GtkWidget *widget,
                                      gint *minimum,
                                      gint *natural);
static void     size_allocate        (GtkWidget *widget,
                                      GdkRectangle *allocation);
static gboolean draw                 (GtkWidget *widget,
                                      cairo_t *cr);
static gboolean button_pressed       (GtkWidget *widget,
                                      GdkEventButton *event);
static gboolean button_releaseed     (GtkWidget *widget,
                                      GdkEventButton *event);
static gboolean motion_notify        (GtkWidget *widget,
                                      GdkEventMotion *event);
static void     move_label           (GwyGraphArea *area,
                                      gint x,
                                      gint y);
static gint     find_curve           (GwyGraphArea *area,
                                      gdouble x,
                                      gdouble y);
static gint     find_selection_edge  (GwyGraphArea *area,
                                      gdouble x,
                                      gdouble y,
                                      int *eindex);
static gint     find_selection       (GwyGraphArea *area,
                                      gdouble x,
                                      gdouble y);
static gint     find_point           (GwyGraphArea *area,
                                      gdouble x,
                                      gdouble y);
static gint     find_line            (GwyGraphArea *area,
                                      gdouble position);
static void     selection_changed    (GwyGraphArea *area);
static void     model_notify         (GwyGraphArea *area,
                                      GParamSpec *pspec,
                                      GwyGraphModel *gmodel);
static void     restore_label_pos    (GwyGraphArea *area);
static void     n_curves_changed     (GwyGraphArea *area);
static void     curve_notify         (GwyGraphArea *area,
                                      gint i,
                                      GParamSpec *pspec);
static void     curve_data_changed   (GwyGraphArea *area,
                                      gint i);
static void     edit_curve_impl      (GwyGraphArea *area,
                                      gint i);
static void     curve_dialog_response(GwyGraphCurveDialog *dialog,
                                      gint response,
                                      GwyGraphArea *area);
static void     previous_curve       (GwyGraphCurveDialog *dialog,
                                      GwyGraphArea *area);
static void     next_curve           (GwyGraphCurveDialog *dialog,
                                      GwyGraphArea *area);
#if 0
static void     gwy_graph_key_response(GwyGraphKeyDialog *dialog,
                                         gint arg1,
                                         gpointer user_data);
static void          label_geometry_changed  (GwyGraphArea *area,
                                              const GdkRectangle *label_allocation);
#endif
static void          repos_label            (GwyGraphArea *area);
static GwySelection* make_selection         (GwyGraphArea *area,
                                             GType type,
                                             gulong *hid);
static GwySelection* make_oriented_selection(GwyGraphArea *area,
                                             GType type,
                                             GtkOrientation orientation,
                                             gulong *hid);
static void          fill_active_area_specs (GwyGraphArea *area,
                                             GwyGraphActiveAreaSpecs *specs);

static guint signals[NUM_SIGNALS];
static GParamSpec *properties[NUM_PROPERTIES] = { NULL, };
static GtkWidgetClass *parent_class = NULL;

G_DEFINE_TYPE_WITH_CODE(GwyGraphArea, gwy_graph_area, GTK_TYPE_BIN,
                        G_ADD_PRIVATE(GwyGraphArea))

static void
gwy_graph_area_class_init(GwyGraphAreaClass *klass)
{
    GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
    GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);
    GtkContainerClass *container_class = GTK_CONTAINER_CLASS(klass);
    GType type = G_TYPE_FROM_CLASS(klass);

    parent_class = gwy_graph_area_parent_class;

    gobject_class->dispose = dispose;
    gobject_class->finalize = finalize;
    gobject_class->get_property = get_property;
    gobject_class->set_property = set_property;

    widget_class->realize = realize;
    widget_class->unrealize = unrealize;
    widget_class->map = map;
    widget_class->unmap = unmap;
    widget_class->draw = draw;
    widget_class->destroy = destroy;
    widget_class->size_allocate = size_allocate;
    widget_class->get_preferred_width_for_height = NULL;
    widget_class->get_preferred_height_for_width = NULL;
    widget_class->get_preferred_width = get_preferred_width;
    widget_class->get_preferred_height = get_preferred_height;

    widget_class->button_press_event = button_pressed;
    widget_class->button_release_event = button_releaseed;
    widget_class->motion_notify_event = motion_notify;

    container_class->child_type = child_type;

    klass->edit_curve = edit_curve_impl;

    /**
     * GwyGraphArea::edit-curve:
     * @gwygraphcurvemodel: The #GwyGraphArea which received the signal.
     * @arg1: The index of the curve to edit.
     *
     * The ::data-changed signal is emitted when a curve properties are to be edited.
     **/
    signals[SGNL_EDIT_CURVE] = g_signal_new("edit-curve", type,
                                            G_SIGNAL_ACTION | G_SIGNAL_RUN_FIRST,
                                            G_STRUCT_OFFSET(GwyGraphAreaClass, edit_curve),
                                            NULL, NULL,
                                            g_cclosure_marshal_VOID__INT,
                                            G_TYPE_NONE, 1, G_TYPE_INT);
    g_signal_set_va_marshaller(signals[SGNL_EDIT_CURVE], type, g_cclosure_marshal_VOID__INTv);

    properties[PROP_STATUS] = g_param_spec_enum("status", NULL,
                                                "The type of reaction to mouse events (zoom, selections).",
                                                GWY_TYPE_GRAPH_STATUS_TYPE,
                                                GWY_GRAPH_STATUS_PLAIN,
                                                GWY_GPARAM_RWE);

    g_object_class_install_properties(gobject_class, NUM_PROPERTIES, properties);
}

static void
dispose(GObject *object)
{
    GwyGraphArea *area = GWY_GRAPH_AREA(object);
    GwyGraphAreaPrivate *priv = area->priv;

    gwy_graph_area_set_model(area, NULL);

    g_clear_signal_handler(&priv->pointsel_id, priv->pointsel);
    g_clear_signal_handler(&priv->xsel_area_id, priv->xsel_area);
    g_clear_signal_handler(&priv->ysel_area_id, priv->ysel_area);
    g_clear_signal_handler(&priv->xsel_lines_id, priv->xsel_lines);
    g_clear_signal_handler(&priv->ysel_lines_id, priv->ysel_lines);
    g_clear_signal_handler(&priv->zoomsel_id, priv->zoomsel);

    g_clear_object(&priv->pointsel);
    g_clear_object(&priv->xsel_area);
    g_clear_object(&priv->ysel_area);
    g_clear_object(&priv->xsel_lines);
    g_clear_object(&priv->ysel_lines);
    g_clear_object(&priv->zoomsel);

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

static void
finalize(GObject *object)
{
    GwyGraphArea *area = GWY_GRAPH_AREA(object);
    GwyGraphAreaPrivate *priv = area->priv;

    g_array_free(priv->x_grid_data, TRUE);
    g_array_free(priv->y_grid_data, TRUE);

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

static void
gwy_graph_area_init(GwyGraphArea *area)
{
    GwyGraphAreaPrivate *priv;

    area->priv = priv = gwy_graph_area_get_instance_private(area);

    priv->pointsel = make_selection(area, GWY_TYPE_SELECTION_POINT, &priv->pointsel_id);
    priv->xsel_area = make_oriented_selection(area, GWY_TYPE_SELECTION_RANGE, GWY_ORIENTATION_HORIZONTAL,
                                              &priv->xsel_area_id);
    priv->ysel_area = make_oriented_selection(area, GWY_TYPE_SELECTION_RANGE, GWY_ORIENTATION_VERTICAL,
                                              &priv->ysel_area_id);
    priv->xsel_lines = make_oriented_selection(area, GWY_TYPE_SELECTION_AXIS, GWY_ORIENTATION_HORIZONTAL,
                                               &priv->xsel_lines_id);
    priv->ysel_lines = make_oriented_selection(area, GWY_TYPE_SELECTION_AXIS, GWY_ORIENTATION_VERTICAL,
                                               &priv->ysel_lines_id);
    priv->zoomsel = make_selection(area, GWY_TYPE_SELECTION_RECTANGLE, &priv->zoomsel_id);

    priv->x_grid_data = g_array_new(FALSE, FALSE, sizeof(gdouble));
    priv->y_grid_data = g_array_new(FALSE, FALSE, sizeof(gdouble));

    priv->rx0 = 1.0;
    priv->ry0 = 0.0;

    priv->natural_height = priv->natural_width = -1;
    priv->enable_user_input = TRUE;
    priv->selection_is_editable = TRUE;

    priv->label = gwy_graph_key_new();
    gtk_container_add(GTK_CONTAINER(area), priv->label);
    gtk_widget_show(priv->label);
    // FIXME GTK3 We should distinguish
    // - spontaneous size change, initiated from the label and resulting in us combining the size (form label) and
    //   position type (from us) and allocating the new size
    // - our size change, which is handled exactly the same
    // - allocation, originating from us moving the label around, which is already done
    //
    //g_signal_connect_swapped(priv->label, "size-allocate", G_CALLBACK(label_geometry_changed), area);

    gtk_widget_set_has_window(GTK_WIDGET(area), FALSE);
}

static void
set_property(GObject *object,
             guint prop_id,
             const GValue *value,
             GParamSpec *pspec)
{
    GwyGraphArea *area = GWY_GRAPH_AREA(object);

    switch (prop_id) {
        case PROP_STATUS:
        gwy_graph_area_set_status(area, g_value_get_enum(value));
        break;

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

static void
get_property(GObject *object,
             guint prop_id,
             GValue *value,
             GParamSpec *pspec)
{
    GwyGraphArea *area = GWY_GRAPH_AREA(object);
    GwyGraphAreaPrivate *priv = area->priv;

    switch (prop_id) {
        case PROP_STATUS:
        g_value_set_enum(value, priv->status);
        break;

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

static GType
child_type(GtkContainer *container)
{
    return GWY_GRAPH_AREA(container)->priv->label ? GWY_TYPE_GRAPH_KEY : G_TYPE_NONE;
}

/**
 * gwy_graph_area_new:
 *
 * Creates a new graph area widget.
 *
 * Returns: Newly created graph area as #GtkWidget.
 **/
GtkWidget*
gwy_graph_area_new(void)
{
    return gtk_widget_new(GWY_TYPE_GRAPH_AREA, NULL);
}

/**
 * gwy_graph_area_set_model:
 * @area: A graph area.
 * @gmodel: New graph model.
 *
 * Sets the graph model of a graph area.
 **/
void
gwy_graph_area_set_model(GwyGraphArea *area,
                         GwyGraphModel *gmodel)
{
    g_return_if_fail(GWY_IS_GRAPH_AREA(area));
    g_return_if_fail(!gmodel || GWY_IS_GRAPH_MODEL(gmodel));

    GwyGraphAreaPrivate *priv = area->priv;
    if (!gwy_set_member_object(area, gmodel, GWY_TYPE_GRAPH_MODEL, &priv->graph_model,
                               "notify", G_CALLBACK(model_notify), &priv->model_notify_id, G_CONNECT_SWAPPED,
                               "curve-notify", G_CALLBACK(curve_notify), &priv->curve_notify_id, G_CONNECT_SWAPPED,
                               "curve-data-changed", G_CALLBACK(curve_data_changed),
                               &priv->curve_data_changed_id, G_CONNECT_SWAPPED,
                               NULL))
        return;

    gwy_graph_key_set_model(GWY_GRAPH_KEY(priv->label), gmodel);
    restore_label_pos(area);
}

/**
 * gwy_graph_area_get_model:
 * @area: A graph area.
 *
 * Gets the model of a graph area.
 *
 * Returns: The graph model this graph area widget displays.
 **/
GwyGraphModel*
gwy_graph_area_get_model(GwyGraphArea *area)
{
    g_return_val_if_fail(GWY_IS_GRAPH_AREA(area), NULL);
    return area->priv->graph_model;
}

static void
destroy(GtkWidget *widget)
{
    GwyGraphArea *area = GWY_GRAPH_AREA(widget);
    GwyGraphAreaPrivate *priv = area->priv;

    if (priv->curve_dialog) {
        gtk_widget_destroy(priv->curve_dialog);
        priv->curve_dialog = NULL;
    }
    if (priv->label_dialog) {
        gtk_widget_destroy(priv->label_dialog);
        priv->label_dialog = NULL;
    }

    parent_class->destroy(widget);
}

static void
get_preferred_width(GtkWidget *widget, gint *minimum, gint *natural)
{
    GwyGraphAreaPrivate *priv = GWY_GRAPH_AREA(widget)->priv;
    if (minimum)
        *minimum = 0;
    if (natural)
        *natural = (priv->natural_width >= 0 ? priv->natural_width : 420);
}

static void
get_preferred_height(GtkWidget *widget, gint *minimum, gint *natural)
{
    GwyGraphAreaPrivate *priv = GWY_GRAPH_AREA(widget)->priv;
    if (minimum)
        *minimum = 0;
    if (natural)
        *natural = (priv->natural_height >= 0 ? priv->natural_height : 320);
}

static void
size_allocate(GtkWidget *widget, GdkRectangle *allocation)
{
    GwyGraphArea *area = GWY_GRAPH_AREA(widget);
    GwyGraphAreaPrivate *priv = area->priv;

    gtk_widget_set_allocation(widget, allocation);

    gwy_debug("allocation %d×%d at (%d,%d)", allocation->width, allocation->height, allocation->x, allocation->y);

    if (priv->event_window)
        gdk_window_move_resize(priv->event_window, allocation->x, allocation->y, allocation->width, allocation->height);

    repos_label(area);
}

static void
realize(GtkWidget *widget)
{
    GwyGraphAreaPrivate *priv = GWY_GRAPH_AREA(widget)->priv;
    parent_class->realize(widget);
    priv->event_window = gwy_create_widget_input_window(widget,
                                                        GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK
                                                        | GDK_BUTTON_MOTION_MASK | GDK_POINTER_MOTION_MASK);
}

static void
unrealize(GtkWidget *widget)
{
    GwyGraphAreaPrivate *priv = GWY_GRAPH_AREA(widget)->priv;
    gwy_destroy_widget_input_window(widget, &priv->event_window);
    parent_class->unrealize(widget);
}

static void
map(GtkWidget *widget)
{
    GwyGraphAreaPrivate *priv = GWY_GRAPH_AREA(widget)->priv;

    parent_class->map(widget);
    if (priv->event_window)
        gdk_window_show(priv->event_window);
}

static void
unmap(GtkWidget *widget)
{
    GwyGraphAreaPrivate *priv = GWY_GRAPH_AREA(widget)->priv;

    priv->cursor_name = NULL;
    if (priv->event_window)
        gdk_window_hide(priv->event_window);
    parent_class->unmap(widget);
}

/* Here x and y are the position where the label is or would be moved to. */
static void
calculate_rxy0(GwyGraphArea *area, gdouble x, gdouble y)
{
    GwyGraphAreaPrivate *priv = area->priv;
    GdkRectangle allocation, label_alloc;
    gtk_widget_get_allocation(GTK_WIDGET(area), &allocation);
    gtk_widget_get_allocation(priv->label, &label_alloc);
    gint free_width = allocation.width - label_alloc.width;
    gint free_height = allocation.height - label_alloc.height;

    priv->rx0 = (free_width > 0) ? fmax(fmin(x, free_width), 0.0)/free_width : 0.5;
    priv->ry0 = (free_height > 0) ? fmax(fmin(y, free_height), 0.0)/free_height : 0.5;
    gwy_debug("x=%g  →  rx0=%g", x, priv->rx0);
    gwy_debug("y=%g  →  ry0=%g", y, priv->ry0);
}

static void
repos_label(GwyGraphArea *area)
{
    GwyGraphAreaPrivate *priv = area->priv;
    gint width, height;
    gtk_widget_get_preferred_width(priv->label, NULL, &width);
    gtk_widget_get_preferred_height(priv->label, NULL, &height);
    GdkRectangle allocation;
    gtk_widget_get_allocation(GTK_WIDGET(area), &allocation);

    gint posx = 0, free_width = allocation.width - width, wb = MIN(free_width/2, 2);
    if (free_width >= 0) {
        posx = GWY_ROUND(priv->rx0*free_width);
        posx = CLAMP(posx, wb, free_width - wb);
    }

    gint posy = 0, free_height = allocation.height - height, hb = MIN(free_height/2, 2);
    if (free_height >= 0) {
        posy = GWY_ROUND(priv->ry0*free_height);
        posy = CLAMP(posy, hb, free_height - hb);
    }
    gwy_debug("repos label at (%d, %d) from r0 (%g, %g)", posx, posy, priv->rx0, priv->ry0);

    move_label(area, posx, posy);
}

static gboolean
draw(GtkWidget *widget, cairo_t *cr)
{
    GwyGraphArea *area = GWY_GRAPH_AREA(widget);
    GwyGraphAreaPrivate *priv = area->priv;

    cairo_save(cr);
    cairo_set_source_rgb(cr, 1.0, 1.0, 1.0);
    cairo_paint(cr);
    cairo_restore(cr);

    GwyGraphStatusType status = priv->status;
    GwyGraphModel *model = priv->graph_model;
    GwyGraphActiveAreaSpecs specs;
    fill_active_area_specs(area, &specs);

    /* draw continuous selection */
    if (status == GWY_GRAPH_STATUS_XSEL || status == GWY_GRAPH_STATUS_YSEL)
        _gwy_graph_draw_selection_ranges(cr, &specs, GWY_SELECTION_RANGE(priv->xsel_area));

    _gwy_graph_draw_grid(cr, &specs,
                         &g_array_index(priv->x_grid_data, gdouble, 0), priv->x_grid_data->len,
                         &g_array_index(priv->y_grid_data, gdouble, 0), priv->y_grid_data->len);

    gint nc = gwy_graph_model_get_n_curves(model);
    for (gint i = 0; i < nc; i++)
        _gwy_graph_draw_curve(cr, &specs, gwy_graph_model_get_curve(model, i));

    if (status == GWY_GRAPH_STATUS_POINTS || status == GWY_GRAPH_STATUS_ZOOM)
        _gwy_graph_draw_selection_points(cr, &specs, GWY_SELECTION_POINT(priv->pointsel));
    else if (status == GWY_GRAPH_STATUS_XLINES || status == GWY_GRAPH_STATUS_YLINES)
        _gwy_graph_draw_selection_lines(cr, &specs, GWY_SELECTION_AXIS(priv->xsel_lines));

    cairo_save(cr);
    cairo_set_source_rgb(cr, 0.0, 0.0, 0.0);
    cairo_set_line_width(cr, 1.0);
    cairo_set_line_join(cr, CAIRO_LINE_JOIN_MITER);
    cairo_rectangle(cr, 0.5, 0.5, specs.area.width-1, specs.area.height-1);
    cairo_stroke(cr);
    cairo_restore(cr);

    if (priv->status == GWY_GRAPH_STATUS_ZOOM && priv->selecting != 0)
        _gwy_graph_draw_selection_areas(cr, &specs, GWY_SELECTION_RECTANGLE(priv->zoomsel));

    parent_class->draw(widget, cr);

    return FALSE;
}

/* FIXME: We do more or less exactly the same things in button-release and motion-notify, except in button-release
 * we also finish the selection. The button-press handler is more complicated because it needs to make some initial
 * decisions. */
static gboolean
button_pressed(GtkWidget *widget, GdkEventButton *event)
{
    GwyGraphArea *area = GWY_GRAPH_AREA(widget);
    GwyGraphAreaPrivate *priv = area->priv;
    GwyGraphStatusType status = priv->status;
    GwyGraphModel *gmodel = priv->graph_model;
    GwyGraphActiveAreaSpecs specs;
    fill_active_area_specs(area, &specs);
    gdouble x = event->x, y = event->y, rx = screen2real_x(&specs, x), ry = screen2real_y(&specs, y);
    gint idx = priv->selected_object_index;

    gwy_debug("event: %g %g", event->x, event->y);
    gboolean label_visible = FALSE;
    g_object_get(priv->graph_model, "label-visible", &label_visible, NULL);
    GdkRectangle allocation, label_alloc;
    gtk_widget_get_allocation(widget, &allocation);
    gtk_widget_get_allocation(priv->label, &label_alloc);
    if (label_visible
        && x >= label_alloc.x - allocation.x && x < label_alloc.x - allocation.x + label_alloc.width
        && y >= label_alloc.y - allocation.y && y < label_alloc.y - allocation.y + label_alloc.height) {
        if (event->type == GDK_2BUTTON_PRESS && priv->enable_user_input) {
            if (!priv->label_dialog)
                priv->label_dialog = _gwy_graph_key_dialog_new();
            _gwy_graph_key_dialog_set_model(GWY_GRAPH_KEY_DIALOG(priv->label_dialog), gmodel);
            gtk_widget_show_all(priv->label_dialog);
            gtk_window_present(GTK_WINDOW(priv->label_dialog));
            return TRUE;
        }
        priv->moving_label = TRUE;
        priv->xoff = GWY_ROUND(x) - (label_alloc.x - allocation.x);
        priv->yoff = GWY_ROUND(y) - (label_alloc.y - allocation.y);
        gwy_debug("started moving (offsets %d, %d)", priv->xoff, priv->yoff);
        return TRUE;
    }

    gint nc = gwy_graph_model_get_n_curves(gmodel);
    if (status == GWY_GRAPH_STATUS_PLAIN && nc > 0 && priv->enable_user_input) {
        gint curve = find_curve(area, rx, ry);
        if (curve >= 0) {
            gwy_graph_area_edit_curve(area, curve);
            return TRUE;
        }
    }

    if (status == GWY_GRAPH_STATUS_ZOOM) {
        gdouble rect[4] = { rx, ry, rx, ry };

        gwy_selection_clear(priv->zoomsel);
        gwy_selection_set_object(priv->zoomsel, 0, rect);
        priv->selecting = TRUE;
        return TRUE;
    }

    /* Everything below are selections. */
    if (!priv->selection_is_editable)
        return TRUE;

    if (status == GWY_GRAPH_STATUS_POINTS && gwy_selection_get_max_objects(priv->pointsel) == 1)
        gwy_selection_clear(priv->pointsel);

    if (status == GWY_GRAPH_STATUS_XLINES && gwy_selection_get_max_objects(priv->xsel_lines) == 1)
        gwy_selection_clear(priv->xsel_lines);

    if (status == GWY_GRAPH_STATUS_YLINES && gwy_selection_get_max_objects(priv->ysel_lines) == 1)
        gwy_selection_clear(priv->ysel_lines);

    if (status == GWY_GRAPH_STATUS_YSEL && gwy_selection_get_max_objects(priv->ysel_area) == 1)
        gwy_selection_clear(priv->ysel_area);

    if (status == GWY_GRAPH_STATUS_POINTS) {
        if (event->button == 1) {
            idx = find_point(area, rx, ry);

            if (!(gwy_selection_is_full(priv->pointsel) && idx == -1)) {
                gdouble point[2] = { rx, ry };
                priv->selecting = TRUE;
                gwy_selection_set_object(priv->pointsel, idx, point);
                if (idx == -1)
                    idx = gwy_selection_get_n_objects(priv->pointsel) - 1;
            }
        }
        else {
            gint i = find_point(area, rx, ry);
            if (i >= 0)
                gwy_selection_delete_object(priv->pointsel, i);
            gwy_selection_finished(priv->pointsel);
        }
    }

    if (status == GWY_GRAPH_STATUS_XSEL || status == GWY_GRAPH_STATUS_YSEL) {
        GwySelection *selection;
        gdouble coords[2];
        gdouble pos;

        if (status == GWY_GRAPH_STATUS_XSEL) {
            pos = rx;
            selection = priv->xsel_area;
        }
        else {
            pos = ry;
            selection = priv->ysel_area;
        }

        if (event->button == 1) {
            gint i = find_selection_edge(area, rx, ry, &priv->selected_border);
            idx = i;
            /* Allow to start a new selection without explicitly clearing the existing one when max_objects is 1 */
            if (gwy_selection_get_max_objects(selection) == 1 && i == -1)
                gwy_selection_clear(selection);

            if (i == -1 && !gwy_selection_is_full(selection)) {
                /* Add a new selection object */
                coords[0] = pos;
                coords[1] = pos;
                /* Start with the `other' border moving */
                priv->selected_border = 1;
                idx = gwy_selection_set_object(selection, -1, coords);
                priv->selecting = TRUE;
            }
            else if (idx != -1) {
                /* Move existing edge. FIXME: This is weird. Why do we assing anything before obtaining the data
                 * AND not doing anything with it? I tried to fix it to what it might was meant to do. */
                /*
                coords[priv->selected_border] = pos;
                gwy_selection_get_object(selection, i, coords);
                */
                gwy_selection_get_object(selection, i, coords);
                coords[priv->selected_border] = pos;
                gwy_selection_set_object(selection, i, coords);
                priv->selecting = TRUE;
            }
        }
        else {
            gint i = find_selection(area, rx, ry);
            /* Remove selection */
            if (i >= 0) {
                gwy_selection_delete_object(selection, i);
                gwy_selection_finished(selection);
            }
        }
    }

    if (status == GWY_GRAPH_STATUS_XLINES) {
        if (event->button == 1) {
            idx = find_line(area, rx);

            if (!(gwy_selection_is_full(priv->xsel_lines) && idx == -1)) {
                gwy_selection_set_object(priv->xsel_lines, idx, &rx);

                priv->selecting = TRUE;
                if (idx == -1)
                    idx = gwy_selection_get_n_objects(priv->xsel_lines) - 1;
            }
        }
        else {
            gint i = find_line(area, rx);
            if (i >= 0)
                gwy_selection_delete_object(priv->xsel_lines, i);
        }
    }

    if (status == GWY_GRAPH_STATUS_YLINES) {
        if (event->button == 1) {
            idx = find_line(area, ry);

            if (!(gwy_selection_is_full(priv->ysel_lines) && idx == -1)) {
                gwy_selection_set_object(priv->ysel_lines, idx, &ry);

                priv->selecting = TRUE;
                if (idx == -1)
                    idx = gwy_selection_get_n_objects(priv->ysel_lines) - 1;
            }
        }
        else {
            gint i = find_line(area, ry);
            if (i >= 0)
                gwy_selection_delete_object(priv->ysel_lines, i);
        }
    }
    priv->selected_object_index = idx;

    return TRUE;
}

static gboolean
button_releaseed(GtkWidget *widget, GdkEventButton *event)
{
    GwyGraphArea *area = GWY_GRAPH_AREA(widget);
    GwyGraphAreaPrivate *priv = area->priv;
    GwyGraphStatusType status = priv->status;
    GwyGraphActiveAreaSpecs specs;
    fill_active_area_specs(area, &specs);
    gdouble x = event->x, y = event->y, rx = screen2real_x(&specs, x), ry = screen2real_y(&specs, y);
    gint idx = priv->selected_object_index;

    if (status == GWY_GRAPH_STATUS_XSEL) {
        gdouble range[2];
        if (priv->selecting && gwy_selection_get_object(priv->xsel_area, idx, range)) {
            range[priv->selected_border] = rx;
            if (range[1] == range[0])
                gwy_selection_delete_object(priv->xsel_area, idx);
            else
                gwy_selection_set_object(priv->xsel_area, idx, range);
            priv->selecting = FALSE;
            gwy_selection_finished(priv->xsel_area);
        }
    }
    else if (status == GWY_GRAPH_STATUS_YSEL) {
        gdouble range[2];
        if (priv->selecting && gwy_selection_get_object(priv->ysel_area, idx, range)) {
            range[priv->selected_border] = ry;
            if (range[1] == range[0])
                gwy_selection_delete_object(priv->ysel_area, idx);
            else
                gwy_selection_set_object(priv->ysel_area, idx, range);
            priv->selecting = FALSE;
            gwy_selection_finished(priv->ysel_area);
        }
    }
    else if (status == GWY_GRAPH_STATUS_XLINES) {
        if (priv->selecting && gwy_selection_get_n_objects(priv->xsel_lines)) {
            priv->selecting = FALSE;
            gwy_selection_set_object(priv->xsel_lines, idx, &rx);
            gwy_selection_finished(priv->xsel_lines);
        }
    }
    else if (status == GWY_GRAPH_STATUS_YLINES) {
        if (priv->selecting && gwy_selection_get_n_objects(priv->ysel_lines)) {
            priv->selecting = FALSE;
            gwy_selection_set_object(priv->ysel_lines, idx, &ry);
            gwy_selection_finished(priv->ysel_lines);
        }
    }
    else if (status == GWY_GRAPH_STATUS_POINTS) {
        if (priv->selecting) {
            gdouble point[2] = { rx, ry };
            gwy_selection_set_object(priv->pointsel, idx, point);
            priv->selecting = FALSE;
            gwy_selection_finished(priv->pointsel);
        }
    }
    else if (status == GWY_GRAPH_STATUS_ZOOM) {
        gint nselected = gwy_selection_get_n_objects(priv->zoomsel);
        if (priv->selecting && nselected) {
            gdouble rect[4];
            gwy_selection_get_object(priv->zoomsel, nselected-1, rect);
            rect[2] = rx;
            rect[3] = ry;
            gwy_selection_set_object(priv->zoomsel, nselected-1, rect);

            priv->selecting = FALSE;
            gwy_selection_finished(priv->zoomsel);
        }
    }

    if (priv->moving_label) {
        gint xpix = GWY_ROUND(x), ypix = GWY_ROUND(y);
        move_label(area, xpix - priv->xoff, ypix - priv->yoff);
        calculate_rxy0(area, x - priv->xoff, y - priv->yoff);

        GwyGraphKeyPosition pos, newpos = GWY_GRAPH_KEY_USER;
        g_object_get(priv->graph_model, "label-position", &pos, NULL);
        if (priv->rx0 < 0.04 && priv->ry0 < 0.04)
            newpos = GWY_GRAPH_KEY_NORTHWEST;
        else if (priv->rx0 > 0.96 && priv->ry0 < 0.04)
            newpos = GWY_GRAPH_KEY_NORTHEAST;
        else if (priv->rx0 > 0.96 && priv->ry0 > 0.96)
            newpos = GWY_GRAPH_KEY_SOUTHEAST;
        else if (priv->rx0 < 0.04 && priv->ry0 > 0.96)
            newpos = GWY_GRAPH_KEY_SOUTHWEST;

        priv->moving_label = FALSE;
        if (newpos != pos || newpos == GWY_GRAPH_KEY_USER) {
            g_object_set(priv->graph_model,
                         "label-position", newpos,
                         "label-relative-x", priv->rx0,
                         "label-relative-y", priv->ry0,
                         NULL);
        }
    }

    return TRUE;
}

static gboolean
motion_notify(GtkWidget *widget, GdkEventMotion *event)
{
    GwyGraphArea *area = GWY_GRAPH_AREA(widget);
    GwyGraphAreaPrivate *priv = area->priv;
    GwyGraphStatusType status = priv->status;
    GwyGraphActiveAreaSpecs specs;
    fill_active_area_specs(area, &specs);
    gdouble x = event->x, y = event->y, rx = screen2real_x(&specs, x), ry = screen2real_y(&specs, y);
    GdkWindow *window = priv->event_window;
    gint idx = priv->selected_object_index;
    gboolean selecting = priv->selecting;

    if (status == GWY_GRAPH_STATUS_XSEL) {
        gwy_switch_window_cursor(window, &priv->cursor_name,
                                 selecting || find_selection_edge(area, rx, ry, NULL) >= 0 ? "ew-resize" : "crosshair");
        gdouble range[2];
        if (selecting && gwy_selection_get_object(priv->xsel_area, idx, range)) {
            range[priv->selected_border] = rx;
            gwy_selection_set_object(priv->xsel_area, idx, range);
        }
    }
    else if (status == GWY_GRAPH_STATUS_YSEL) {
        gwy_switch_window_cursor(window, &priv->cursor_name,
                                 selecting || find_selection_edge(area, rx, ry, NULL) >= 0 ? "ns-resize" : "crosshair");
        gdouble range[2];
        if (selecting && gwy_selection_get_object(priv->ysel_area, idx, range)) {
            range[priv->selected_border] = ry;
            gwy_selection_set_object(priv->ysel_area, idx, range);
        }
    }
    else if (status == GWY_GRAPH_STATUS_XLINES) {
        gwy_switch_window_cursor(window, &priv->cursor_name,
                                 selecting || find_line(area, rx) >= 0 ? "ew-resize" : "crosshair");
        if (selecting && gwy_selection_get_n_objects(priv->xsel_lines))
            gwy_selection_set_object(priv->xsel_lines, idx, &rx);
    }
    else if (status == GWY_GRAPH_STATUS_YLINES) {
        gwy_switch_window_cursor(window, &priv->cursor_name,
                                 selecting || find_line(area, ry) >= 0 ? "ns-resize" : "crosshair");
        if (selecting && gwy_selection_get_n_objects(priv->ysel_lines))
            gwy_selection_set_object(priv->ysel_lines, idx, &ry);
    }
    else if (status == GWY_GRAPH_STATUS_POINTS) {
        gwy_switch_window_cursor(window, &priv->cursor_name,
                                 selecting || find_point(area, rx, ry) != -1 ? "move" : "crosshair");
        if (selecting) {
            gdouble point[2] = { rx, ry };
            gwy_selection_set_object(priv->pointsel, idx, point);
        }
    }
    else if (status == GWY_GRAPH_STATUS_ZOOM) {
        gint nselected = gwy_selection_get_n_objects(priv->zoomsel);
        if (selecting && nselected) {
            gdouble rect[4];
            gwy_selection_get_object(priv->zoomsel, nselected-1, rect);
            rect[2] = rx;
            rect[3] = ry;
            gwy_selection_set_object(priv->zoomsel, nselected-1, rect);
        }
    }

    /* Label movement. */
    if (priv->moving_label)
        move_label(area, GWY_ROUND(x) - priv->xoff, GWY_ROUND(y) - priv->yoff);

    gdk_event_request_motions(event);
    return FALSE;
}

/* NB: The function consumes coordinates with origin at the area origin (which is what we get from the event window,
 * for instance). But the label must be positioned according to the parent's drawing window, so we must add our own
 * allocation origin. */
static void
move_label(GwyGraphArea *area, gint x, gint y)
{
    GwyGraphAreaPrivate *priv = area->priv;

    gwy_debug("moving label to %d, %d (inside)", x, y);
    GdkRectangle allocation, label_alloc;
    gtk_widget_get_allocation(GTK_WIDGET(area), &allocation);
    gtk_widget_get_preferred_width(priv->label, NULL, &label_alloc.width);
    gtk_widget_get_preferred_height(priv->label, NULL, &label_alloc.height);

    label_alloc.x = allocation.x + MAX(MIN(x, allocation.width - label_alloc.width), 0);
    label_alloc.width = MIN(label_alloc.width, allocation.width);

    label_alloc.y = allocation.y + MAX(MIN(y, allocation.height - label_alloc.height), 0);
    label_alloc.height = MIN(label_alloc.height, allocation.height);

    gtk_widget_size_allocate(GTK_WIDGET(priv->label), &label_alloc);
}

/* FIXME: This is wrong on several levels. First, we must measure screen distances (it is nonsensical in logscale).
 * Second, we must primarily measure distance from symbols (datapoints) and only secondarily from lines – when we draw
 * lines. We should also not produce NaN, do it more efficiently, etc. */
static gint
find_curve(GwyGraphArea *area, gdouble x, gdouble y)
{
    GwyGraphAreaPrivate *priv = area->priv;
    GwyGraphModel *gmodel = priv->graph_model;
    gint closestid = -1;
    gdouble closestdistance = G_MAXDOUBLE;

    gint nc = gwy_graph_model_get_n_curves(gmodel);
    for (gint i = 0; i < nc; i++) {
        GwyGraphCurveModel *curvemodel = gwy_graph_model_get_curve(gmodel, i);
        gint ndata = gwy_graph_curve_model_get_n_data(curvemodel);
        const gdouble *xdata = gwy_graph_curve_model_get_xdata(curvemodel);
        const gdouble *ydata = gwy_graph_curve_model_get_ydata(curvemodel);
        for (gint j = 0; j < ndata-1; j++) {
            if (xdata[j] <= x && xdata[j+1] >= x) {
                /* FIXME: This can give NaN if x coordinates coincide. */
                gdouble distance = fabs(y - ydata[j] + (x - xdata[j])*(ydata[j+1] - ydata[j])/(xdata[j+1] - xdata[j]));
                if (distance < closestdistance) {
                    closestdistance = distance;
                    closestid = i;
                }
                break;
            }
        }
    }
    if (fabs(closestdistance/(priv->y_max - priv->y_min)) < 0.05)
        return closestid;
    else
        return -1;
}

/* FIXME: This is needs to be done in screen coordinates! It is completely nonsensical in logscale. */
/**
 * gwy_graph_area_find_selection_edge:
 * @area: A graph area.
 * @x: Real x position.
 * @y: Real y position.
 * @eindex: Location store edge index (index of particular edge coordinate inside the selection object).
 *
 * Finds range selection object nearest to given coordinates.
 *
 * Returns: The index of selection object found, -1 when none is found.
 **/
static gint
find_selection_edge(GwyGraphArea *area, gdouble x, gdouble y, int *eindex)
{
    GwyGraphAreaPrivate *priv = area->priv;
    GwyGraphStatusType status = priv->status;
    GwySelection *selection;
    gdouble coords[2], dists[2];
    gdouble maxoff, min, pos;
    gint n, i, mi, ei;

    if (status == GWY_GRAPH_STATUS_XSEL || status == GWY_GRAPH_STATUS_YSEL) {
        /* FIXME: What is 50? */
        if (status == GWY_GRAPH_STATUS_XSEL) {
            pos = x;
            maxoff = (priv->x_max - priv->x_min)/50;
            selection = priv->xsel_area;
        }
        else {
            pos = y;
            maxoff = (priv->y_max - priv->y_min)/50;
            selection = priv->ysel_area;
        }

        mi = -1;
        ei = -1;
        min = G_MAXDOUBLE;
        n = gwy_selection_get_n_objects(selection);
        for (i = 0; i < n; i++) {
            gwy_selection_get_object(selection, i, coords);
            dists[0] = fabs(coords[0] - pos);
            dists[1] = fabs(coords[1] - pos);
            if (dists[1] <= dists[0]) {
                if (dists[1] < min) {
                    min = dists[1];
                    mi = i;
                    ei = 1;
                }
            }
            else {
                if (dists[0] < min) {
                    min = dists[0];
                    mi = i;
                    ei = 0;
                }
            }
        }

        if (min > maxoff)
            mi = -1;
        else if (eindex)
            *eindex = ei;
        return mi;
    }

    /* XXX: No other is implemented. The only other case which makes sense is a region-selection, but not zoom,
     * because the zoom rectangle always disapears and cannot be edited. */

    return -1;
}

/**
 * find_selection:
 * @area: A graph area.
 * @x: Real x position.
 * @y: Real y position.
 *
 * Finds range selection containing given coordinates.
 *
 * Returns: The index of selection object found, -1 when none is found.
 **/
static gint
find_selection(GwyGraphArea *area, gdouble x, gdouble y)
{
    GwyGraphAreaPrivate *priv = area->priv;
    GwyGraphStatusType status = priv->status;
    GwySelection *selection;
    gdouble pos;

    if (status == GWY_GRAPH_STATUS_XSEL || status == GWY_GRAPH_STATUS_YSEL) {
        if (status == GWY_GRAPH_STATUS_XSEL) {
            pos = x;
            selection = priv->xsel_area;
        }
        else {
            pos = y;
            selection = priv->ysel_area;
        }

        gint n = gwy_selection_get_n_objects(priv->pointsel);
        for (gint i = 0; i < n; i++) {
            gdouble range[2];
            gwy_selection_get_object(selection, i, range);
            if (pos >= MIN(range[0], range[1]) && pos <= MAX(range[0], range[1]))
                return i;
        }
        return -1;
    }

    return -1;
}

/* FIXME: This is bonkers. Not only it is borken in logscale, it does not even return the closest point!
 * Use something like gwy_math_find_nearest_point()! */
static gint
find_point(GwyGraphArea *area, gdouble x, gdouble y)
{
    GwyGraphAreaPrivate *priv = area->priv;
    gdouble xmin, ymin, xmax, ymax;

    /* FIXME: What is 50? */
    gdouble xoff = (priv->x_max - priv->x_min)/50;
    gdouble yoff = (priv->y_min - priv->y_max)/50;
    gint n = gwy_selection_get_n_objects(priv->pointsel);
    for (gint i = 0; i < n; i++) {
        gdouble point[2];
        gwy_selection_get_object(priv->pointsel, i, point);

        xmin = point[0] - xoff;
        xmax = point[0] + xoff;
        ymin = point[1] - yoff;
        ymax = point[1] + yoff;

        if (xmin <= x && xmax >= x && ymin <= y && ymax >= y)
            return i;
    }
    return -1;
}

/* FIXME: Ditto. */
static gint
find_line(GwyGraphArea *area, gdouble position)
{
    GwyGraphAreaPrivate *priv = area->priv;
    gdouble min = 0, max = 0, xoff, yoff, coord;
    guint n, i;

    if (priv->status == GWY_GRAPH_STATUS_XLINES) {
        /* FIXME: What is 100? */
        xoff = (priv->x_max - priv->x_min)/100;
        n = gwy_selection_get_n_objects(priv->xsel_lines);
        for (i = 0; i < n; i++) {
            gwy_selection_get_object(priv->xsel_lines, i, &coord);

            min = coord - xoff;
            max = coord + xoff;
            if (min <= position && max >= position)
                return i;
        }
    }
    else if (priv->status == GWY_GRAPH_STATUS_YLINES) {
        /* FIXME: What is 100? */
        yoff = (priv->y_max - priv->y_min)/100;
        n = gwy_selection_get_n_objects(priv->ysel_lines);
        for (i = 0; i < n; i++) {
            gwy_selection_get_object(priv->ysel_lines, i, &coord);

            min = coord - yoff;
            max = coord + yoff;
            if (min <= position && max >= position)
                return i;
        }
    }

    return -1;
}

static void
model_notify(GwyGraphArea *area,
             GParamSpec *pspec,
             G_GNUC_UNUSED GwyGraphModel *gmodel)
{
    if (gwy_stramong(pspec->name, "n-curves", "grid-type", NULL)) {
        GtkWidget *widget = GTK_WIDGET(area);
        if (gwy_strequal(pspec->name, "n-curves"))
            n_curves_changed(area);
        if (gtk_widget_is_drawable(widget))
            gtk_widget_queue_draw(widget);
        return;
    }

    if (gwy_stramong(pspec->name, "label-position", "label-relative-x", "label-relative-y", NULL)) {
        restore_label_pos(area);
        return;
    }
}

static void
restore_label_pos(GwyGraphArea *area)
{
    GwyGraphAreaPrivate *priv = area->priv;
    GwyGraphModel *gmodel = priv->graph_model;
    GwyGraphKeyPosition pos = GWY_GRAPH_KEY_NORTHWEST;

    if (gmodel)
        g_object_get(gmodel, "label-position", &pos, NULL);

    if (pos == GWY_GRAPH_KEY_NORTHWEST) {
        priv->rx0 = 0.0;
        priv->ry0 = 0.0;
    }
    else if (pos == GWY_GRAPH_KEY_NORTHEAST) {
        priv->rx0 = 1.0;
        priv->ry0 = 0.0;
    }
    else if (pos == GWY_GRAPH_KEY_SOUTHWEST) {
        priv->rx0 = 0.0;
        priv->ry0 = 1.0;
    }
    else if (pos == GWY_GRAPH_KEY_SOUTHEAST) {
        priv->rx0 = 1.0;
        priv->ry0 = 1.0;
    }
    else {
        if (gmodel)
            g_object_get(gmodel, "label-relative-x", &priv->rx0, "label-relative-y", &priv->ry0, NULL);
    }

    if (gtk_widget_is_drawable(GTK_WIDGET(area)))
        gtk_widget_queue_draw(GTK_WIDGET(area));
}

static void
n_curves_changed(GwyGraphArea *area)
{
    GwyGraphAreaPrivate *priv = area->priv;
    if (!priv->curve_dialog)
        return;

    GwyGraphCurveDialog *dialog = GWY_GRAPH_CURVE_DIALOG(priv->curve_dialog);
    GwyGraphCurveModel *cmodel = gwy_graph_curve_dialog_get_model(dialog);
    if (!gtk_widget_get_visible(GTK_WIDGET(dialog)) || !cmodel)
        return;
    gint n = gwy_graph_model_get_n_curves(priv->graph_model);
    gint i = gwy_graph_model_get_curve_index(priv->graph_model, cmodel);
    gwy_graph_curve_dialog_set_switching(dialog, i > 0, i < n-1);
    if (i == -1)
        gwy_graph_area_edit_curve(area, -1);
}

static void
curve_notify(GwyGraphArea *area,
             G_GNUC_UNUSED gint i,
             G_GNUC_UNUSED GParamSpec *pspec)
{
    if (gtk_widget_is_drawable(GTK_WIDGET(area)))
        gtk_widget_queue_draw(GTK_WIDGET(area));
}

static void
curve_data_changed(GwyGraphArea *area,
                   G_GNUC_UNUSED gint i)
{
    if (gtk_widget_is_drawable(GTK_WIDGET(area)))
        gtk_widget_queue_draw(GTK_WIDGET(area));
}

static void
selection_changed(GwyGraphArea *area)
{
    gtk_widget_queue_draw(GTK_WIDGET(area));
}

static void
curve_dialog_response(GwyGraphCurveDialog *dialog, gint response, G_GNUC_UNUSED GwyGraphArea *area)
{
    if (response == GTK_RESPONSE_CLOSE)
        gtk_widget_hide(GTK_WIDGET(dialog));
}

static void
previous_curve(GwyGraphCurveDialog *dialog, GwyGraphArea *area)
{
    GwyGraphAreaPrivate *priv = area->priv;
    GwyGraphCurveModel *cmodel = gwy_graph_curve_dialog_get_model(dialog);
    if (!priv->graph_model || !cmodel)
        return;

    gint i = gwy_graph_model_get_curve_index(priv->graph_model, cmodel);
    if (i > 0) {
        priv->switching_curves = TRUE;
        gwy_graph_area_edit_curve(area, i-1);
    }
}

static void
next_curve(GwyGraphCurveDialog *dialog, GwyGraphArea *area)
{
    GwyGraphAreaPrivate *priv = area->priv;
    GwyGraphCurveModel *cmodel = gwy_graph_curve_dialog_get_model(dialog);
    if (!priv->graph_model || !cmodel)
        return;

    gint n = gwy_graph_model_get_n_curves(priv->graph_model);
    gint i = gwy_graph_model_get_curve_index(priv->graph_model, cmodel);
    if (i+1 < n) {
        priv->switching_curves = TRUE;
        gwy_graph_area_edit_curve(area, i+1);
    }
}

#if 0
static void
label_geometry_changed(GwyGraphArea *area)
{
    if (!area->priv->moving_label)
        repos_label(area);
}
#endif

/**
 * gwy_graph_area_enable_user_input:
 * @area: A graph area.
 * @enable: %TRUE to enable user interaction, %FALSE to disable it.
 *
 * Enables/disables auxiliary graph area dialogs (invoked by clicking the
 * mouse).
 *
 * Note, however, that this setting does not control editability of selections.
 * Use gwy_graph_area_set_selection_editable() for that.
 **/
void
gwy_graph_area_enable_user_input(GwyGraphArea *area, gboolean enable)
{
    g_return_if_fail(GWY_IS_GRAPH_AREA(area));

    GwyGraphAreaPrivate *priv = area->priv;
    if (!enable == !priv->enable_user_input)
        return;

    priv->enable_user_input = !!enable;
    if (!enable) {
        if (priv->curve_dialog)
            gtk_widget_hide(priv->curve_dialog);
        if (priv->label_dialog)
            gtk_widget_hide(priv->label_dialog);
    }
    gwy_graph_key_enable_user_input(GWY_GRAPH_KEY(priv->label), enable);
}

/**
 * gwy_graph_area_set_selection_editable:
 * @area: A graph area.
 * @setting: %TRUE to enable selection editing, %FALSE to disable it.
 *
 * Enables/disables selection editing using mouse.
 *
 * When selection editing is disabled the graph area status type determines the selection type that can be drawn on
 * the area.  However, the user cannot modify it.
 **/
void
gwy_graph_area_set_selection_editable(GwyGraphArea *area, gboolean setting)
{
    g_return_if_fail(GWY_IS_GRAPH_AREA(area));

    GwyGraphAreaPrivate *priv = area->priv;
    if (!setting == !priv->selection_is_editable)
        return;

    if (!setting && priv->selecting) {
        /* FIXME stop selecting */
        g_warning("Setting selection editability while selecting is not handled.");
        priv->selecting = FALSE;
    }
    priv->selection_is_editable = !!setting;
}

/**
 * gwy_graph_area_set_x_range:
 * @area: A graph area.
 * @x_min: The minimum x value, in real coodrinates.
 * @x_max: The maximum x value, in real coodrinates.
 *
 * Sets the horizontal range a graph area displays.
 **/
void
gwy_graph_area_set_x_range(GwyGraphArea *area,
                           gdouble x_min,
                           gdouble x_max)
{
    g_return_if_fail(GWY_IS_GRAPH_AREA(area));

    gwy_debug("%p: %g, %g", area, x_min, x_max);
    GwyGraphAreaPrivate *priv = area->priv;
    if (x_min != priv->x_min || x_max != priv->x_max) {
        priv->x_min = x_min;
        priv->x_max = x_max;
        if (gtk_widget_is_drawable(GTK_WIDGET(area)))
            gtk_widget_queue_draw(GTK_WIDGET(area));
    }
}

/**
 * gwy_graph_area_set_y_range:
 * @area: A graph area.
 * @y_min: The minimum y value, in real coodrinates.
 * @y_max: The maximum y value, in real coodrinates.
 *
 * Sets the vertical range a graph area displays.
 **/
void
gwy_graph_area_set_y_range(GwyGraphArea *area,
                           gdouble y_min,
                           gdouble y_max)
{
    g_return_if_fail(GWY_IS_GRAPH_AREA(area));

    gwy_debug("%p: %g, %g", area, y_min, y_max);
    GwyGraphAreaPrivate *priv = area->priv;
    if (y_min != priv->y_min || y_max != priv->y_max) {
        priv->y_min = y_min;
        priv->y_max = y_max;
        if (gtk_widget_is_drawable(GTK_WIDGET(area)))
            gtk_widget_queue_draw(GTK_WIDGET(area));
    }
}

/**
 * gwy_graph_area_set_x_grid_data:
 * @area: A graph area.
 * @ndata: The number of points in @grid_data.
 * @grid_data: Array of grid line positions on the x-axis (in real values, not pixels).
 *
 * Sets the grid data on the x-axis of a graph area
 **/
void
gwy_graph_area_set_x_grid_data(GwyGraphArea *area,
                               guint ndata,
                               const gdouble *grid_data)
{
    g_return_if_fail(GWY_IS_GRAPH_AREA(area));

    GwyGraphAreaPrivate *priv = area->priv;
    g_array_set_size(priv->x_grid_data, 0);
    g_array_append_vals(priv->x_grid_data, grid_data, ndata);

    if (gtk_widget_is_drawable(GTK_WIDGET(area)))
        gtk_widget_queue_draw(GTK_WIDGET(area));
}

/**
 * gwy_graph_area_set_y_grid_data:
 * @area: A graph area.
 * @ndata: The number of points in @grid_data.
 * @grid_data: Array of grid line positions on the y-axis (in real values, not pixels).
 *
 * Sets the grid data on the y-axis of a graph area
 **/
void
gwy_graph_area_set_y_grid_data(GwyGraphArea *area,
                               guint ndata,
                               const gdouble *grid_data)
{
    g_return_if_fail(GWY_IS_GRAPH_AREA(area));

    GwyGraphAreaPrivate *priv = area->priv;
    g_array_set_size(priv->y_grid_data, 0);
    g_array_append_vals(priv->y_grid_data, grid_data, ndata);

    if (gtk_widget_is_drawable(GTK_WIDGET(area)))
        gtk_widget_queue_draw(GTK_WIDGET(area));
}

/**
 * gwy_graph_area_get_x_grid_data:
 * @area: A graph area.
 * @ndata: Location to store the number of returned positions.
 *
 * Gets the grid data on the x-axis of a graph area.
 *
 * Returns: Array of grid line positions (in real values, not pixels) owned by the graph area.
 **/
const gdouble*
gwy_graph_area_get_x_grid_data(GwyGraphArea *area,
                               guint *ndata)
{
    g_return_val_if_fail(GWY_IS_GRAPH_AREA(area), NULL);

    GwyGraphAreaPrivate *priv = area->priv;
    if (ndata)
        *ndata = priv->x_grid_data->len;
    return (const gdouble*)priv->x_grid_data->data;
}

/**
 * gwy_graph_area_get_y_grid_data:
 * @area: A graph area.
 * @ndata: Location to store the number of returned positions.
 *
 * Gets the grid data on the y-axis of a graph area.
 *
 * Returns: Array of grid line positions (in real values, not pixels) owned by the graph area.
 **/
const gdouble*
gwy_graph_area_get_y_grid_data(GwyGraphArea *area,
                               guint *ndata)
{
    g_return_val_if_fail(GWY_IS_GRAPH_AREA(area), NULL);

    GwyGraphAreaPrivate *priv = area->priv;
    if (ndata)
        *ndata = priv->y_grid_data->len;
    return (const gdouble*)priv->y_grid_data->data;
}

/**
 * gwy_graph_area_coords_screen_to_real:
 * @area: A graph area.
 * @xscr: Widget x-coordinate.
 * @yscr: Widget y-coordinate.
 * @x: Location to store data x-coordinate (or %NULL).
 * @y: Location to store data y-coordinate (or %NULL).
 *
 * Converts graph area widget coordinates to data coordinates.
 **/
void
gwy_graph_area_coords_widget_to_real(GwyGraphArea *area,
                                     gdouble xscr,
                                     gdouble yscr,
                                     gdouble *x,
                                     gdouble *y)
{
    g_return_if_fail(GWY_IS_GRAPH_AREA(area));

    GwyGraphActiveAreaSpecs specs;
    fill_active_area_specs(area, &specs);
    if (x)
        *x = screen2real_x(&specs, xscr);
    if (y)
        *y = screen2real_y(&specs, yscr);
}

/**
 * gwy_graph_area_get_selection:
 * @area: A graph area.
 * @status: Graph status. Passing %GWY_GRAPH_STATUS_PLAIN mode (which has no selection associated) can be used to
 *          obtain the selection for the current mode.
 *
 * Gets the selection object corresponding to a status of a graph area.
 *
 * A selection object exists even for inactive status types (selection modes), therefore also selections for other
 * modes than the currently active one can be requested.
 *
 * The selection type is #GwySelectionAxis for point-wise selections along axes %GWY_GRAPH_STATUS_XLINES and
 * %GWY_GRAPH_STATUS_YLINES.
 *
 * The selection type is #GwySelectionRange for range selections along axes %GWY_GRAPH_STATUS_XSEL and
 * %GWY_GRAPH_STATUS_YSEL.
 *
 * The selection type is #GwySelectionPoint for the point-wise selection %GWY_GRAPH_STATUS_POINTS.
 *
 * The selection type is #GwySelectionRectangle for the zoom mode %GWY_GRAPH_STATUS_ZOOM (which is generally handled
 * internally by the graph).
 *
 * Returns: The requested selection.  It is %NULL only if @status is %GWY_GRAPH_STATUS_PLAIN and the current
 *          selection mode is %GWY_GRAPH_STATUS_PLAIN.
 **/
GwySelection*
gwy_graph_area_get_selection(GwyGraphArea *area,
                             GwyGraphStatusType status)
{
    g_return_val_if_fail(GWY_IS_GRAPH_AREA(area), NULL);

    GwyGraphAreaPrivate *priv = area->priv;
    if (status == GWY_GRAPH_STATUS_PLAIN)
        status = priv->status;

    if (status == GWY_GRAPH_STATUS_PLAIN)
        return NULL;
    if (status == GWY_GRAPH_STATUS_XSEL)
        return priv->xsel_area;
    if (status == GWY_GRAPH_STATUS_YSEL)
        return priv->ysel_area;
    if (status == GWY_GRAPH_STATUS_POINTS)
        return priv->pointsel;
    if (status == GWY_GRAPH_STATUS_ZOOM)
        return priv->zoomsel;
    if (status == GWY_GRAPH_STATUS_XLINES)
        return priv->xsel_lines;
    if (status == GWY_GRAPH_STATUS_YLINES)
        return priv->ysel_lines;

    g_return_val_if_reached(NULL);
}

/**
 * gwy_graph_area_set_status:
 * @area: A graph area.
 * @status: New graph area status.
 *
 * Sets the status of a graph area.
 *
 * When the area is inside a #GwyGraph, use gwy_graph_set_status() instead
 * (also see this function for details).
 **/
void
gwy_graph_area_set_status(GwyGraphArea *area, GwyGraphStatusType status)
{
    g_return_if_fail(GWY_IS_GRAPH_AREA(area));
    g_return_if_fail(status <= GWY_GRAPH_STATUS_ZOOM);

    GwyGraphAreaPrivate *priv = area->priv;
    if (status == priv->status)
        return;

    priv->status = status;
    if (gtk_widget_is_drawable(GTK_WIDGET(area)))
        gtk_widget_queue_draw(GTK_WIDGET(area));
    g_object_notify_by_pspec(G_OBJECT(area), properties[PROP_STATUS]);
}

/**
 * gwy_graph_area_get_status:
 * @area: A graph area.
 *
 * Gets the status of a grap area.
 *
 * See gwy_graph_area_set_status().
 *
 * Returns: The current graph area status.
 **/
GwyGraphStatusType
gwy_graph_area_get_status(GwyGraphArea *area)
{
    g_return_val_if_fail(GWY_IS_GRAPH_AREA(area), 0);

    return area->priv->status;
}

static void
edit_curve_impl(GwyGraphArea *area, gint i)
{
    GwyGraphAreaPrivate *priv = area->priv;

    if (i < 0) {
        if (priv->curve_dialog)
            gtk_widget_hide(priv->curve_dialog);
        return;
    }

    g_return_if_fail(priv->graph_model);
    if (!priv->curve_dialog) {
        priv->curve_dialog = gwy_graph_curve_dialog_new();
        g_signal_connect(priv->curve_dialog, "response", G_CALLBACK(curve_dialog_response), area);
        g_signal_connect(priv->curve_dialog, "previous", G_CALLBACK(previous_curve), area);
        g_signal_connect(priv->curve_dialog, "next", G_CALLBACK(next_curve), area);
    }
    gint n = gwy_graph_model_get_n_curves(priv->graph_model);
    GwyGraphCurveModel *cmodel = gwy_graph_model_get_curve(priv->graph_model, i);
    g_return_if_fail(cmodel);
    gwy_graph_curve_dialog_set_model(GWY_GRAPH_CURVE_DIALOG(priv->curve_dialog), cmodel);
    gwy_graph_curve_dialog_set_switching(GWY_GRAPH_CURVE_DIALOG(priv->curve_dialog), i > 0, i < n-1);
    if (!priv->switching_curves) {
        gtk_widget_show_all(priv->curve_dialog);
        gtk_window_present(GTK_WINDOW(priv->curve_dialog));
    }
    priv->switching_curves = FALSE;
}

/**
 * gwy_graph_area_edit_curve:
 * @area: A graph area.
 * @id: The index of the curve to edit properties of.
 *
 * Invokes the curve property dialog for a curve plotted in a graph area.
 *
 * If the dialog is already displayed, it is switched to the requested curve.
 **/
void
gwy_graph_area_edit_curve(GwyGraphArea *area,
                          gint id)
{
    g_return_if_fail(GWY_IS_GRAPH_AREA(area));
    g_signal_emit(area, signals[SGNL_EDIT_CURVE], 0, id);
}

/**
 * gwy_graph_area_set_natural_size:
 * @area: A graph area.
 * @width: Natural width the graph area should prefer, or a negative value to use the default.
 * @height: Natural height the area graph should prefer, or a negative value to use the default.
 *
 * Sets the preferred natural dimensions of a graph area.
 *
 * Graphs can be very small, but have relatively large default (natural) dimensions. Since GTK+ tries to make windows
 * large enough for the natural widget sizes, it does not work well when they are used as small preview plots in
 * dialogs. This function allows setting the natural size arbitrarily.
 *
 * The minimum size is not changed (and is very small by default). Use gtk_widget_set_size_request() to set a minimum
 * size.
 **/
void
gwy_graph_area_set_natural_size(GwyGraphArea *area,
                                gint width,
                                gint height)
{
    g_return_if_fail(GWY_IS_GRAPH_AREA(area));
    GwyGraphAreaPrivate *priv = area->priv;

    /* Canonicalise negative values. */
    width = MAX(width, -1);
    height = MAX(height, -1);
    if (width == priv->natural_width && height == priv->natural_height)
        return;

    priv->natural_width = width;
    priv->natural_height = height;
    gtk_widget_queue_resize(GTK_WIDGET(area));
}

static GwySelection*
make_selection(GwyGraphArea *area, GType type, gulong *hid)
{
    GwySelection *selection;

    selection = GWY_SELECTION(g_object_new(type, NULL));
    gwy_selection_set_max_objects(selection, 1);
    *hid = g_signal_connect_swapped(selection, "changed", G_CALLBACK(selection_changed), area);

    return selection;
}

static GwySelection*
make_oriented_selection(GwyGraphArea *area, GType type, GtkOrientation orientation,
                        gulong *hid)
{
    GwySelection *selection;

    selection = GWY_SELECTION(g_object_new(type, NULL));
    gwy_selection_set_max_objects(selection, 1);
    g_object_set(selection, "orientation", orientation, NULL);
    *hid = g_signal_connect_swapped(selection, "changed", G_CALLBACK(selection_changed), area);

    return selection;
}

/* FIXME GTK3 there is a lot of duplicate information about the widget are being shuffled around. But we are probably
 * going to get rid of gwygraphbasics and make it internal – and then will be the time to refactor it. */
static void
fill_active_area_specs(GwyGraphArea *area, GwyGraphActiveAreaSpecs *specs)
{
    GwyGraphAreaPrivate *priv = area->priv;

    /* XXX: This relies on GwyGraph setting the ranges because otherwise the real ranges remain unset. */
    specs->real_xmin = priv->x_min;
    specs->real_ymin = priv->y_min;
    specs->real_width = priv->x_max - priv->x_min;
    specs->real_height = priv->y_max - priv->y_min;
    gtk_widget_get_allocation(GTK_WIDGET(area), &specs->area);
    /* The drawing coordinates begin at (0, 0). */
    specs->area.x = specs->area.y = 0;
    g_object_get(priv->graph_model,
                 "x-logarithmic", &specs->log_x,
                 "y-logarithmic", &specs->log_y,
                 NULL);
}

/**
 * SECTION: graph-area
 * @title: GwyGraphArea
 * @short_description: Layout for drawing graph curves
 *
 * #GwyGraphArea is the central part of #GwyGraph widget. It plots a set of data curves with the given plot
 * properties.
 *
 * It is recommended to use it within #GwyGraph. However, it can also be used separately.
 **/

/* vim: set cin columns=120 tw=118 et ts=4 sw=4 cino=>1s,e0,n0,f0,{0,}0,^0,\:1s,=0,g1s,h0,t0,+1s,c3,(0,u0 : */
