/*
 *  $Id: fit-table.c 30028 2026-06-09 13:45:30Z yeti-dn $
 *  Copyright (C) 2025 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 <string.h>
#include <glib/gi18n-lib.h>
#include <gtk/gtk.h>

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

#include "libgwyui/fit-table.h"
#include "libgwyui/utils.h"

/* FIXME: Reverse dependence, change sanity.h to normal API and get rid of parts of it, as needed. */

enum {
    COLUMN_CHECK,
    COLUMN_NAME,
    COLUMN_EQUALS,
    COLUMN_VALUE,
    COLUMN_VALUE_UNIT,
    COLUMN_PM,
    COLUMN_ERROR,
    COLUMN_ERROR_UNIT,
    NUM_COLUMNS
};

enum {
    SGNL_CHANGED,
    SGNL_ACTIVATED,
    SGNL_TOGGLED,
    NUM_SIGNALS
};

typedef struct {
    GtkWidget *widgets[NUM_COLUMNS];
    GwyUnit *unit;
    gdouble value;
    gdouble old_value;
    gdouble error;
    gdouble magnitude;
    gulong toggled_id;
    gulong changed_id;
    GwyNLFitParamFlags flags;
    gboolean checked : 1;
    gboolean edited : 1;
    gboolean error_set : 1;
    gboolean value_hidden : 1;
    gboolean sensitive : 1;
} ValueRow;

struct _GwyFitTablePrivate {
    GArray *rows;
    GwyValueFormat *vf;
    GString *str;
    gboolean values_editable;
    gboolean has_checkboxes;
    guint nheaders;
};

static void      finalize               (GObject *object);
static void      format_value           (GwyFitTable *table,
                                         guint i);
static void      format_error           (GwyFitTable *table,
                                         guint i);
static void      checkbox_toggled       (GwyFitTable *table,
                                         GtkToggleButton *toggle);
static void      value_edited           (GwyFitTable *table,
                                         GtkEntry *entry);
static void      value_activated        (GwyFitTable *table,
                                         GtkEntry *entry);
static ValueRow* get_row                (GwyFitTable *table,
                                         guint i);
static void      destroy_widget         (GwyFitTable *table,
                                         guint i,
                                         guint col);
static void      add_checkbox_to_row    (GwyFitTable *table,
                                         guint i);
static void      add_value_widget_to_row(GwyFitTable *table,
                                         guint i,
                                         gboolean is_entry);
static void      update_sensitivity     (ValueRow *row);

static guint signals[NUM_SIGNALS];
static GtkGridClass *parent_class = NULL;

static G_DEFINE_QUARK(gwy-row-id, row_id)

G_DEFINE_TYPE_WITH_CODE(GwyFitTable, gwy_fit_table, GTK_TYPE_GRID,
                        G_ADD_PRIVATE(GwyFitTable))

static void
gwy_fit_table_class_init(GwyFitTableClass *klass)
{
    GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
    GType type = G_TYPE_FROM_CLASS(klass);

    parent_class = gwy_fit_table_parent_class;

    gobject_class->finalize = finalize;

    /**
     * GwyFitTable::changed:
     * @gwyfittable: The #GwyFitTable which received the signal.
     * @arg1: Index of the changed row.
     *
     * The ::changed signal is emitted whenever the user edits a value entry (not necessarily to a valid number). It
     * can only be emitted when values are edtiable and is not generally emitted on programmatic updates.
     **/
    signals[SGNL_CHANGED] = g_signal_new("changed", type,
                                         G_SIGNAL_RUN_FIRST,
                                         G_STRUCT_OFFSET(GwyFitTableClass, changed),
                                         NULL, NULL,
                                         g_cclosure_marshal_VOID__INT,
                                         G_TYPE_NONE, 1, G_TYPE_INT);
    g_signal_set_va_marshaller(signals[SGNL_CHANGED], type, g_cclosure_marshal_VOID__INTv);

    /**
     * GwyFitTable::activated:
     * @gwyfittable: The #GwyFitTable which received the signal.
     * @arg1: Index of the activated row.
     *
     * The ::activated signal is emitted whenever a value entry is activated (after parsing the value). It can only be
     * emitted when values are edtiable.
     **/
    signals[SGNL_ACTIVATED] = g_signal_new("activated", type,
                                           G_SIGNAL_RUN_FIRST,
                                           G_STRUCT_OFFSET(GwyFitTableClass, activated),
                                           NULL, NULL,
                                           g_cclosure_marshal_VOID__INT,
                                           G_TYPE_NONE, 1, G_TYPE_INT);
    g_signal_set_va_marshaller(signals[SGNL_ACTIVATED], type, g_cclosure_marshal_VOID__INTv);

    /**
     * GwyFitTable::toggled:
     * @gwyfittable: The #GwyFitTable which received the signal.
     * @arg1: Index of the toggled row.
     *
     * The ::toggled signal is emitted whenever the row checkox is toggled. It can only be emitted when rows have
     * checkboxes and is not generally emitted on programmatic changes.
     **/
    signals[SGNL_TOGGLED] = g_signal_new("toggled", type,
                                         G_SIGNAL_RUN_FIRST,
                                         G_STRUCT_OFFSET(GwyFitTableClass, toggled),
                                         NULL, NULL,
                                         g_cclosure_marshal_VOID__INT,
                                         G_TYPE_NONE, 1, G_TYPE_INT);
    g_signal_set_va_marshaller(signals[SGNL_TOGGLED], type, g_cclosure_marshal_VOID__INTv);
}

