/*
 *  $Id: cprofile.c 29276 2026-01-17 12:27:05Z yeti-dn $
 *  Copyright (C) 2019-2026 David Necas (Yeti).
 *  E-mail: yeti@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 <glib/gi18n-lib.h>
#include <string.h>
#include <gtk/gtk.h>
#include <gwy.h>

enum {
    COLUMN_I, COLUMN_X, COLUMN_Y, NCOLUMNS
};

enum {
    PARAM_MODE,
    PARAM_MASKING,
    PARAM_THICKNESS,
    PARAM_ZERO_CROSS,
    PARAM_HOLD_SELECTION,
    PARAM_OPTIONS_VISIBLE,
    PARAM_TARGET_GRAPH,
};

#define GWY_TYPE_TOOL_CPROFILE            (gwy_tool_cprofile_get_type())
#define GWY_TOOL_CPROFILE(obj)            (G_TYPE_CHECK_INSTANCE_CAST((obj), GWY_TYPE_TOOL_CPROFILE, GwyToolCprofile))
#define GWY_IS_TOOL_CPROFILE(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), GWY_TYPE_TOOL_CPROFILE))
#define GWY_TOOL_CPROFILE_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS((obj), GWY_TYPE_TOOL_CPROFILE, GwyToolCprofileClass))

typedef struct _GwyToolCprofile      GwyToolCprofile;
typedef struct _GwyToolCprofileClass GwyToolCprofileClass;

struct _GwyToolCprofile {
    GwyPlainTool parent_instance;

    GwyParams *params;

    GtkTreeView *treeview;
    GtkTreeModel *model;

    GArray *xydata;
    GwyGraphModel *gmodel;
    GdkPixbuf *colorpixbuf;

    GwyParamTable *table;

    /* potential class data */
    GwyValueFormat *pixel_format;
};

struct _GwyToolCprofileClass {
    GwyPlainToolClass parent_class;
};

static gboolean     module_register                    (void);
static GwyParamDef* define_module_params               (void);
static GType        gwy_tool_cprofile_get_type         (void)                      G_GNUC_CONST;
static void         gwy_tool_cprofile_finalize         (GObject *object);
static void         gwy_tool_cprofile_init_dialog      (GwyToolCprofile *tool);
static void         gwy_tool_cprofile_data_switched    (GwyTool *gwytool,
                                                        GwyDataView *data_view);
static void         gwy_tool_cprofile_data_changed     (GwyPlainTool *plain_tool);
static void         gwy_tool_cprofile_response         (GwyTool *tool,
                                                        gint response_id);
static void         gwy_tool_cprofile_apply            (GwyToolCprofile *tool);
static void         gwy_tool_cprofile_selection_changed(GwyPlainTool *plain_tool,
                                                        gint hint);
static void         param_changed                      (GwyToolCprofile *tool,
                                                        gint id);
static void         update_curve                       (GwyToolCprofile *tool,
                                                        gint i);
static void         update_all_curves                  (GwyToolCprofile *tool);
static void         render_cell                        (GtkCellLayout *layout,
                                                        GtkCellRenderer *renderer,
                                                        GtkTreeModel *model,
                                                        GtkTreeIter *iter,
                                                        gpointer user_data);
static void         render_color                       (GtkCellLayout *layout,
                                                        GtkCellRenderer *renderer,
                                                        GtkTreeModel *model,
                                                        GtkTreeIter *iter,
                                                        gpointer user_data);

static GwyModuleInfo module_info = {
    GWY_MODULE_ABI_VERSION,
    &module_register,
    N_("Profile tool which reads horizontal and/or vertical scan lines."),
    "Yeti <yeti@gwyddion.net>",
    "2.2",
    "David Nečas (Yeti)",
    "2019",
};

GWY_MODULE_QUERY2(module_info, cprofile)

G_DEFINE_TYPE(GwyToolCprofile, gwy_tool_cprofile, GWY_TYPE_PLAIN_TOOL)

static gboolean
module_register(void)
{
    gwy_tool_func_register(GWY_TYPE_TOOL_CPROFILE);

    return TRUE;
}

