/*
 *  $Id: color-axis.c 29742 2026-04-06 09:18:09Z 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 <gtk/gtk.h>

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

#include "libgwyui/color-axis.h"
#include "libgwyui/types.h"
#include "libgwyui/cairo-utils.h"
#include "libgwyui/widget-impl-utils.h"

enum {
    PROP_0,
    PROP_ORIENTATION,
    PROP_TICKS_STYLE,
    PROP_UNIT,
    PROP_GRADIENT,
    PROP_LABELS_VISIBLE,
    NUM_PROPERTIES
};

enum { MIN_TICK_DISTANCE = 30 };

struct _GwyColorAxisPrivate {
    GdkWindow *event_window;

    GtkOrientation orientation;
    GwyTicksStyle ticks_style;
    gboolean labels_visible;

    GwyGradient *gradient;
    gulong gradient_changed_id;
    gulong default_gradient_changed_id;
    cairo_pattern_t *gradient_pattern;

    gint stripe_width;

    gdouble min;
    gdouble max;

    GString *label_text;
    GwyUnit *unit;
    gulong unit_changed_id;

    gint tick_length;
    gint labelb_size;
    gint labele_size;

    gboolean inverted;

    GwyColorAxisMapFunc map_ticks;
    gpointer map_ticks_data;
};

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 void     dispose                 (GObject *object);
static void     finalize                (GObject *object);
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 void     draw_labels             (GwyColorAxis *axis,
                                         cairo_t *cr,
                                         gint prec);
static gint     step_to_prec            (gdouble d);
static void     update                  (GwyColorAxis *axis);
static void     unit_changed            (GwyColorAxis *axis);
static void     gradient_changed        (GwyColorAxis *axis);
static void     follow_default_gradient (GwyColorAxis *axis,
                                         gboolean follow);
static void     default_gradient_changed(GwyColorAxis *axis);

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

G_DEFINE_TYPE_WITH_CODE(GwyColorAxis, gwy_color_axis, GTK_TYPE_WIDGET,
                        G_ADD_PRIVATE(GwyColorAxis))

static void
gwy_color_axis_class_init(GwyColorAxisClass *klass)
{
    GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
    GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);

    parent_class = gwy_color_axis_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->get_preferred_width = get_preferred_width;
    widget_class->get_preferred_height = get_preferred_height;
    widget_class->size_allocate = size_allocate;

    properties[PROP_ORIENTATION] = g_param_spec_enum("orientation", NULL,
                                                     "Axis orientation",
                                                     GTK_TYPE_ORIENTATION,
                                                     GTK_ORIENTATION_VERTICAL,
                                                     G_PARAM_CONSTRUCT_ONLY
                                                     | G_PARAM_READWRITE
                                                     | G_PARAM_STATIC_STRINGS);
    properties[PROP_TICKS_STYLE] = g_param_spec_enum("ticks-style", NULL,
                                                     "The style of axis ticks",
                                                     GWY_TYPE_TICKS_STYLE,
                                                     GWY_TICKS_STYLE_AUTO,
                                                     GWY_GPARAM_RWE);
    /**
     * GwyColorAxis:unit
     *
     * The unit of axis values.
     *
     * Units are set by value. The unit object does not change. Setting the property is the same as getting the unit
     * object and using gwy_unit_assing() to modify it.
     **/
    properties[PROP_UNIT] = g_param_spec_object("unit", NULL,
                                                "Unit to display in labels",
                                                GWY_TYPE_UNIT,
                                                GWY_GPARAM_RWE);
    properties[PROP_GRADIENT] = g_param_spec_object("gradient", NULL,
                                                    "Color gradient the axis displays",
                                                    GWY_TYPE_GRADIENT,
                                                    GWY_GPARAM_RWE);
    properties[PROP_LABELS_VISIBLE] = g_param_spec_boolean("labels-visible", NULL,
                                                           "Whether minimum and maximum labels are visible",
                                                           TRUE,
                                                           GWY_GPARAM_RWE);

    g_object_class_install_properties(gobject_class, NUM_PROPERTIES, properties);
}

static void
gwy_color_axis_init(GwyColorAxis *axis)
{
    GwyColorAxisPrivate *priv;

    axis->priv = priv = gwy_color_axis_get_instance_private(axis);

    priv->orientation = GTK_ORIENTATION_VERTICAL;
    priv->tick_length = 6;
    priv->stripe_width = 10;
    priv->labels_visible = TRUE;
    priv->ticks_style = GWY_TICKS_STYLE_AUTO;

    priv->min = 0.0;
    priv->max = 1.0;
    priv->inverted = FALSE;

    priv->unit = gwy_unit_new(NULL);
    priv->unit_changed_id = g_signal_connect_swapped(priv->unit, "value-changed", G_CALLBACK(unit_changed), axis);
    gwy_color_axis_set_gradient(axis, NULL);

    gtk_widget_set_has_window(GTK_WIDGET(axis), FALSE);
}