static void
free_value_row(gpointer user_data)
{
    ValueRow *row = (ValueRow*)user_data;

    g_clear_object(&row->unit);
}

static void
gwy_fit_table_init(GwyFitTable *table)
{
    GwyFitTablePrivate *priv;

    gtk_grid_set_row_spacing(GTK_GRID(table), 2);

    priv = table->priv = gwy_fit_table_get_instance_private(table);

    /* Do not set any spacings on the grid. We might leave row and/or column 0 empty, and do not want any spacing
     * around them. */
    priv->rows = g_array_new(FALSE, FALSE, sizeof(ValueRow));
    g_array_set_clear_func(priv->rows, free_value_row);
    priv->vf = gwy_value_format_new(1.0, 5.0, NULL);
    priv->str = g_string_new(NULL);
}

static void
finalize(GObject *object)
{
    GwyFitTable *table = GWY_FIT_TABLE(object);
    GwyFitTablePrivate *priv = table->priv;

    gwy_fit_table_resize(table, 0);
    gwy_value_format_free(priv->vf);
    g_string_free(priv->str, TRUE);
    g_array_free(priv->rows, TRUE);

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

/**
 * gwy_fit_table_new:
 *
 * Creates a new table of fit results.
 *
 * Returns: New fit results table widget.
 **/
GtkWidget*
gwy_fit_table_new(void)
{
    return gtk_widget_new(GWY_TYPE_FIT_TABLE, NULL);
}

/**
 * gwy_fit_table_set_has_checkboxes:
 * @table: A table of fit results.
 * @setting: %TRUE if each row should have a checkbox; %FALSE if it should not.
 *
 * Sets whether rows in a table of fit results have checkboxes.
 **/
void
gwy_fit_table_set_has_checkboxes(GwyFitTable *table,
                                 gboolean setting)
{
    g_return_if_fail(GWY_IS_FIT_TABLE(table));
    GwyFitTablePrivate *priv = table->priv;
    if (!setting == !priv->has_checkboxes)
        return;

    priv->has_checkboxes = setting;
    guint n = priv->rows->len;
    for (guint i = 0; i < n; i++) {
        if (setting)
            add_checkbox_to_row(table, i);
        else {
            destroy_widget(table, i, COLUMN_CHECK);
            g_array_index(priv->rows, ValueRow, i).toggled_id = 0;
        }
    }
}

/**
 * gwy_fit_table_get_has_checkboxes:
 * @table: A table of fit results.
 *
 * Reports whether rows in a table of fit results have checkboxes.
 *
 * Returns: %TRUE if each row has a checkbox; %FALSE if it does not.
 **/
gboolean
gwy_fit_table_get_has_checkboxes(GwyFitTable *table)
{
    g_return_val_if_fail(GWY_IS_FIT_TABLE(table), FALSE);
    return table->priv->has_checkboxes;
}

/**
 * gwy_fit_table_set_values_editable:
 * @table: A table of fit results.
 * @setting: %TRUE if values should editable entries; %FALSE if they are only displayed.
 *
 * Sets whether the values in a table of fit results are editable.
 **/
void
gwy_fit_table_set_values_editable(GwyFitTable *table,
                                  gboolean setting)
{
    g_return_if_fail(GWY_IS_FIT_TABLE(table));
    GwyFitTablePrivate *priv = table->priv;
    if (!setting == !priv->values_editable)
        return;

    priv->values_editable = setting;
    guint n = priv->rows->len;
    for (guint i = 0; i < n; i++) {
        destroy_widget(table, i, COLUMN_VALUE);
        ValueRow *row = &g_array_index(priv->rows, ValueRow, i);
        row->changed_id = 0;
        if (setting)
            row->value_hidden = FALSE;
        add_value_widget_to_row(table, i, setting);
        format_value(table, i);
    }
}

/**
 * gwy_fit_table_get_values_editable:
 * @table: A table of fit results.
 *
 * Reports whether the values in a table of fit results are editable.
 *
 * Returns: %TRUE if values are editable entries; %FALSE if they are only displayed.
 **/
gboolean
gwy_fit_table_get_values_editable(GwyFitTable *table)
{
    g_return_val_if_fail(GWY_IS_FIT_TABLE(table), FALSE);
    return table->priv->values_editable;
}

/**
 * gwy_fit_table_resize:
 * @table: A table of fit results.
 * @n: New number of parameters.
 *
 * Resizes a table of fit results.
 *
 * If the table shrinks then no further setup is necessary, in principle. If it expands, you should usually set row
 * labels, and possibly values and errors (which are created unset). Note that row sensitivity is not reset (you can
 * use gwy_fit_table_reset_sensitivity()). Typically, this function is used when the fit model changes, so all rows
 * are set up afterwards.
 **/
void
gwy_fit_table_resize(GwyFitTable *table,
                     guint n)
{
    g_return_if_fail(GWY_IS_FIT_TABLE(table));
    GwyFitTablePrivate *priv = table->priv;

    GtkGrid *grid = GTK_GRID(table);
    GArray *rows = priv->rows;
    guint row_offset = !!priv->nheaders;
    if (n < rows->len) {
        for (guint i = rows->len; i > n; i--)
            gtk_grid_remove_row(grid, i+row_offset - 1);
        g_array_remove_range(rows, n, rows->len - n);
    }
    while (rows->len < n) {
        const guint left_aligned[] = { COLUMN_NAME, COLUMN_VALUE_UNIT, COLUMN_ERROR_UNIT };
        const guint right_aligned[] = { COLUMN_ERROR };
        const guint symbols[] = { COLUMN_EQUALS, COLUMN_PM };

        ValueRow row;
        gwy_clear1(row);
        row.unit = gwy_unit_new(NULL);
        row.magnitude = 1.0;
        row.sensitive = TRUE;
        row.widgets[COLUMN_NAME] = gtk_label_new(NULL);
        row.widgets[COLUMN_EQUALS] = gtk_label_new("=");
        row.widgets[COLUMN_VALUE_UNIT] = gtk_label_new(NULL);
        row.widgets[COLUMN_PM] = gtk_label_new("±");
        row.widgets[COLUMN_ERROR] = gtk_label_new(NULL);
        row.widgets[COLUMN_ERROR_UNIT] = gtk_label_new(NULL);
        for (guint i = 0; i < G_N_ELEMENTS(left_aligned); i++)
            gtk_label_set_xalign(GTK_LABEL(row.widgets[left_aligned[i]]), 0.0);
        for (guint i = 0; i < G_N_ELEMENTS(right_aligned); i++)
            gtk_label_set_xalign(GTK_LABEL(row.widgets[right_aligned[i]]), 1.0);
        for (guint i = 0; i < G_N_ELEMENTS(symbols); i++) {
            gwy_set_widget_margins(row.widgets[symbols[i]], 4, 4, -1, -1);
            gtk_widget_set_hexpand(row.widgets[symbols[i]], FALSE);
        }
        gwy_set_widget_margins(row.widgets[COLUMN_VALUE_UNIT], 4, -1, -1, -1);
        gwy_set_widget_margins(row.widgets[COLUMN_ERROR_UNIT], 4, -1, -1, -1);
        for (guint i = 0; i < NUM_COLUMNS; i++) {
            if (row.widgets[i]) {
                gtk_grid_attach(grid, row.widgets[i], i, rows->len + row_offset, 1, 1);
                gtk_widget_show(row.widgets[i]);
            }
        }

        g_array_append_val(rows, row);

        guint i = rows->len - 1;
        add_value_widget_to_row(table, i, priv->values_editable);
        if (priv->has_checkboxes)
            add_checkbox_to_row(table, i);

        /* This will give the value of 0 and empty error. */
        format_value(table, i);
        format_error(table, i);
    }
}

/* FIXME: This is kind of stupid because the caller needs to know what columns are there internally. */
/**
 * gwy_fit_table_set_header:
 * @table: A table of fit results.
 * @markup: Header label as valid Pango markup.
 * @col: The first column of the header label.
 * @width: The column span of the header label.
 *
 * Sets a header label in a table of fit results.
 *
 * An existing header starting at the same column is replaced. If you try to create overlapping headers the behaviour
 * is undefined.
 **/
void
gwy_fit_table_set_header(GwyFitTable *table,
                         const gchar *markup,
                         guint col,
                         guint width)
{
    g_return_if_fail(GWY_IS_FIT_TABLE(table));
    GwyFitTablePrivate *priv = table->priv;

    if (priv->nheaders) {
        GtkWidget *old = gtk_grid_get_child_at(GTK_GRID(table), col, 0);
        if (old) {
            priv->nheaders--;
            gtk_widget_destroy(old);
        }
    }
    else {
        gtk_grid_insert_row(GTK_GRID(table), 0);
    }

    gtk_grid_attach(GTK_GRID(table), gwy_label_new_header(markup), col, 0, width, 1);
    priv->nheaders++;
}

/**
 * gwy_fit_table_set_name:
 * @table: A table of fit results.
 * @i: Row index in the table.
 * @markup: Parameter name as valid Pango markup.
 *
 * Sets the name of a parameter in a table of fit results.
 **/
void
gwy_fit_table_set_name(GwyFitTable *table,
                       guint i,
                       const gchar *markup)
{
    ValueRow *row;
    if (!(row = get_row(table, i)))
        return;
    gtk_label_set_markup(GTK_LABEL(row->widgets[COLUMN_NAME]), markup);
}

/**
 * gwy_fit_table_set_tooltip:
 * @table: A table of fit results.
 * @i: Row index in the table.
 * @markup: Parameter tooltip as valid Pango markup.
 *
 * Sets the tooltip of a parameter in a table of fit results.
 **/
void
gwy_fit_table_set_tooltip(GwyFitTable *table,
                          guint i,
                          const gchar *markup)
{
    ValueRow *row;
    if (!(row = get_row(table, i)))
        return;

    gtk_widget_set_tooltip_markup(row->widgets[COLUMN_NAME], markup);
    gtk_widget_set_tooltip_markup(row->widgets[COLUMN_EQUALS], markup);
    gtk_widget_set_tooltip_markup(row->widgets[COLUMN_VALUE], markup);
}

/**
 * gwy_fit_table_set_unit:
 * @table: A table of fit results.
 * @i: Row index in the table.
 * @unit: New parameter unit. It is passed by value. The object is not kept.
 *
 * Sets the unit of a parameter in a table of fit results.
 *
 * Setting the unit generally causes reformatting of the value and error (in addition to the unit labels).
 **/
void
gwy_fit_table_set_unit(GwyFitTable *table,
                       guint i,
                       GwyUnit *unit)
{
    ValueRow *row;
    if (!(row = get_row(table, i)))
        return;

    if (gwy_unit_equal(unit, row->unit))
        return;

    gwy_unit_assign(row->unit, unit);
    format_value(table, i);
    format_error(table, i);
}

/**
 * gwy_fit_table_set_flags:
 * @table: A table of fit results.
 * @i: Row index in the table.
 * @flags: Parameter flags.
 *
 * Sets the flags for a parameter in a table of fit results.
 **/
void
gwy_fit_table_set_flags(GwyFitTable *table,
                        guint i,
                        GwyNLFitParamFlags flags)
{
    ValueRow *row;
    if (!(row = get_row(table, i)))
        return;

    if (flags == row->flags)
        return;

    row->flags = flags;
    format_value(table, i);
    format_error(table, i);
}

/**
 * gwy_fit_table_set_value:
 * @table: A table of fit results.
 * @i: Row index in the table.
 * @value: The value of @i-th parameter.
 *
 * Sets the value of a parameter in a table of fit results.
 **/
void
gwy_fit_table_set_value(GwyFitTable *table,
                        guint i,
                        gdouble value)
{
    ValueRow *row;
    if (!(row = get_row(table, i)))
        return;

    /* Enforce absolute value, which is a property, but ignore angle, which is a user-internal transformation. */
    if (row->flags & GWY_NLFIT_PARAM_ABSVAL)
        value = fabs(value);

    row->edited = FALSE;
    row->value_hidden = FALSE;
    row->old_value = value;
    if (value == row->value)
        return;
    row->value = value;
    format_value(table, i);
}

/**
 * gwy_fit_table_set_error:
 * @table: A table of fit results.
 * @i: Row index in the table.
 * @error: The value of @i-th parameter.
 *
 * Sets the error of a parameter in a table of fit results.
 *
 * Use gwy_fit_table_clear_error() to unset the error.
 **/
void
gwy_fit_table_set_error(GwyFitTable *table,
                        guint i,
                        gdouble error)
{
    ValueRow *row;
    if (!(row = get_row(table, i)))
        return;

    error = fabs(error);
    if (row->error_set && error == row->error)
        return;
    row->error = error;
    row->error_set = TRUE;
    format_error(table, i);
    /* Adjust precision to the error, if possible. */
    format_value(table, i);
}

/**
 * gwy_fit_table_clear_error:
 * @table: A table of fit results.
 * @i: Row index in the table.
 *
 * Clears the error of a parameter in a table of fit results.
 *
 * Claring the error means no error is displayed, for instance to indicate the parameter has not yet been fitted.
 * Use gwy_fit_table_set_error() to display an error.
 *
 * Use gwy_fit_table_clear_errors() to clear all errors.
 **/
void
gwy_fit_table_clear_error(GwyFitTable *table,
                          guint i)
{
    g_return_if_fail(GWY_IS_FIT_TABLE(table));
    ValueRow *row;
    if (!(row = get_row(table, i)))
        return;

    if (!row->error_set)
        return;
    row->error_set = FALSE;
    format_error(table, i);
}

/**
 * gwy_fit_table_clear_errors:
 * @table: A table of fit results.
 *
 * Clears the errors of all parameter in a table of fit results.
 *
 * See gwy_fit_table_clear_error() for clearing invididual errors and discussion.
 **/
void
gwy_fit_table_clear_errors(GwyFitTable *table)
{
    g_return_if_fail(GWY_IS_FIT_TABLE(table));
    GwyFitTablePrivate *priv = table->priv;
    GArray *rows = priv->rows;

    for (guint i = 0; i < rows->len; i++) {
        ValueRow *row = &g_array_index(rows, ValueRow, i);
        if (row->error_set) {
            row->error_set = FALSE;
            format_error(table, i);
        }
    }
}

/**
 * gwy_fit_table_error_is_set:
 * @table: A table of fit results.
 * @i: Row index in the table.
 *
 * Reports whether an error in a table of fit results is set or cleared.
 *
 * Returns: %TRUE if the error is set (and displayed), %FALSE when it is cleared (and hidden).
 **/
gboolean
gwy_fit_table_error_is_set(GwyFitTable *table,
                           guint i)
{
    g_return_val_if_fail(GWY_IS_FIT_TABLE(table), FALSE);
    ValueRow *row;
    if (!(row = get_row(table, i)))
        return FALSE;
    return !!row->error_set;
}

/**
 * gwy_fit_table_clear_value:
 * @table: A table of fit results.
 * @i: Row index in the table.
 *
 * Clears the value of a parameter in a table of fit results.
 *
 * Claring the value means no value is displayed, for instance to indicate the parameter has not yet been initialised
 * in any manner. The stored value remains unchanged. Use gwy_fit_table_set_value() to display a value.
 *
 * Only passively displayed values, i.e. those not set editable using gwy_fit_table_set_values_editable(), can be
 * cleared.
 *
 * Use gwy_fit_table_clear_values() to clear all values.
 **/
void
gwy_fit_table_clear_value(GwyFitTable *table,
                          guint i)
{
    g_return_if_fail(GWY_IS_FIT_TABLE(table));
    g_return_if_fail(!table->priv->values_editable);
    ValueRow *row;
    if (!(row = get_row(table, i)))
        return;

    if (row->value_hidden)
        return;
    row->value_hidden = TRUE;
    format_value(table, i);
}

/**
 * gwy_fit_table_clear_values:
 * @table: A table of fit results.
 *
 * Clears the values of all parameters in a table of fit results.
 *
 * See gwy_fit_table_clear_value() for clearing invididual values and discussion.
 **/
void
gwy_fit_table_clear_values(GwyFitTable *table)
{
    g_return_if_fail(GWY_IS_FIT_TABLE(table));
    GwyFitTablePrivate *priv = table->priv;
    g_return_if_fail(!priv->values_editable);
    GArray *rows = priv->rows;

    for (guint i = 0; i < rows->len; i++) {
        ValueRow *row = &g_array_index(rows, ValueRow, i);
        if (!row->value_hidden) {
            row->value_hidden = TRUE;
            format_value(table, i);
        }
    }
}

/**
 * gwy_fit_table_value_is_set:
 * @table: A table of fit results.
 * @i: Row index in the table.
 *
 * Reports whether an value in a table of fit results is set or cleared.
 *
 * Returns: %TRUE if the value is set (and displayed), %FALSE when it is cleared (and hidden).
 **/
gboolean
gwy_fit_table_value_is_set(GwyFitTable *table,
                           guint i)
{
    g_return_val_if_fail(GWY_IS_FIT_TABLE(table), FALSE);
    ValueRow *row;
    if (!(row = get_row(table, i)))
        return FALSE;
    return !row->value_hidden;
}

/**
 * gwy_fit_table_revert_values:
 * @table: A table of fit results.
 *
 * Reverts the values of all parameter in a table of fit results.
 *
 * The values are set to the last programmatic update (done using gwy_fit_table_set_value()), discarding all user
 * modifications.
 **/
void
gwy_fit_table_revert_values(GwyFitTable *table)
{
    g_return_if_fail(GWY_IS_FIT_TABLE(table));
    GwyFitTablePrivate *priv = table->priv;
    GArray *rows = priv->rows;

    for (guint i = 0; i < rows->len; i++) {
        ValueRow *row = &g_array_index(rows, ValueRow, i);
        if (row->edited) {
            row->edited = FALSE;
            row->value = row->old_value;
            format_value(table, i);
        }
    }
}

/**
 * gwy_fit_table_set_checked:
 * @table: A table of fit results.
 * @i: Row index in the table.
 * @checked: %TRUE to check the checkbox; %FALSE to uncheck it.
 *
 * Sets the whether the checkox of a parameter in a table of fit results is checked.
 *
 * The state is remebmered even when no checkboxes are actually shown.
 **/
void
gwy_fit_table_set_checked(GwyFitTable *table,
                          guint i,
                          gboolean checked)
{
    g_return_if_fail(GWY_IS_FIT_TABLE(table));
    ValueRow *row;
    if (!(row = get_row(table, i)))
        return;

    if (!checked == !row->checked)
        return;
    row->checked = checked;

    GtkWidget *check = row->widgets[COLUMN_CHECK];
    if (check) {
        g_signal_handler_block(check, row->toggled_id);
        gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(check), row->checked);
        g_signal_handler_unblock(check, row->toggled_id);
    }
}