static GwyParamDef*
define_module_params(void)
{
    static const GwyEnum modes[] = {
        /* NB: These are not swapped. Show the direction of the lines to the user, not the coordinate she selects. */
        { N_("Vertical"),   GWY_DIRECTION_HORIZONTAL, },
        { N_("Horizontal"), GWY_DIRECTION_VERTICAL,   },
        { N_("Cross"),      GWY_DIRECTION_BOTH,       },
    };
    static GwyParamDef *paramdef = NULL;

    if (paramdef)
        return paramdef;

    paramdef = gwy_param_def_new();
    gwy_param_def_set_function_name(paramdef, "cprofile");
    gwy_param_def_add_gwyenum(paramdef, PARAM_MODE, "mode", _("_Mode"),
                              modes, G_N_ELEMENTS(modes), GWY_DIRECTION_BOTH);
    gwy_param_def_add_enum(paramdef, PARAM_MASKING, "masking", NULL, GWY_TYPE_MASKING_TYPE, GWY_MASK_IGNORE);
    gwy_param_def_add_int(paramdef, PARAM_THICKNESS, "thickness", _("_Thickness"), 1, 128, 1);
    gwy_param_def_add_boolean(paramdef, PARAM_ZERO_CROSS, "zero_cross", _("Cross at _zero"), TRUE);
    gwy_param_def_add_boolean(paramdef, PARAM_OPTIONS_VISIBLE, "options_visible", NULL, FALSE);
    gwy_param_def_add_target_graph(paramdef, PARAM_TARGET_GRAPH, NULL, NULL);
    gwy_param_def_add_hold_selection(paramdef, PARAM_HOLD_SELECTION, "hold_selection", NULL);

    return paramdef;
}

static void
gwy_tool_cprofile_class_init(GwyToolCprofileClass *klass)
{
    GwyPlainToolClass *ptool_class = GWY_PLAIN_TOOL_CLASS(klass);
    GwyToolClass *tool_class = GWY_TOOL_CLASS(klass);
    GObjectClass *gobject_class = G_OBJECT_CLASS(klass);

    gobject_class->finalize = gwy_tool_cprofile_finalize;

    tool_class->icon_name = GWY_ICON_CROSS_PROFILE;
    tool_class->title = _("Profiles Along Axes");
    tool_class->tooltip = _("Read horizontal and/or vertical profiles");
    tool_class->prefix = "/module/cprofile";
    tool_class->default_width = 640;
    tool_class->default_height = 400;
    tool_class->data_switched = gwy_tool_cprofile_data_switched;
    tool_class->response = gwy_tool_cprofile_response;

    ptool_class->data_changed = gwy_tool_cprofile_data_changed;
    ptool_class->selection_changed = gwy_tool_cprofile_selection_changed;
}