static void
set_property(GObject *object,
             guint prop_id,
             const GValue *value,
             GParamSpec *pspec)
{
    GwyColorAxis *axis = GWY_COLOR_AXIS(object);
    GwyColorAxisPrivate *priv = axis->priv;
    GwyUnit *unit;
    gboolean changed;

    switch (prop_id) {
        case PROP_ORIENTATION:
        /* Constr-only */
        axis->priv->orientation = g_value_get_enum(value);
        break;

        case PROP_TICKS_STYLE:
        gwy_color_axis_set_ticks_style(axis, g_value_get_enum(value));
        break;

        case PROP_UNIT:
        unit = g_value_get_object(value);
        if ((changed = !gwy_unit_equal(priv->unit, unit))) {
            gwy_unit_assign(priv->unit, unit);
            g_object_notify_by_pspec(object, properties[PROP_UNIT]);
        }
        break;

        case PROP_GRADIENT:
        gwy_color_axis_set_gradient(axis, g_value_get_object(value));
        break;

        case PROP_LABELS_VISIBLE:
        gwy_color_axis_set_labels_visible(axis, g_value_get_boolean(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)
{
    GwyColorAxis *axis = GWY_COLOR_AXIS(object);
    GwyColorAxisPrivate *priv = axis->priv;

    switch (prop_id) {
        case PROP_ORIENTATION:
        g_value_set_enum(value, priv->orientation);
        break;

        case PROP_TICKS_STYLE:
        g_value_set_enum(value, priv->ticks_style);
        break;

        case PROP_UNIT:
        g_value_set_object(value, priv->unit);
        break;

        case PROP_GRADIENT:
        g_value_set_object(value, gwy_color_axis_get_gradient(axis));
        break;

        case PROP_LABELS_VISIBLE:
        g_value_set_boolean(value, priv->labels_visible);
        break;

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

/**
 * gwy_color_axis_new_with_range:
 * @orientation: The orientation of the axis.
 * @min: The minimum.
 * @max: The maximum.
 *
 * Creates a new color axis.
 *
 * Returns: The newly created color axis as a #GtkWidget.
 **/
GtkWidget*
gwy_color_axis_new_with_range(GtkOrientation orientation,
                              gdouble min,
                              gdouble max)
{
    GtkWidget *widget = gwy_color_axis_new(orientation);
    GwyColorAxis *axis = GWY_COLOR_AXIS(widget);
    GwyColorAxisPrivate *priv = axis->priv;

    if (max < min)
        priv->inverted = TRUE;
    priv->min = MIN(min, max);
    priv->max = MAX(min, max);

    return widget;
}

/**
 * gwy_color_axis_new:
 * @orientation: The orientation of the axis.
 *
 * Creates a new color axis.
 *
 * Returns: The newly created color axis as a #GtkWidget.
 **/
GtkWidget*
gwy_color_axis_new(GtkOrientation orientation)
{
    return g_object_new(GWY_TYPE_COLOR_AXIS, "orientation", orientation, NULL);
}

static void
dispose(GObject *object)
{
    GwyColorAxis *axis = GWY_COLOR_AXIS(object);
    GwyColorAxisPrivate *priv = axis->priv;

    follow_default_gradient(axis, FALSE);
    g_clear_signal_handler(&priv->gradient_changed_id, priv->gradient);
    g_clear_signal_handler(&priv->unit_changed_id, priv->unit);

    g_clear_pointer(&priv->gradient_pattern, cairo_pattern_destroy);

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

static void
finalize(GObject *object)
{
    GwyColorAxis *axis = GWY_COLOR_AXIS(object);
    GwyColorAxisPrivate *priv = axis->priv;

    g_clear_object(&priv->gradient);
    g_clear_object(&priv->unit);

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

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

static void
realize(GtkWidget *widget)
{
    GwyColorAxisPrivate *priv = GWY_COLOR_AXIS(widget)->priv;

    parent_class->realize(widget);
    priv->event_window = gwy_create_widget_input_window(widget,
                                                        GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK);
    update(GWY_COLOR_AXIS(widget));
}

static void
map(GtkWidget *widget)
{
    GwyColorAxisPrivate *priv = GWY_COLOR_AXIS(widget)->priv;
    parent_class->map(widget);
    gdk_window_show(priv->event_window);
}

static void
unmap(GtkWidget *widget)
{
    GwyColorAxisPrivate *priv = GWY_COLOR_AXIS(widget)->priv;
    gdk_window_hide(priv->event_window);
    parent_class->unmap(widget);
}

/* FIXME */
static void
get_preferred_width(GtkWidget *widget,
                    gint *minimum, gint *natural)
{
    GwyColorAxis *axis = GWY_COLOR_AXIS(widget);
    GwyColorAxisPrivate *priv = axis->priv;

    *minimum = *natural = (priv->orientation == GTK_ORIENTATION_VERTICAL ? 80 : 1);
}

static void
get_preferred_height(GtkWidget *widget,
                     gint *minimum, gint *natural)
{
    GwyColorAxis *axis = GWY_COLOR_AXIS(widget);
    GwyColorAxisPrivate *priv = axis->priv;

    *minimum = *natural = (priv->orientation == GTK_ORIENTATION_VERTICAL ? 1 : 50);
}

static void
size_allocate(GtkWidget *widget,
              GdkRectangle *allocation)
{
    GwyColorAxisPrivate *priv = GWY_COLOR_AXIS(widget)->priv;
    parent_class->size_allocate(widget, allocation);
    if (priv->event_window)
        gdk_window_move_resize(priv->event_window, allocation->x, allocation->y, allocation->width, allocation->height);
    update(GWY_COLOR_AXIS(widget));
}

/* auxilliary function to compute decimal points from tickdist */
static gint
step_to_prec(gdouble d)
{
    gdouble resd = log10(7.5) - log10(d);
    if (resd != resd)
        return 1;
    if (resd > 1e20)
        return 1;
    if (resd < 1.0)
        resd = 1.0;
    return (gint)floor(resd);
}

static void
draw_labels_ticks(GwyColorAxis *axis, cairo_t *cr)
{
    GtkWidget *widget = GTK_WIDGET(axis);
    GwyColorAxisPrivate *priv = axis->priv;
    gint width, height, swidth, off, tlength, size, pos, prec = 1, mintdist = MIN_TICK_DISTANCE;
    guint txlen, i;
    gdouble scale, x, x2, m, tickdist, max;
    GString *strlabel;
    PangoLayout *layout;
    PangoRectangle rectlabel;
    GArray *tickx;
    GwyValueFormat *format = NULL;

    if (!priv->labels_visible) {
        priv->labelb_size = 1;
        priv->labele_size = 1;
    }

    GdkRectangle allocation;
    gtk_widget_get_allocation(widget, &allocation);
    width = allocation.width;
    height = allocation.height;
    swidth = priv->stripe_width;
    off = swidth + 2;
    tlength = priv->tick_length;

    /* Draw frame around false color scale and boundary marks */
    GtkStyleContext *context = gwy_setup_widget_foreground_drawing(widget, cr);

    cairo_set_line_width(cr, 1.0);
    if (priv->orientation == GTK_ORIENTATION_VERTICAL) {
        cairo_rectangle(cr, 0.5, 0.5, swidth, height-1);
        cairo_move_to(cr, 0.5, 0.5);
        cairo_rel_line_to(cr, swidth + tlength, 0.0);
        cairo_move_to(cr, 0.5, height - 0.5);
        cairo_rel_line_to(cr, swidth + tlength, 0.0);
        size = height;
    }
    else {
        cairo_rectangle(cr, 0.5, 0.5, width-1, swidth);
        cairo_move_to(cr, 0.5, 0.5);
        cairo_rel_line_to(cr, 0.5, swidth + tlength);
        cairo_move_to(cr, width - 0.5, 0.5);
        cairo_rel_line_to(cr, 0.5, swidth + tlength);
        size = width;
    }
    cairo_stroke(cr);

    /* Don't attempt to draw anything if rounding errors are too large or scale calculation can overflow */
    x = priv->max - priv->min;
    max = MAX(fabs(priv->min), fabs(priv->max));
    if (x < 1e-15*max || x <= 1e4*G_MINDOUBLE || max >= 1e-4*G_MAXDOUBLE)
        return;

    strlabel = g_string_new(NULL);
    layout = gtk_widget_create_pango_layout(widget, "");
    format = gwy_unit_get_format(priv->unit, GWY_UNIT_FORMAT_VFMARKUP, max, NULL);

    if (priv->ticks_style == GWY_TICKS_STYLE_AUTO || priv->ticks_style == GWY_TICKS_STYLE_UNLABELLED) {
        scale = size/(priv->max - priv->min);
        x = MIN_TICK_DISTANCE/scale;
        m = gwy_exp10(floor(log10(x)));
        x /= m;
        gwy_debug("scale: %g x: %g m: %g", scale, x, m);
        if (x == 1.0)
            x = 1.0;
        else if (x <= 2.0)
            x = 2.0;
        else if (x <= 5.0)
            x = 5.0;
        else
            x = 10.0;
        tickdist = x*m;
        x = floor(priv->min/tickdist)*tickdist;
        max = ceil(priv->max/tickdist)*tickdist;
        /* if labels_visible, then compute precision */
        if (priv->labels_visible) {
            prec = step_to_prec(tickdist/format->magnitude);
            gwy_debug("precision 1: %d; %g", prec, tickdist/format->magnitude);
            /* if horizontal, then recompute tickdist */
            if (priv->orientation == GTK_ORIENTATION_HORIZONTAL) {
                /* compute maximum size of all labels */
                x2 = x;
                while (x2 <= max) {
                    /* Spaces here in format string cause gap size between tick
                     * labels in horizontal color axis */
                    g_string_printf(strlabel, "%3.*f  ", prec, x2/format->magnitude);
                    pango_layout_set_markup(layout, strlabel->str, strlabel->len);
                    pango_layout_get_pixel_extents(layout, NULL, &rectlabel);
                    if (rectlabel.width > mintdist)
                        mintdist = rectlabel.width;
                    x2 += tickdist;
                }
                /* tickdist recomputation */
                x = mintdist/scale;
                m = gwy_exp10(floor(log10(x)));
                x /= m;
                gwy_debug("scale: %g x: %g m: %g", scale, x, m);
                if (x == 1.0)
                    x = 1.0;
                else if (x <= 2.0)
                    x = 2.0;
                else if (x <= 5.0)
                    x = 5.0;
                else
                    x = 10.0;
                tickdist = x*m;
                x = floor(priv->min/tickdist)*tickdist;
                max = ceil(priv->max/tickdist)*tickdist;
                prec = step_to_prec(tickdist/format->magnitude);
                gwy_debug("precision 2: %d; %g", prec, tickdist/format->magnitude);
            }
            /* draw min and max label; non-drawing when not labels_visible dealt with inside the function */
            draw_labels(axis, cr, prec);
        }
        gwy_debug("tickdist: %g x: %g max: %g", tickdist, x, max);
        tickx = g_array_new(FALSE, FALSE, sizeof(gdouble));
        while (x <= max) {
            g_array_append_val(tickx, x);
            x += tickdist;
        }
        txlen = tickx->len;
        if (priv->map_ticks && txlen) {
            gdouble *tmx = g_new(gdouble, txlen);

            priv->map_ticks(axis, (const gdouble*)tickx->data, tmx, txlen, priv->map_ticks_data);
            g_array_set_size(tickx, 0);
            for (i = 0; i < txlen; i++) {
                x = priv->min + (priv->max - priv->min)*tmx[i];
                g_array_append_val(tickx, x);
            }
            g_free(tmx);
        }
        if (priv->orientation == GTK_ORIENTATION_VERTICAL) {
            for (i = 0; i < txlen; i++) {
                x = g_array_index(tickx, gdouble, i);
                pos = size-1 - GWY_ROUND((x - priv->min)*scale);
                if (pos > priv->labelb_size && pos < size-1-priv->labele_size)
                    gwy_cairo_line(cr, swidth, pos, swidth + tlength/2, pos);
            }
            cairo_stroke(cr);
            if (priv->ticks_style == GWY_TICKS_STYLE_AUTO && priv->labels_visible) {
                for (i = 0; i < txlen; i++) {
                    x = g_array_index(tickx, gdouble, i);
                    pos = size-1 - GWY_ROUND((x - priv->min)*scale);
                    if (pos > priv->labelb_size && pos < size-1-priv->labele_size) {
                        g_string_printf(strlabel, "%3.*f", prec, x/format->magnitude);
                        pango_layout_set_markup(layout, strlabel->str, strlabel->len);
                        pango_layout_get_pixel_extents(layout, NULL, &rectlabel);
                        /* prevent drawing over maximum label */
                        if (pos - rectlabel.height > priv->labelb_size)
                            gtk_render_layout(context, cr, off, pos-rectlabel.height, layout);
                    }
                }
            }
        }
        else {
            for (i = 0; i < txlen; i++) {
                x = g_array_index(tickx, gdouble, i);
                pos = GWY_ROUND((x - priv->min)*scale);
                if (pos > priv->labelb_size && pos < size-1-priv->labele_size)
                    gwy_cairo_line(cr, pos, swidth, pos, swidth + tlength/2);
            }
            cairo_stroke(cr);
            if (priv->ticks_style == GWY_TICKS_STYLE_AUTO && priv->labels_visible) {
                for (i = 0; i < txlen; i++) {
                    x = g_array_index(tickx, gdouble, i);
                    pos = size-1 - GWY_ROUND((x - priv->min)*scale);
                    if (pos > priv->labelb_size && pos < size-1-priv->labele_size) {
                        g_string_printf(strlabel, "%3.*f", prec, x/format->magnitude);
                        pango_layout_set_markup(layout, strlabel->str, strlabel->len);
                        pango_layout_get_pixel_extents(layout, NULL, &rectlabel);
                        /* prevent drawing over maximum label */
                        if (pos + rectlabel.width < size-1-priv->labele_size)
                            gtk_render_layout(context, cr, pos, off, layout);
                    }
                }
            }
        }
        g_array_free(tickx, TRUE);
    }
    else if (priv->ticks_style == GWY_TICKS_STYLE_NONE) {
        draw_labels(axis, cr, 1);
    }
    else if (priv->ticks_style == GWY_TICKS_STYLE_CENTER) {
        draw_labels(axis, cr, 1);
        x2 = (priv->max+priv->min)*0.5;
        if (priv->orientation == GTK_ORIENTATION_VERTICAL) {
            pos = height/2;
            gwy_cairo_line(cr, swidth, height/2, swidth + tlength/2, height/2);
            if (priv->labels_visible) {
                g_string_printf(strlabel, "%3.1f", x2/format->magnitude);
                pango_layout_set_markup(layout, strlabel->str, strlabel->len);
                pango_layout_get_pixel_extents(layout, NULL, &rectlabel);
                /* prevent drawing over maximum label */
                if (pos - rectlabel.height > priv->labelb_size)
                    gtk_render_layout(context, cr, off, pos - rectlabel.height, layout);
            }
        }
        else {
            pos = width/2;
            gwy_cairo_line(cr, width/2, swidth, width/2, swidth + tlength/2);
            if (priv->labels_visible) {
                g_string_printf(strlabel, "%3.1f  ", x2/format->magnitude);
                pango_layout_set_markup(layout, strlabel->str, strlabel->len);
                pango_layout_get_pixel_extents(layout, NULL, &rectlabel);
                /* prevent drawing over maximum label */
                if (pos + rectlabel.width < size-1-priv->labele_size)
                    gtk_render_layout(context, cr, pos, off, layout);
            }
        }
    }
    GWY_FREE_VALUE_FORMAT(format);
    g_string_free(strlabel, TRUE);
    g_object_unref(layout);
}

static gboolean
draw(GtkWidget *widget, cairo_t *cr)
{
    GwyColorAxis *axis = GWY_COLOR_AXIS(widget);
    GwyColorAxisPrivate *priv = axis->priv;

    if (!priv->gradient_pattern) {
        GtkPositionType towards = (priv->orientation == GTK_ORIENTATION_VERTICAL ? GTK_POS_TOP : GTK_POS_RIGHT);
        priv->gradient_pattern = gwy_cairo_pattern_create_gradient(priv->gradient, towards, FALSE);
    }

    GdkRectangle allocation;
    gtk_widget_get_allocation(widget, &allocation);
    cairo_matrix_t matrix;
    if (priv->orientation == GTK_ORIENTATION_VERTICAL) {
        cairo_matrix_init_scale(&matrix, 1.0, 1.0/allocation.height);
        if (priv->inverted) {
            cairo_matrix_scale(&matrix, 1.0, -1.0);
            cairo_matrix_translate(&matrix, 0.0, -allocation.height);
        }
    }
    else {
        cairo_matrix_init_scale(&matrix, 1.0/allocation.width, 1.0);
        if (priv->inverted) {
            cairo_matrix_scale(&matrix, -1.0, 1.0);
            /* FIXME: Is this correct? */
            cairo_matrix_translate(&matrix, -allocation.width, 0.0);
        }
    }
    cairo_pattern_set_matrix(priv->gradient_pattern, &matrix);

    cairo_save(cr);
    if (priv->orientation == GTK_ORIENTATION_VERTICAL)
        cairo_rectangle(cr, 0, 0, priv->stripe_width, allocation.height);
    else
        cairo_rectangle(cr, 0, 0, allocation.width, priv->stripe_width);
    cairo_set_source(cr, priv->gradient_pattern);
    cairo_fill(cr);
    cairo_restore(cr);

    draw_labels_ticks(axis, cr);

    return FALSE;
}

static void
draw_labels(GwyColorAxis *axis, cairo_t *cr, gint prec)
{
    GwyColorAxisPrivate *priv = axis->priv;
    GtkWidget *widget = GTK_WIDGET(axis);
    PangoRectangle rectmin, rectmax, rectunits;
    gint xthickness = 1, ythickness = 1;

    gwy_debug("labels_visible: %d", priv->labels_visible);

    if (!priv->labels_visible) {
        priv->labelb_size = 1;
        priv->labele_size = 1;
        return;
    }

    gint width = gtk_widget_get_allocated_width(widget);
    gint height = gtk_widget_get_allocated_height(widget);
    gint swidth = priv->stripe_width;
    gint off = swidth + 2;

    /* Compute minimum and maximum numbers */
    GString *strmin = g_string_new(NULL);
    GString *strmax = g_string_new(NULL);
    gdouble max = MAX(fabs(priv->min), fabs(priv->max));
    GwyValueFormat *format = gwy_unit_get_format(priv->unit, GWY_UNIT_FORMAT_VFMARKUP, max, NULL);

    /* min label */
    if (max == 0)
        g_string_assign(strmin, "0.0");
    else
        g_string_printf(strmin, "%3.*f  ", prec, priv->min/format->magnitude);

    PangoLayout *layoutmin = gtk_widget_create_pango_layout(widget, "");
    pango_layout_set_markup(layoutmin, strmin->str, strmin->len);
    pango_layout_get_pixel_extents(layoutmin, NULL, &rectmin);

    /* max label */
    gdouble max_label_aux = (max == 0 ? 0.0 : priv->max/format->magnitude);
    PangoLayout *layoutmax = gtk_widget_create_pango_layout(widget, "");
    if (priv->orientation == GTK_ORIENTATION_VERTICAL) {
        g_string_printf(strmax, "%3.*f %s", prec, max_label_aux, format->units);
        /* Measure text */
        pango_layout_set_markup(layoutmax, strmax->str, strmax->len);
        pango_layout_get_pixel_extents(layoutmax, NULL, &rectmax);
        if (rectmax.width + off > width) {
            /* twoline layout */
            g_string_printf(strmax, "%3.*f\n%s", prec, priv->max/format->magnitude, format->units);
            pango_layout_set_markup(layoutmax, strmax->str, strmax->len);
            pango_layout_get_pixel_extents(layoutmax, NULL, &rectmax);
        }
    }
    else {
        g_string_printf(strmax, "%3.*f", prec, max_label_aux);
        pango_layout_set_markup(layoutmax, strmax->str, strmax->len);
        pango_layout_get_pixel_extents(layoutmax, NULL, &rectmax);
    }

    /* Draw text */
    GtkStyleContext *context = gtk_widget_get_style_context(widget);
    if (priv->orientation == GTK_ORIENTATION_VERTICAL) {
        gtk_render_layout(context, cr, off, ythickness, layoutmax);
        priv->labelb_size = rectmax.height;
    }
    else {
        gtk_render_layout(context, cr, width - rectmax.width - xthickness, off, layoutmax);
        priv->labele_size = rectmax.width;
    }

    if (priv->orientation == GTK_ORIENTATION_VERTICAL) {
        gtk_render_layout(context, cr, off, height - rectmin.height - ythickness, layoutmin);
        priv->labele_size = rectmin.height;
    }
    else {
        gtk_render_layout(context, cr, xthickness, off, layoutmin);
        priv->labelb_size = rectmin.width;
    }

    if (priv->orientation == GTK_ORIENTATION_HORIZONTAL) {
        /* Draw units separately */
        GString *strunits = g_string_new(NULL);
        PangoLayout *layoutunits = gtk_widget_create_pango_layout(widget, "");
        g_string_printf(strunits, "%s", format->units);
        pango_layout_set_markup(layoutunits, strunits->str, strunits->len);
        pango_layout_get_pixel_extents(layoutunits, NULL, &rectunits);
        gtk_render_layout(context, cr, width-rectunits.width-xthickness, off+rectmax.height, layoutunits);
        g_object_unref(layoutunits);
        g_string_free(strunits, TRUE);
    }

    g_object_unref(layoutmin);
    g_object_unref(layoutmax);
    g_string_free(strmin, TRUE);
    g_string_free(strmax, TRUE);
    GWY_FREE_VALUE_FORMAT(format);
}

/**
 * gwy_color_axis_get_range:
 * @axis: A color axis.
 * @min: Location to store the range maximum (or %NULL).
 * @max: Location to store the range minimum (or %NULL).
 *
 * Gets the range of a color axis.
 **/
void
gwy_color_axis_get_range(GwyColorAxis *axis,
                         gdouble *min,
                         gdouble *max)
{
    g_return_if_fail(GWY_IS_COLOR_AXIS(axis));

    GwyColorAxisPrivate *priv = axis->priv;
    if (min)
        *min = priv->inverted ? priv->max : priv->min;
    if (max)
        *max = priv->inverted ? priv->min : priv->max;
}

/**
 * gwy_color_axis_set_range:
 * @axis: A color axis.
 * @min: The range minimum.
 * @max: The range maximum.
 *
 * Sets the range of a color axis.
 **/
void
gwy_color_axis_set_range(GwyColorAxis *axis,
                         gdouble min,
                         gdouble max)
{
    g_return_if_fail(GWY_IS_COLOR_AXIS(axis));

    GwyColorAxisPrivate *priv = axis->priv;
    gboolean inverted = max < min;

    if ((!priv->inverted && priv->min == min && priv->max == max)
        || (priv->inverted && priv->min == max && priv->max == min))
        return;

    priv->min = MIN(min, max);
    priv->max = MAX(min, max);

    if (priv->inverted != inverted)
        priv->inverted = inverted;
    update(axis);
}

/**
 * gwy_color_axis_get_gradient:
 * @axis: A colour axis.
 *
 * Gets the colour gradient a colour axis uses.
 *
 * If no specific gradient has been set and the default one is used, the function returns %NULL. This is consistent
 * with getting the corresponding property. If you still need the #GwyGradient object in such case, use
 * gwy_gradients_get_gradient() with %NULL argument.
 *
 * Returns: (transfer none) (nullable):
 *          The current colour gradient, possibly %NULL.
 **/
GwyGradient*
gwy_color_axis_get_gradient(GwyColorAxis *axis)
{
    g_return_val_if_fail(GWY_IS_COLOR_AXIS(axis), NULL);
    GwyColorAxisPrivate *priv = axis->priv;
    return priv->default_gradient_changed_id ? NULL : priv->gradient;
}

/**
 * gwy_color_axis_set_gradient:
 * @axis: A color axis.
 * @gradient: (transfer none) (nullable):
 *            Colour gradient the axis should show.
 *
 * Sets the colour gradient a colour axis should use.
 **/
void
gwy_color_axis_set_gradient(GwyColorAxis *axis,
                            GwyGradient *gradient)
{
    g_return_if_fail(GWY_IS_COLOR_AXIS(axis));
    g_return_if_fail(!gradient || GWY_IS_GRADIENT(gradient));
    GwyColorAxisPrivate *priv = axis->priv;
    GwyGradient *real_gradient = gradient ? gradient : gwy_gradients_get_gradient(NULL);
    gboolean prop_changed = (priv->default_gradient_changed_id ? !!gradient : gradient != priv->gradient);
    gboolean visually_changed = gwy_set_member_object(axis, real_gradient, GWY_TYPE_GRADIENT, &priv->gradient,
                                                      "data-changed", gradient_changed,
                                                      &priv->gradient_changed_id, G_CONNECT_SWAPPED,
                                                      NULL);
    if (visually_changed) {
        g_clear_pointer(&priv->gradient_pattern, cairo_pattern_destroy);
        update(axis);
    }
    if (prop_changed) {
        follow_default_gradient(axis, !gradient);
        g_object_notify_by_pspec(G_OBJECT(axis), properties[PROP_GRADIENT]);
    }
}

/**
 * gwy_color_axis_get_unit:
 * @axis: A color axis.
 *
 * Gets the SI unit a color axis displays.
 *
 * Returns: The SI unit.
 **/
GwyUnit*
gwy_color_axis_get_unit(GwyColorAxis *axis)
{
    g_return_val_if_fail(GWY_IS_COLOR_AXIS(axis), NULL);
    return axis->priv->unit;
}

/**
 * gwy_color_axis_get_ticks_style:
 * @axis: A color axis.
 *
 * Gets ticks style of a color axis.
 *
 * Returns: The ticks style.
 **/
GwyTicksStyle
gwy_color_axis_get_ticks_style(GwyColorAxis *axis)
{
    g_return_val_if_fail(GWY_IS_COLOR_AXIS(axis), GWY_TICKS_STYLE_NONE);

    return axis->priv->ticks_style;
}

/**
 * gwy_color_axis_set_ticks_style:
 * @axis: A color axis.
 * @ticks_style: The ticks style to use.
 *
 * Sets the ticks style of a color axis.
 **/
void
gwy_color_axis_set_ticks_style(GwyColorAxis *axis,
                               GwyTicksStyle ticks_style)
{
    g_return_if_fail(GWY_IS_COLOR_AXIS(axis));
    g_return_if_fail(ticks_style <= GWY_TICKS_STYLE_UNLABELLED);

    GwyColorAxisPrivate *priv = axis->priv;
    if (priv->ticks_style == ticks_style)
        return;

    priv->ticks_style = ticks_style;
    g_object_notify_by_pspec(G_OBJECT(axis), properties[PROP_TICKS_STYLE]);
    update(axis);
}

/**
 * gwy_color_axis_get_labels_visible:
 * @axis: A color axis.
 *
 * Gets the visibility of labels of a color axis.
 *
 * Returns: %TRUE if labels are displayed, %FALSE if they are omitted.
 **/
gboolean
gwy_color_axis_get_labels_visible(GwyColorAxis *axis)
{
    g_return_val_if_fail(GWY_IS_COLOR_AXIS(axis), FALSE);

    return axis->priv->labels_visible;
}

/**
 * gwy_color_axis_set_labels_visible:
 * @axis: A color axis.
 * @labels_visible: %TRUE to display labels with minimum and maximum values, %FALSE to display no labels.
 *
 * Sets the visibility of labels of a color axis.
 **/
void
gwy_color_axis_set_labels_visible(GwyColorAxis *axis,
                                  gboolean labels_visible)
{
    g_return_if_fail(GWY_IS_COLOR_AXIS(axis));

    GwyColorAxisPrivate *priv = axis->priv;
    if (!priv->labels_visible == !labels_visible)
        return;

    priv->labels_visible = !!labels_visible;
    g_object_notify_by_pspec(G_OBJECT(axis), properties[PROP_LABELS_VISIBLE]);
    update(axis);
}

static void
update(GwyColorAxis *axis)
{
    if (gtk_widget_is_drawable(GTK_WIDGET(axis)))
        gtk_widget_queue_draw(GTK_WIDGET(axis));
}

/**
 * gwy_color_axis_set_tick_map_func:
 * @axis: A color axis.
 * @func: Tick mapping function.
 * @user_data: Data to pass to @func.
 *
 * Set the tick mapping function for a color axis.
 *
 * The axis calculates tick positions as for the linear axis and then places them non-linearly using @func.  Hence
 * a mapping function should be used with ticks mode %GWY_TICKS_STYLE_UNLABELLED because minimum tick spacing is not
 * guaranteed.
 **/
void
gwy_color_axis_set_tick_map_func(GwyColorAxis *axis,
                                 GwyColorAxisMapFunc func,
                                 gpointer user_data)
{
    g_return_if_fail(GWY_IS_COLOR_AXIS(axis));

    GwyColorAxisPrivate *priv = axis->priv;

    if (priv->map_ticks == func && priv->map_ticks_data == user_data)
        return;

    priv->map_ticks = func;
    priv->map_ticks_data = user_data;
    update(axis);
}

static void
unit_changed(GwyColorAxis *axis)
{
    update(axis);
    g_object_notify_by_pspec(G_OBJECT(axis), properties[PROP_UNIT]);
}

static void
gradient_changed(GwyColorAxis *axis)
{
    GwyColorAxisPrivate *priv = axis->priv;
    g_clear_pointer(&priv->gradient_pattern, cairo_pattern_destroy);
    update(axis);
}

static void
follow_default_gradient(GwyColorAxis *axis, gboolean follow)
{
    GwyColorAxisPrivate *priv = axis->priv;
    GwyInventory *inventory = gwy_gradients();
    if (!follow) {
        g_clear_signal_handler(&priv->default_gradient_changed_id, inventory);
        return;
    }
    if (priv->default_gradient_changed_id)
        return;
    priv->default_gradient_changed_id = g_signal_connect_swapped(inventory, "default-changed",
                                                                 G_CALLBACK(default_gradient_changed), axis);
}

static void
default_gradient_changed(GwyColorAxis *axis)
{
    /* That should do it. */
    gwy_color_axis_set_gradient(axis, NULL);
}

/**
 * SECTION: color-axis
 * @title: GwyColorAxis
 * @short_description: Axis with a false color scale
 * @see_also: #GwyAxis -- Axis for use in graphs,
 *            #GwyRuler -- Horizontal and vertical rulers
 *
 * Color axis displays an axis with tick marks together with a false colour gradient. It can be used to display the
 * false colour mapping (scale).
 **/

/**
 * GwyColorAxisMapFunc:
 * @axis: A color axis.
 * @z: Array of length @n of tick values.
 * @mapped: Array of length @n where values mapped to [0,1] should be placed.
 * @n: Length of @z and @mapped.
 * @user_data: Data passed to gwy_color_axis_set_tick_map_func().
 *
 * Type of color axis non-linear tick mapping function.
 **/

/**
 * GwyTicksStyle:
 * @GWY_TICKS_STYLE_NONE: Ticks are not drawn.
 * @GWY_TICKS_STYLE_CENTER: One tick is drawn at center.
 * @GWY_TICKS_STYLE_AUTO: A number of automatically chosen ticks are drawn at `round' values.
 * @GWY_TICKS_STYLE_UNLABELED: Only edge ticks are labelled, interior ticks are drawn but not labelled.
 * @GWY_TICKS_STYLE_UNLABELLED: Alias for %GWY_TICKS_STYLE_UNLABELED.
 *
 * Axis ticks style (used in #GwyColorAxis).
 **/

/* 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 : */