/**
 * gwy_fit_table_get_value:
 * @table: A table of fit results.
 * @i: Row index in the table.
 *
 * Gets the value of a parameter in a table of fit results.
 *
 * The value is only updated when the value entry is activated or loses focus (and on programmatic updates).
 *
 * See also gwy_fit_table_gather() for getting information about all parameters at once.
 *
 * Returns: The parsed parameter value.
 **/
gdouble
gwy_fit_table_get_value(GwyFitTable *table,
                        guint i)
{
    g_return_val_if_fail(GWY_IS_FIT_TABLE(table), 0.0);
    ValueRow *row;
    if (!(row = get_row(table, i)))
        return 0.0;

    return row->value;
}

/**
 * gwy_fit_table_get_error:
 * @table: A table of fit results.
 * @i: Row index in the table.
 *
 * Gets the error of a parameter in a table of fit results.
 *
 * The error is always whatever was set using the last gwy_fit_table_set_error() for the paramters. Clearing an error
 * does not change the value. This functions exists in case you do not want to keep around an array of errors
 * yourself.
 *
 * See also gwy_fit_table_gather() for getting information about all parameters at once.
 *
 * Returns: The parameter error.
 **/
gdouble
gwy_fit_table_get_error(GwyFitTable *table,
                        guint i)
{
    g_return_val_if_fail(GWY_IS_FIT_TABLE(table), 0.0);
    ValueRow *row;
    if (!(row = get_row(table, i)))
        return 0.0;

    return row->error;
}