static void
gwy_tool_cprofile_finalize(GObject *object)
{
    GwyToolCprofile *tool = GWY_TOOL_CPROFILE(object);

    gwy_params_save_to_settings(tool->params);
    g_clear_object(&tool->params);
    if (tool->xydata)
        g_array_free(tool->xydata, TRUE);
    if (tool->model) {
        gtk_tree_view_set_model(tool->treeview, NULL);
        g_clear_object(&tool->model);
    }
    g_clear_object(&tool->gmodel);
    g_clear_object(&tool->colorpixbuf);
    GWY_FREE_VALUE_FORMAT(tool->pixel_format);

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

static void
gwy_tool_cprofile_init(GwyToolCprofile *tool)
{
    GwyPlainTool *plain_tool = GWY_PLAIN_TOOL(tool);
    gint width, height;

    plain_tool->unit_style = GWY_UNIT_FORMAT_MARKUP;
    plain_tool->lazy_updates = TRUE;

    tool->params = gwy_params_new_from_settings(define_module_params());
    tool->pixel_format = gwy_value_format_new(1.0, 0, _("px"));
    tool->model = GTK_TREE_MODEL(gwy_null_store_new(0));

    gtk_icon_size_lookup(GTK_ICON_SIZE_MENU, &width, &height);
    height |= 1;
    tool->colorpixbuf = gdk_pixbuf_new(GDK_COLORSPACE_RGB, FALSE, 8, height, height);

    gwy_plain_tool_connect_selection(plain_tool, GWY_TYPE_LAYER_CROSS, "cross");
    gwy_plain_tool_enable_selection_holding(plain_tool);

    gwy_tool_cprofile_init_dialog(tool);
}

static void
gwy_tool_cprofile_init_dialog(GwyToolCprofile *tool)
{
    static const gchar *column_titles[] = {
        "<b>n</b>", "<b>x</b>", "<b>y</b>",
    };
    GtkDialog *dialog = GTK_DIALOG(GWY_TOOL(tool)->dialog);
    GtkTreeViewColumn *column;
    GtkCellRenderer *renderer;
    GtkWidget *scwin, *label, *hbox, *vbox, *options, *graph;
    GwyParamTable *table;
    guint i;

    tool->gmodel = gwy_graph_model_new();
    g_object_set(tool->gmodel, "title", _("Profiles"), "label-visible", FALSE, NULL);

    hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
    gtk_box_pack_start(GTK_BOX(gtk_dialog_get_content_area(dialog)), hbox, TRUE, TRUE, 0);

    /* Left pane */
    vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
    gtk_widget_set_hexpand(vbox, FALSE);
    gtk_box_pack_start(GTK_BOX(hbox), vbox, FALSE, TRUE, 0);

    /* Line coordinates */
    tool->treeview = GTK_TREE_VIEW(gtk_tree_view_new_with_model(tool->model));
    gwy_plain_tool_enable_object_deletion(GWY_PLAIN_TOOL(tool), tool->treeview);

    for (i = 0; i < NCOLUMNS; i++) {
        column = gtk_tree_view_column_new();
        gtk_tree_view_column_set_expand(column, TRUE);
        gtk_tree_view_column_set_alignment(column, 0.5);
        g_object_set_data(G_OBJECT(column), "id", GUINT_TO_POINTER(i));
        renderer = gtk_cell_renderer_text_new();
        g_object_set(renderer, "xalign", 1.0, NULL);
        gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(column), renderer, TRUE);
        gtk_cell_layout_set_cell_data_func(GTK_CELL_LAYOUT(column), renderer, render_cell, tool, NULL);
        if (i == COLUMN_I) {
            renderer = gtk_cell_renderer_pixbuf_new();
            g_object_set(renderer, "pixbuf", tool->colorpixbuf, NULL);
            gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(column), renderer, FALSE);
            gtk_cell_layout_set_cell_data_func(GTK_CELL_LAYOUT(column), renderer, render_color, tool, NULL);
        }

        label = gtk_label_new(NULL);
        gtk_label_set_markup(GTK_LABEL(label), column_titles[i]);
        gtk_tree_view_column_set_widget(column, label);
        gtk_widget_show(label);
        gtk_tree_view_append_column(tool->treeview, column);
    }

    scwin = gtk_scrolled_window_new(NULL, NULL);
    gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scwin), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
    gtk_container_add(GTK_CONTAINER(scwin), GTK_WIDGET(tool->treeview));
    gtk_box_pack_start(GTK_BOX(vbox), scwin, TRUE, TRUE, 0);

    /* Options */
    options = gwy_create_expander_with_param(_("<b>Options</b>"), tool->params, PARAM_OPTIONS_VISIBLE);
    gtk_box_pack_start(GTK_BOX(vbox), options, FALSE, FALSE, 0);

    table = tool->table = gwy_param_table_new(tool->params);
    gwy_param_table_append_combo(table, PARAM_MODE);
    gwy_param_table_append_combo(table, PARAM_MASKING);
    gwy_param_table_append_slider(table, PARAM_THICKNESS);
    gwy_param_table_set_unitstr(table, PARAM_THICKNESS, _("px"));
    gwy_param_table_append_checkbox(table, PARAM_ZERO_CROSS);
    gwy_param_table_append_target_graph(table, PARAM_TARGET_GRAPH, tool->gmodel);
    gwy_param_table_append_hold_selection(table, PARAM_HOLD_SELECTION);
    gwy_plain_tool_add_param_table(GWY_PLAIN_TOOL(tool), table);
    gtk_container_add(GTK_CONTAINER(options), gwy_param_table_widget(table));

    graph = gwy_graph_new(tool->gmodel);
    gwy_graph_enable_user_input(GWY_GRAPH(graph), FALSE);
    gtk_box_pack_start(GTK_BOX(hbox), graph, TRUE, TRUE, 2);

    gwy_plain_tool_add_clear_button(GWY_PLAIN_TOOL(tool));
    gwy_tool_add_hide_button(GWY_TOOL(tool), FALSE);
    gwy_plain_tool_add_apply_button(GWY_PLAIN_TOOL(tool));
    gtk_dialog_set_default_response(dialog, GTK_RESPONSE_APPLY);
    gtk_dialog_set_response_sensitive(dialog, GTK_RESPONSE_APPLY, FALSE);
    gwy_help_add_to_tool_dialog(dialog, GWY_TOOL(tool), GWY_HELP_DEFAULT);

    g_signal_connect_swapped(tool->table, "param-changed", G_CALLBACK(param_changed), tool);

    gtk_widget_show_all(gtk_dialog_get_content_area(dialog));
    gwy_param_table_param_changed(tool->table, -1);
}

static void
render_cell(GtkCellLayout *layout,
            GtkCellRenderer *renderer,
            GtkTreeModel *model,
            GtkTreeIter *iter,
            gpointer user_data)
{
    GwyToolCprofile *tool = (GwyToolCprofile*)user_data;
    GwyPlainTool *plain_tool = GWY_PLAIN_TOOL(tool);
    const GwyValueFormat *vf;
    gchar buf[32];
    gdouble line[4];
    gdouble val;
    guint idx, id;

    id = GPOINTER_TO_UINT(g_object_get_data(G_OBJECT(layout), "id"));
    gtk_tree_model_get(model, iter, 0, &idx, -1);
    if (id == COLUMN_I) {
        g_snprintf(buf, sizeof(buf), "%d", idx + 1);
        g_object_set(renderer, "text", buf, NULL);
        return;
    }

    gwy_selection_get_object(plain_tool->selection, idx, line);

    vf = tool->pixel_format;
    if (id == COLUMN_X)
        val = floor(gwy_field_rtoj(plain_tool->field, line[0]));
    else if (id == COLUMN_Y)
        val = floor(gwy_field_rtoi(plain_tool->field, line[1]));
    else {
        g_return_if_reached();
    }

    if (vf)
        g_snprintf(buf, sizeof(buf), "%.*f", vf->precision, val/vf->magnitude);
    else
        g_snprintf(buf, sizeof(buf), "%.3g", val);

    g_object_set(renderer, "text", buf, NULL);
}

static void
fill_pixbuf_triangular(GdkPixbuf *pixbuf, const GwyRGBA *ulcolor, const GwyRGBA *brcolor)
{
    gint width, height, bpp, rowstride, i, ir, j, jto;
    guchar *pixels, *row;
    guint ulpixel, brpixel;
    guchar ulsamples[3], brsamples[3], mixsamples[3];
    gboolean mixme;

    width = gdk_pixbuf_get_width(pixbuf);
    height = gdk_pixbuf_get_height(pixbuf);
    rowstride = gdk_pixbuf_get_rowstride(pixbuf);
    bpp = gdk_pixbuf_get_n_channels(pixbuf);
    g_return_if_fail(bpp == 3 || bpp == 4);
    pixels = gdk_pixbuf_get_pixels(pixbuf);

    ulpixel = gwy_rgba_to_pixbuf_pixel(ulcolor);
    brpixel = gwy_rgba_to_pixbuf_pixel(brcolor);
    brsamples[2] = (brpixel >> 8) & 0xff;
    brsamples[1] = (brpixel >> 16) & 0xff;
    brsamples[0] = (brpixel >> 24) & 0xff;

    for (i = 0; i < 3; i++) {
        ulpixel >>= 8;
        ulsamples[2-i] = ulpixel & 0xff;
        brpixel >>= 8;
        brsamples[2-i] = brpixel & 0xff;
        mixsamples[2-i] = (ulsamples[2-i] + brsamples[2-i] + 1)/2;
    }

    for (i = 0; i < height; i++) {
        row = pixels + i*rowstride;
        ir = height-1 - i;
        jto = ((2*ir + 1)*width - height)/(2*height);
        jto = MIN(jto, width);
        mixme = ((2*jto + 1)*height == (2*ir + 1)*width);
        for (j = 0; j < jto; j++) {
            *(row++) = ulsamples[0];
            *(row++) = ulsamples[1];
            *(row++) = ulsamples[2];
            if (bpp == 4)
                *(row++) = 0xff;
        }
        if (mixme) {
            *(row++) = mixsamples[0];
            *(row++) = mixsamples[1];
            *(row++) = mixsamples[2];
            if (bpp == 4)
                *(row++) = 0xff;
            jto++;
        }
        for (j = jto; j < width; j++) {
            *(row++) = brsamples[0];
            *(row++) = brsamples[1];
            *(row++) = brsamples[2];
            if (bpp == 4)
                *(row++) = 0xff;
        }
    }
}