/**
 * gwy_fit_table_get_checked:
 * @table: A table of fit results.
 * @i: Row index in the table.
 *
 * Gets the whether the checkbox of parameter in a table of fit results is checked.
 *
 * The checked state is remembered even when checkboxes are hidden.
 *
 * See also gwy_fit_table_gather() for getting information about all parameters at once.
 *
 * Returns: %TRUE if the checkbox is checked.
 **/
gboolean
gwy_fit_table_get_checked(GwyFitTable *table,
                          guint i)
{
    g_return_val_if_fail(GWY_IS_FIT_TABLE(table), FALSE);
    ValueRow *row;
    if (!(row = get_row(table, i)))
        return FALSE;

    /* Normalise the boolean to some non-(-1) form. */
    return !!row->checked;
}

/**
 * gwy_fit_table_set_sensitive:
 * @table: A table of fit results.
 * @i: Row index in the table.
 * @setting: %TRUE to make the row sensitive, %FALSE to make it insensitive.
 *
 * Sets the sensitivity of a row in a table of fit results.
 *
 * This function should be rarely needed and probably only makes sense when the parameters are editable.
 **/
void
gwy_fit_table_set_sensitive(GwyFitTable *table,
                            guint i,
                            gboolean setting)
{
    g_return_if_fail(GWY_IS_FIT_TABLE(table));
    ValueRow *row;
    if (!(row = get_row(table, i)))
        return;
    if (!row->sensitive == !setting)
        return;
    row->sensitive = !!setting;
    update_sensitivity(row);
}