static void
render_color(G_GNUC_UNUSED GtkCellLayout *layout,
             G_GNUC_UNUSED GtkCellRenderer *renderer,
             GtkTreeModel *model,
             GtkTreeIter *iter,
             gpointer user_data)
{
    GwyToolCprofile *tool = (GwyToolCprofile*)user_data;
    GwyDirectionFlags mode = gwy_params_get_enum(tool->params, PARAM_MODE);
    GwyGraphCurveModel *gcmodel;
    GwyRGBA *rgba, *rgba2;
    guint idx, pixel;

    gtk_tree_model_get(model, iter, 0, &idx, -1);

    if (mode != GWY_DIRECTION_BOTH) {
        gcmodel = gwy_graph_model_get_curve(tool->gmodel, idx);
        g_object_get(gcmodel, "color", &rgba, NULL);
        pixel = 0xff | gwy_rgba_to_pixbuf_pixel(rgba);
        gwy_rgba_free(rgba);
        gdk_pixbuf_fill(tool->colorpixbuf, pixel);
        return;
    }

    gcmodel = gwy_graph_model_get_curve(tool->gmodel, 2*idx);
    g_object_get(gcmodel, "color", &rgba, NULL);
    gcmodel = gwy_graph_model_get_curve(tool->gmodel, 2*idx + 1);
    g_object_get(gcmodel, "color", &rgba2, NULL);
    fill_pixbuf_triangular(tool->colorpixbuf, rgba, rgba2);
    gwy_rgba_free(rgba2);
    gwy_rgba_free(rgba);
}

static void
gwy_tool_cprofile_data_switched(GwyTool *gwytool,
                                GwyDataView *data_view)
{
    GwyPlainTool *plain_tool = GWY_PLAIN_TOOL(gwytool);
    GwyToolCprofile *tool = GWY_TOOL_CPROFILE(gwytool);
    gboolean ignore = (data_view == plain_tool->data_view);

    GWY_TOOL_CLASS(gwy_tool_cprofile_parent_class)->data_switched(gwytool, data_view);

    if (ignore || plain_tool->init_failed)
        return;

    if (data_view) {
        gwy_object_set_or_reset(plain_tool->layer, GWY_TYPE_LAYER_CROSS,
                                "directions", gwy_params_get_enum(tool->params, PARAM_MODE),
                                "thickness", (gdouble)gwy_params_get_int(tool->params, PARAM_THICKNESS),
                                "editable", TRUE,
                                "focus", -1,
                                NULL);
        gwy_selection_set_max_objects(plain_tool->selection, 1024);
        gwy_plain_tool_hold_selection(plain_tool, gwy_params_get_flags(tool->params, PARAM_HOLD_SELECTION));
    }

    gwy_graph_model_remove_all_curves(tool->gmodel);
    update_all_curves(tool);
    gwy_param_table_data_id_refilter(tool->table, PARAM_TARGET_GRAPH);
}

static void
gwy_tool_cprofile_data_changed(GwyPlainTool *plain_tool)
{
    update_all_curves(GWY_TOOL_CPROFILE(plain_tool));
    gwy_param_table_data_id_refilter(GWY_TOOL_CPROFILE(plain_tool)->table, PARAM_TARGET_GRAPH);
}

static void
param_changed(GwyToolCprofile *tool, gint id)
{
    GwyPlainTool *plain_tool = GWY_PLAIN_TOOL(tool);
    GwyParams *params = tool->params;
    gboolean do_update = (id != PARAM_MASKING && id != PARAM_TARGET_GRAPH && id != PARAM_OPTIONS_VISIBLE);

    if (id == PARAM_MASKING)
        do_update = do_update || (plain_tool->field && plain_tool->mask_field);
    if (id < 0 || id == PARAM_MODE) {
        if (plain_tool->layer) {
            gwy_layer_cross_set_directions(GWY_LAYER_CROSS(plain_tool->layer),
                                           gwy_params_get_enum(params, PARAM_MODE));
        }
        gwy_graph_model_remove_all_curves(tool->gmodel);
    }
    if (id < 0 || id == PARAM_THICKNESS) {
        if (plain_tool->layer) {
            gwy_layer_cross_set_thickness(GWY_LAYER_CROSS(plain_tool->layer),
                                          gwy_params_get_int(params, PARAM_THICKNESS));
        }
    }
    if (do_update)
        update_all_curves(tool);
}

static void
gwy_tool_cprofile_response(GwyTool *tool, gint response_id)
{
    GWY_TOOL_CLASS(gwy_tool_cprofile_parent_class)->response(tool, response_id);

    if (response_id == GTK_RESPONSE_APPLY)
        gwy_tool_cprofile_apply(GWY_TOOL_CPROFILE(tool));
}

static void
gwy_tool_cprofile_selection_changed(GwyPlainTool *plain_tool,
                                    gint hint)
{
    GwyToolCprofile *tool = GWY_TOOL_CPROFILE(plain_tool);
    GtkDialog *dialog = GTK_DIALOG(GWY_TOOL(tool)->dialog);
    GwyNullStore *store = store = GWY_NULL_STORE(tool->model);
    gint n = gwy_null_store_get_n_rows(store);

    g_return_if_fail(hint <= n);

    if (hint < 0) {
        gtk_tree_view_set_model(tool->treeview, NULL);
        n = (plain_tool->selection ? gwy_selection_get_data(plain_tool->selection, NULL) : 0);
        gwy_null_store_set_n_rows(store, n);
        gtk_tree_view_set_model(tool->treeview, tool->model);
        gwy_graph_model_remove_all_curves(tool->gmodel);
        update_all_curves(tool);
    }
    else {
        GtkTreeSelection *selection;
        GtkTreePath *path;
        GtkTreeIter iter;

        if (hint < n)
            gwy_null_store_row_changed(store, hint);
        else
            gwy_null_store_set_n_rows(store, n+1);
        update_curve(tool, hint);
        n++;

        gtk_tree_model_iter_nth_child(tool->model, &iter, NULL, hint);
        path = gtk_tree_model_get_path(tool->model, &iter);
        selection = gtk_tree_view_get_selection(tool->treeview);
        gtk_tree_selection_select_iter(selection, &iter);
        gtk_tree_view_scroll_to_cell(tool->treeview, path, NULL, FALSE, 0.0, 0.0);
        gtk_tree_path_free(path);
    }

    gtk_dialog_set_response_sensitive(dialog, GTK_RESPONSE_APPLY, n > 0);
}

static void
update_all_curves(GwyToolCprofile *tool)
{
    GwyPlainTool *plain_tool = GWY_PLAIN_TOOL(tool);
    GwyNullStore *store = GWY_NULL_STORE(tool->model);
    gint n, i, nstore;

    if (!plain_tool->selection || !(n = gwy_selection_get_data(plain_tool->selection, NULL))) {
        gwy_graph_model_remove_all_curves(tool->gmodel);
        return;
    }

    nstore = gwy_null_store_get_n_rows(store);
    for (i = 0; i < n; i++) {
        update_curve(tool, i);
        if (i < nstore)
            gwy_null_store_row_changed(store, i);
    }
    gwy_null_store_set_n_rows(store, n);
}