/**
 * gwy_fit_table_reset_sensitivity:
 * @table: A table of fit results.
 *
 * Resets the sensitivity of all rows in a table of fit results.
 *
 * All rows are made sensitive, as is the default. This function should be rarely needed and probably only makes sense
 * when the parameters are editable.
 **/
void
gwy_fit_table_reset_sensitivity(GwyFitTable *table)
{
    g_return_if_fail(GWY_IS_FIT_TABLE(table));
    GwyFitTablePrivate *priv = table->priv;
    GArray *rows = priv->rows;

    for (guint i = 0; i < rows->len; i++) {
        ValueRow *row = &g_array_index(rows, ValueRow, i);
        if (!row->sensitive) {
            row->sensitive = TRUE;
            update_sensitivity(row);
        }
    }
}

/**
 * gwy_fit_table_gather:
 * @table: A table of fit results.
 * @values: Array to fill with parameter values, or %NULL.
 * @errors: Array to fill with parameter errors, or %NULL.
 * @checked: Array to fill with whether parameters are checked, or %NULL.
 *
 * Gathers information about all parameters in a table of fit results.
 *
 * All arguments @value, @errors and @checked may be %NULL. Use gwy_fit_table_get_value(), gwy_fit_table_get_error()
 * and gwy_fit_table_get_checked() to obtain individual information.
 *
 * Returns: How many parameters are checked. If you want to know the number of unchecked paramers you need to subtract
 *          the return value from the table size.
 **/