/* See libgwyddion/internal.h */
static inline gint
nielded_included(const gint *m, GwyMaskingType masking)
{
    if (masking == GWY_MASK_INCLUDE)
        return *m > 0;
    if (masking == GWY_MASK_EXCLUDE)
        return *m <= 0;
    return 1;
}

static void
extract_column_profile(GwyField *dfield,
                       GwyNield *mask, GwyMaskingType masking,
                       GArray *xydata, gint col, gint thickness)
{
    gint xres = gwy_field_get_xres(dfield), yres = gwy_field_get_yres(dfield);
    gdouble dy = gwy_field_get_dy(dfield);
    const gdouble *d = gwy_field_get_data_const(dfield);

    g_array_set_size(xydata, 0);

    gint jfrom = col - (thickness - 1)/2;
    jfrom = MAX(jfrom, 0);
    gint jto = col + thickness/2 + 1;
    jto = MIN(jto, xres);

    const gint *m = (mask ? gwy_nield_get_data_const(mask) : NULL);
    for (gint i = 0; i < yres; i++) {
        const gdouble *drow = d + i*xres + jfrom;
        gdouble z = 0.0;
        gint count = 0;
        if (m) {
            const gint *mrow = m + i*xres + jfrom;
            count = 0;
            for (gint j = 0; j < jto - jfrom; j++) {
                if (nielded_included(mrow + j, masking)) {
                    z += drow[j];
                    count++;
                }
            }
        }
        else {
            count = jto - jfrom;
            for (gint j = 0; j < count; j++)
                z += drow[j];
        }

        if (count) {
            GwyXY xy = { .x = dy*i, .y = z/count };
            g_array_append_val(xydata, xy);
        }
    }
}

static void
extract_row_profile(GwyField *dfield,
                    GwyNield *mask, GwyMaskingType masking,
                    GArray *xydata, gint row, gint thickness)
{
    gint xres = gwy_field_get_xres(dfield), yres = gwy_field_get_yres(dfield);
    gdouble dx = gwy_field_get_dx(dfield);
    const gdouble *d = gwy_field_get_data_const(dfield);

    g_array_set_size(xydata, xres);
    GwyXY *xy = &g_array_index(xydata, GwyXY, 0);
    gwy_clear(xy, xres);

    gint ifrom = row - (thickness - 1)/2;
    ifrom = MAX(ifrom, 0);
    gint ito = row + thickness/2 + 1;
    ito = MIN(ito, yres);

    const gint *m = (mask ? gwy_nield_get_data_const(mask) : NULL);
    if (!mask) {
        for (gint j = 0; j < xres; j++)
            xy[j].x = ito - ifrom;
    }

    for (gint i = ifrom; i < ito; i++) {
        const gdouble *drow = d + i*xres;
        if (m) {
            const gint *mrow = m + i*xres;
            for (gint j = 0; j < xres; j++) {
                if (nielded_included(mrow + j, masking)) {
                    xy[j].y += drow[j];
                    xy[j].x += 1.0;
                }
            }
        }
        else {
            for (gint j = 0; j < xres; j++)
                xy[j].y += drow[j];
        }
    }

    gint n = 0;
    for (gint j = 0; j < xres; j++) {
        if (xy[j].x > 0.0) {
            xy[n].y = xy[j].y/xy[j].x;
            xy[n].x = dx*j;
            n++;
        }
    }
    g_array_set_size(xydata, n);
}

static void
add_x_offset(GArray *xydata, gdouble offset)
{
    guint i, n;

    n = xydata->len;
    for (i = 0; i < n; i++)
        g_array_index(xydata, GwyXY, i).x += offset;
}