gint
gwy_fit_table_gather(GwyFitTable *table,
                     gdouble *values,
                     gdouble *errors,
                     gboolean *checked)
{
    g_return_val_if_fail(GWY_IS_FIT_TABLE(table), 0);
    guint n = table->priv->rows->len;
    guint nchecked = 0;
    for (guint i = 0; i < n; i++) {
        ValueRow *row = get_row(table, i);
        if (values)
            values[i] = row->value;
        if (errors)
            errors[i] = row->error;
        if (checked)
            checked[i] = !!row->checked;
        nchecked += !!row->checked;
    }
    return nchecked;
}

static gdouble
transform_from_display(ValueRow *row, gdouble value)
{
    if (row->flags & GWY_NLFIT_PARAM_ANGLE)
        value = gwy_deg2rad(value);
    if (row->flags & GWY_NLFIT_PARAM_ABSVAL)
        value = fabs(value);
    return value;
}

static gdouble
transform_to_display(ValueRow *row, gdouble value)
{
    if (row->flags & GWY_NLFIT_PARAM_ANGLE)
        value = gwy_rad2deg(value);
    if (row->flags & GWY_NLFIT_PARAM_ABSVAL)
        value = fabs(value);
    return value;
}

static void
fix_minus(GString *str)
{
    if (str->str[0] != '-')
        return;

    g_string_erase(str, 0, 1);
    g_string_insert(str, 0, "−");
}