static void
update_one_curve(GwyToolCprofile *tool, gint i, gint id, gboolean is_vert)
{
    GwyGraphCurveModel *gcmodel;
    GwyField *field;
    const GwyRGBA *color;
    GArray *xydata;
    gchar *desc;
    gint n;

    xydata = tool->xydata;

    n = gwy_graph_model_get_n_curves(tool->gmodel);
    if (i < n) {
        gcmodel = gwy_graph_model_get_curve(tool->gmodel, i);
        gwy_graph_curve_model_set_data_interleaved(gcmodel, (gdouble*)xydata->data, xydata->len);
        return;
    }

    gcmodel = gwy_graph_curve_model_new();
    if (is_vert)
        desc = g_strdup_printf(_("Vertical profile %d"), id);
    else
        desc = g_strdup_printf(_("Horizontal profile %d"), id);
    color = gwy_graph_get_preset_color(i);
    g_object_set(gcmodel,
                 "mode", GWY_GRAPH_CURVE_LINE,
                 "description", desc,
                 "color", color,
                 NULL);
    g_free(desc);
    gwy_graph_model_add_curve(tool->gmodel, gcmodel);
    gwy_graph_curve_model_set_data_interleaved(gcmodel, (gdouble*)xydata->data, xydata->len);
    g_object_unref(gcmodel);

    if (i == 0) {
        field = GWY_PLAIN_TOOL(tool)->field;
        gwy_graph_model_set_units_from_field(tool->gmodel, field, 1, 0, 0, 1);
        gwy_param_table_data_id_refilter(tool->table, PARAM_TARGET_GRAPH);
    }
}

static void
update_curve(GwyToolCprofile *tool, gint i)
{
    GwyPlainTool *plain_tool = GWY_PLAIN_TOOL(tool);
    GwyField *field = plain_tool->field;
    GwyNield *mask = plain_tool->mask_field;
    GwyDirectionFlags mode = gwy_params_get_enum(tool->params, PARAM_MODE);
    gboolean zero_cross = gwy_params_get_boolean(tool->params, PARAM_ZERO_CROSS);
    gint thickness = gwy_params_get_int(tool->params, PARAM_THICKNESS);
    GwyMaskingType masking = gwy_params_get_masking(tool->params, PARAM_MASKING, &mask);
    gdouble xy[2];
    gint col, row;
    GArray *xydata;

    g_return_if_fail(plain_tool->selection);
    if (!gwy_selection_get_object(plain_tool->selection, i, xy))
        g_return_if_reached();

    if (!tool->xydata)
        tool->xydata = g_array_new(FALSE, FALSE, sizeof(GwyXY));
    xydata = tool->xydata;

    col = gwy_field_rtoj(field, xy[0]);
    row = gwy_field_rtoi(field, xy[1]);
    if (mode & GWY_DIRECTION_VERTICAL) {
        extract_row_profile(field, mask, masking, xydata, row, thickness);
        add_x_offset(xydata, zero_cross ? -xy[0] : gwy_field_get_xoffset(field));
        update_one_curve(tool, (mode & GWY_DIRECTION_HORIZONTAL) ? 2*i + 0 : i, i+1, FALSE);
    }
    if (mode & GWY_DIRECTION_HORIZONTAL) {
        extract_column_profile(field, mask, masking, xydata, col, thickness);
        add_x_offset(xydata, zero_cross ? -xy[1] : gwy_field_get_yoffset(field));
        update_one_curve(tool, (mode & GWY_DIRECTION_VERTICAL) ? 2*i + 1 : i, i+1, TRUE);
    }
}

static void
gwy_tool_cprofile_apply(GwyToolCprofile *tool)
{
    GwyPlainTool *plain_tool = GWY_PLAIN_TOOL(tool);

    g_return_if_fail(plain_tool->selection);
    gint n = gwy_selection_get_data(plain_tool->selection, NULL);
    g_return_if_fail(n);

    GwyGraphModel *gmodel;
    if ((gmodel = gwy_params_get_graph(tool->params, PARAM_TARGET_GRAPH))) {
        gwy_graph_model_append_curves(gmodel, tool->gmodel, 1);
        return;
    }

    gmodel = gwy_graph_model_copy(tool->gmodel);
    g_object_set(gmodel, "label-visible", TRUE, NULL);
    gint newid = gwy_file_add_graph(GWY_FILE(plain_tool->container), gmodel);
    gwy_file_set_visible(GWY_FILE(plain_tool->container), GWY_FILE_GRAPH, newid, TRUE);
    g_object_unref(gmodel);
}

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