static void
format_value(GwyFitTable *table, guint i)
{
    GwyFitTablePrivate *priv = table->priv;
    GwyValueFormat *vf = priv->vf;
    gboolean editable = priv->values_editable;
    ValueRow *row = get_row(table, i);
    gdouble v = transform_to_display(row, row->value);
    GwyUnit *unit = (row->flags & GWY_NLFIT_PARAM_ANGLE ? gwy_unit_new("deg") : g_object_ref(row->unit));
    GtkWidget *widget = row->widgets[COLUMN_VALUE];
    GtkWidget *unit_label = row->widgets[COLUMN_VALUE_UNIT];

    if (row->value_hidden) {
        g_assert(!editable);
        gtk_label_set_text(GTK_LABEL(widget), NULL);
        gtk_label_set_text(GTK_LABEL(unit_label), NULL);
        return;
    }

    /* If the user enters exact zero, do not update the magnitude because that means an unexpected reset to base
     * units. */
    if (G_UNLIKELY(v == 0.0)) {
        gint power10 = GWY_ROUND(log10(row->magnitude));
        gwy_unit_get_format_for_power10(unit, GWY_UNIT_FORMAT_VFMARKUP, power10, vf);
    }
    else if (row->error_set && row->error) {
        /* FIXME: Try harder to format values and errors consistently. Maybe even always format both together. */
        gdouble e = transform_to_display(row, row->error);
        gwy_unit_get_format_with_resolution(unit, GWY_UNIT_FORMAT_VFMARKUP, v, fmin(0.1*e, 0.01*v), vf);
    }
    else {
        gwy_unit_get_format(unit, GWY_UNIT_FORMAT_VFMARKUP, v, vf);
        vf->precision += 3;
    }
    g_object_unref(unit);
    g_string_printf(priv->str, "%.*f", vf->precision, v/vf->magnitude);
    row->magnitude = vf->magnitude;

    if (editable) {
        g_signal_handler_block(widget, row->changed_id);
        gtk_entry_set_text(GTK_ENTRY(widget), priv->str->str);
        g_signal_handler_unblock(widget, row->changed_id);
    }
    else {
        fix_minus(priv->str);
        gtk_label_set_text(GTK_LABEL(widget), priv->str->str);
    }
    gtk_label_set_markup(GTK_LABEL(unit_label), vf->units);
}

static void
format_error(GwyFitTable *table, guint i)
{
    GwyFitTablePrivate *priv = table->priv;
    GwyValueFormat *vf = priv->vf;
    ValueRow *row = get_row(table, i);
    GwyUnit *unit = (row->flags & GWY_NLFIT_PARAM_ANGLE ? gwy_unit_new("deg") : g_object_ref(row->unit));

    if (!row->error_set) {
        gtk_label_set_text(GTK_LABEL(row->widgets[COLUMN_PM]), NULL);
        gtk_label_set_text(GTK_LABEL(row->widgets[COLUMN_ERROR]), NULL);
        gtk_label_set_text(GTK_LABEL(row->widgets[COLUMN_ERROR_UNIT]), NULL);
        return;
    }

    gdouble e = transform_to_display(row, row->error);
    gtk_label_set_text(GTK_LABEL(row->widgets[COLUMN_PM]), "±");
    gwy_unit_get_format_with_digits(unit, GWY_UNIT_FORMAT_VFMARKUP, e, 1, vf);
    g_object_unref(unit);
    gtk_label_set_markup(GTK_LABEL(row->widgets[COLUMN_ERROR_UNIT]), vf->units);
    g_string_printf(priv->str, "%.*f", vf->precision, e/vf->magnitude);
    gtk_label_set_text(GTK_LABEL(row->widgets[COLUMN_ERROR]), priv->str->str);
}

static void
checkbox_toggled(GwyFitTable *table, GtkToggleButton *toggle)
{
    guint i = GPOINTER_TO_UINT(g_object_get_qdata(G_OBJECT(toggle), row_id_quark()));
    ValueRow *row = get_row(table, i);
    row->checked = gtk_toggle_button_get_active(toggle);

    g_signal_emit(table, signals[SGNL_TOGGLED], 0, i);
}

static void
value_edited(GwyFitTable *table, GtkEntry *entry)
{
    guint i = GPOINTER_TO_UINT(g_object_get_qdata(G_OBJECT(entry), row_id_quark()));
    ValueRow *row = get_row(table, i);
    row->edited = TRUE;

    g_signal_emit(table, signals[SGNL_CHANGED], 0, i);
}

static void
value_activated(GwyFitTable *table, GtkEntry *entry)
{
    guint i = GPOINTER_TO_UINT(g_object_get_qdata(G_OBJECT(entry), row_id_quark()));
    ValueRow *row = get_row(table, i);

    row->value = row->magnitude * g_strtod(gtk_entry_get_text(GTK_ENTRY(row->widgets[COLUMN_VALUE])), NULL);
    row->value = transform_from_display(row, row->value),
    format_value(table, i);

    g_signal_emit(table, signals[SGNL_ACTIVATED], 0, i);
}

static ValueRow*
get_row(GwyFitTable *table, guint i)
{
    g_return_val_if_fail(GWY_IS_FIT_TABLE(table), NULL);
    GwyFitTablePrivate *priv = table->priv;
    GArray *rows = priv->rows;
    g_return_val_if_fail(i < rows->len, NULL);
    return &g_array_index(rows, ValueRow, i);
}

static void
destroy_widget(GwyFitTable *table, guint i, guint col)
{
    ValueRow *row;
    if (!(row = get_row(table, i)))
        return;

    g_return_if_fail(row->widgets[col]);
    gtk_widget_destroy(row->widgets[col]);
    row->widgets[col] = NULL;
}

static void
add_checkbox_to_row(GwyFitTable *table, guint i)
{
    ValueRow *row;
    if (!(row = get_row(table, i)))
        return;

    g_return_if_fail(!row->widgets[COLUMN_CHECK]);

    guint row_offset = !!table->priv->nheaders;
    GtkWidget *check = row->widgets[COLUMN_CHECK] = gtk_check_button_new();
    if (!row->sensitive)
        gtk_widget_set_sensitive(check, FALSE);
    g_object_set_qdata(G_OBJECT(check), row_id_quark(), GUINT_TO_POINTER(i));
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(check), row->checked);
    gwy_set_widget_margins(check, 0, 6, 1, 1);
    gtk_grid_attach(GTK_GRID(table), check, COLUMN_CHECK, i + row_offset, 1, 1);
    gtk_widget_show(check);
    row->toggled_id = g_signal_connect_swapped(check, "toggled", G_CALLBACK(checkbox_toggled), table);
}

static void
add_value_widget_to_row(GwyFitTable *table, guint i, gboolean is_entry)
{
    ValueRow *row;
    if (!(row = get_row(table, i)))
        return;

    g_return_if_fail(!row->widgets[COLUMN_VALUE]);

    guint row_offset = !!table->priv->nheaders;
    GtkWidget *widget;
    if (is_entry) {
        widget = gtk_entry_new();
        g_object_set_qdata(G_OBJECT(widget), row_id_quark(), GUINT_TO_POINTER(i));
        gtk_entry_set_width_chars(GTK_ENTRY(widget), 13);
        gtk_widget_set_hexpand(widget, TRUE);
    }
    else {
        widget = gtk_label_new(NULL);
        gtk_label_set_xalign(GTK_LABEL(widget), 1.0);
    }
    row->widgets[COLUMN_VALUE] = widget;
    if (!row->sensitive)
        gtk_widget_set_sensitive(widget, FALSE);
    gtk_grid_attach(GTK_GRID(table), widget, COLUMN_VALUE, i + row_offset, 1, 1);
    gtk_widget_show(widget);
    if (is_entry) {
        row->changed_id = g_signal_connect_swapped(widget, "changed", G_CALLBACK(value_edited), table);
        g_signal_connect_swapped(widget, "activate", G_CALLBACK(value_activated), table);
        gwy_widget_set_activate_on_unfocus(widget, TRUE);
    }
}

static void
update_sensitivity(ValueRow *row)
{
    for (guint i = 0; i < NUM_COLUMNS; i++) {
        if (row->widgets[i])
            gtk_widget_set_sensitive(row->widgets[i], row->sensitive);
    }
}

/**
 * SECTION:fit-table
 * @title: GwyFitTable
 * @short_description: Table of fit results
 *
 * #GwyFitTable can be used either for passive display of least-squares fitting results or for user-settable parameter
 * values. If parameters should be edtiable, use gwy_fit_table_set_values_editable() and often also
 * gwy_fit_table_set_has_checkboxes() to add checkboxes controlling fixed/free parameters. The widget does not assign
 * any meaning to the checkboxes.
 *
 * A fit table keeps track of all parameter values, errors and checkbox states. It can even revert parameters to the
 * last values set programatically, i.e. revert user changes (for example since the last fit). Therefore, it can be
 * used as the primary store if you do not wish to keep a copy of everything.
 **/

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