/*
 *  $Id: linematch.c 30047 2026-06-11 08:30:45Z yeti-dn $
 *  Copyright (C) 2015-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>
#include "libgwyddion/omp.h"
#include "../preview.h"

#define RUN_MODES (GWY_RUN_IMMEDIATE | GWY_RUN_INTERACTIVE)

/* Lower symmetric part indexing; i MUST be greater or equal than j */
#define SLi(a, i, j) a[(i)*((i) + 1)/2 + (j)]

enum {
    MAX_DEGREE = 5,
};

typedef enum {
    LINE_MATCH_POLY         = 0,
    LINE_MATCH_MEDIAN       = 1,
    LINE_MATCH_MEDIAN_DIFF  = 2,
    LINE_MATCH_MODUS        = 3,
    LINE_MATCH_MATCH        = 4,
    LINE_MATCH_TRIMMED_MEAN = 5,
    LINE_MATCH_TMEAN_DIFF   = 6,
    LINE_MATCH_FACET_TILT   = 7,
} LineMatchMethod;

enum {
    PARAM_METHOD,
    PARAM_MASKING,
    PARAM_DIRECTION,
    PARAM_MAX_DEGREE,
    PARAM_DO_EXTRACT,
    PARAM_DO_PLOT,
    PARAM_TRIM_FRACTION,
    PARAM_TARGET_GRAPH,
};

typedef struct {
    GwyParams *params;
    GwyField *field;
    GwyNield *mask;
    GwyField *result;
    GwyField *bg;
    GwyLine *shifts;
} ModuleArgs;

typedef struct {
    ModuleArgs *args;
    GtkWidget *dialog;
    GwyParamTable *table;
    GwyGraphModel *gmodel;
} ModuleGUI;

static gboolean         module_register          (void);
static GwyParamDef*     define_module_params     (void);
static void             module_main              (GwyFile *data,
                                                  GwyRunModeFlags mode);
static void             execute                  (ModuleArgs *args);
static GwyDialogOutcome run_gui                  (ModuleArgs *args,
                                                  GwyFile *data,
                                                  gint id);
static void             param_changed            (ModuleGUI *gui,
                                                  gint id);
static void             preview                  (gpointer user_data);
static void             linematch_do_trimmed_mean(GwyField *field,
                                                  GwyNield *mask,
                                                  GwyLine *shifts,
                                                  GwyMaskingType masking,
                                                  gdouble trim_fraction);
static void             linematch_do_trimmed_diff(GwyField *field,
                                                  GwyNield *mask,
                                                  GwyLine *shifts,
                                                  GwyMaskingType masking,
                                                  gdouble trim_fraction);
static void             linematch_do_modus       (GwyField *field,
                                                  GwyNield *mask,
                                                  GwyLine *shifts,
                                                  GwyMaskingType masking);
static void             linematch_do_match       (GwyField *field,
                                                  GwyNield *mask,
                                                  GwyLine *shifts,
                                                  GwyMaskingType masking);
static void             linematch_do_facet_tilt  (GwyField *field,
                                                  GwyNield *mask,
                                                  GwyLine *shifts,
                                                  GwyMaskingType masking);
static void             zero_level_row_shifts    (GwyLine *shifts);

static const GwyEnum methods[] = {
    { gwy_NC("linematch", "Polynomial"), LINE_MATCH_POLY,         },
    { N_("Median"),                      LINE_MATCH_MEDIAN,       },
    { N_("Median of differences"),       LINE_MATCH_MEDIAN_DIFF,  },
    { N_("Modus"),                       LINE_MATCH_MODUS,        },
    { gwy_NC("linematch", "Matching"),   LINE_MATCH_MATCH,        },
    { N_("Trimmed mean"),                LINE_MATCH_TRIMMED_MEAN, },
    { N_("Trimmed mean of differences"), LINE_MATCH_TMEAN_DIFF,   },
    { N_("Facet-level tilt"),            LINE_MATCH_FACET_TILT,   },
};

static GwyModuleInfo module_info = {
    GWY_MODULE_ABI_VERSION,
    &module_register,
    N_("Aligns rows by various methods."),
    "Yeti <yeti@gwyddion.net>",
    "2.1",
    "David Nečas (Yeti)",
    "2015",
};

GWY_MODULE_QUERY2(module_info, linematch)

static gboolean
module_register(void)
{
    gwy_image_func_register("align_rows",
                            module_main,
                            N_("/_Correct Data/_Align Rows..."),
                            GWY_ICON_LINE_LEVEL,
                            RUN_MODES,
                            GWY_MENU_FLAG_IMAGE,
                            N_("Align rows using various methods"));

    return TRUE;
}

static GwyParamDef*
define_module_params(void)
{
    static GwyParamDef *paramdef = NULL;

    if (paramdef)
        return paramdef;

    paramdef = gwy_param_def_new();
    gwy_param_def_set_function_name(paramdef, "linematch");
    gwy_param_def_add_gwyenum(paramdef, PARAM_METHOD, "method", _("Method"),
                              methods, G_N_ELEMENTS(methods), LINE_MATCH_MEDIAN);
    gwy_param_def_add_enum(paramdef, PARAM_MASKING, "masking", NULL, GWY_TYPE_MASKING_TYPE, GWY_MASK_IGNORE);
    gwy_param_def_add_enum(paramdef, PARAM_DIRECTION, "direction", NULL, GWY_TYPE_ORIENTATION,
                           GWY_ORIENTATION_HORIZONTAL);
    gwy_param_def_add_int(paramdef, PARAM_MAX_DEGREE, "max_degree", _("_Polynomial degree"), 0, MAX_DEGREE, 1);
    gwy_param_def_add_boolean(paramdef, PARAM_DO_EXTRACT, "do_extract", _("E_xtract background"), FALSE);
    gwy_param_def_add_boolean(paramdef, PARAM_DO_PLOT, "do_plot", _("Plot background _graph"), FALSE);
    gwy_param_def_add_double(paramdef, PARAM_TRIM_FRACTION, "trim_fraction", _("_Trim fraction"), 0.0, 0.5, 0.05);
    gwy_param_def_add_target_graph(paramdef, PARAM_TARGET_GRAPH, "target_graph", NULL);
    return paramdef;
}

static void
module_main(GwyFile *data, GwyRunModeFlags mode)
{
    GwyField *field;
    GQuark quark;
    GwyParams *params;
    ModuleArgs args;
    gint id, newid;
    const gchar *methodname;
    gchar *title;

    g_return_if_fail(mode & RUN_MODES);
    gwy_data_browser_get_current(GWY_APP_FIELD_KEY, &quark,
                                 GWY_APP_FIELD, &field,
                                 GWY_APP_MASK_FIELD, &args.mask,
                                 GWY_APP_FIELD_ID, &id,
                                 0);
    g_return_if_fail(field && quark);

    args.field = field;
    args.bg = gwy_field_new_alike(field, FALSE);
    args.shifts = gwy_line_new(field->yres, field->yreal, FALSE);
    gwy_field_copy_units_to_line(field, args.shifts);
    args.params = params = gwy_params_new_from_settings(define_module_params());

    if (mode == GWY_RUN_INTERACTIVE) {
        GwyDialogOutcome outcome = run_gui(&args, data, id);
        gwy_params_save_to_settings(params);
        if (outcome != GWY_DIALOG_HAVE_RESULT)
            goto end;
        gwy_app_undo_qcheckpointv(GWY_DICT(data), 1, &quark);
        gwy_field_copy_data(args.result, field);
    }
    else {
        gwy_app_undo_qcheckpointv(GWY_DICT(data), 1, &quark);
        args.result = g_object_ref(field);
        execute(&args);
    }

    gwy_field_data_changed(field);
    gwy_log_add_full(data, GWY_FILE_IMAGE, id, id, "image::align_rows",
                     "settings-name", "linematch",
                     NULL);

    methodname = gwy_enum_to_string(gwy_params_get_enum(params, PARAM_METHOD), methods, G_N_ELEMENTS(methods));
    methodname = gwy_C(methodname);
    title = g_strdup_printf("%s (%s)", _("Row background"), methodname);

    if (gwy_params_get_boolean(params, PARAM_DO_EXTRACT)) {
        newid = gwy_file_add_image(data, args.bg);
        gwy_file_set_visible(data, GWY_FILE_IMAGE, newid, TRUE);
        gwy_file_sync_items(data, GWY_FILE_IMAGE, id,
                            data, GWY_FILE_IMAGE, newid,
                            GWY_FILE_ITEM_PALETTE, FALSE);
        gwy_file_set_title(data, GWY_FILE_IMAGE, newid, title, TRUE);
        gwy_log_add_full(data, GWY_FILE_IMAGE, id, newid, "image::align_rows",
                         "settings-name", "linematch",
                         NULL);
    }

    if (gwy_params_get_boolean(params, PARAM_DO_PLOT)) {
        GwyGraphModel *gmodel = gwy_graph_model_new();
        GwyGraphCurveModel *gcmodel = gwy_graph_curve_model_new();
        GwyAppDataId target_graph_id = gwy_params_get_data_id(params, PARAM_TARGET_GRAPH);

        gwy_graph_curve_model_set_data_from_line(gcmodel, args.shifts, 0, 0);
        g_object_set(gcmodel,
                     "description", title,
                     "mode", GWY_GRAPH_CURVE_LINE,
                     "color", gwy_graph_get_preset_color(0),
                     NULL);
        gwy_graph_model_add_curve(gmodel, gcmodel);
        g_object_unref(gcmodel);

        g_object_set(gmodel,
                     "title", _("Row background"),
                     "axis-label-bottom", _("Vertical position"),
                     "axis-label-left", _("Corrected offset"),
                     NULL);
        gwy_graph_model_set_units_from_line(gmodel, args.shifts);
        gwy_app_add_graph_or_curves(gmodel, data, &target_graph_id, 1);
        g_object_unref(gmodel);
    }

    g_free(title);

end:
    g_object_unref(params);
    g_clear_object(&args.result);
    g_clear_object(&args.shifts);
    g_clear_object(&args.bg);
}

static GwyDialogOutcome
run_gui(ModuleArgs *args, GwyFile *data, gint id)
{
    GwyDialogOutcome outcome;
    GtkWidget *hbox, *dataview;
    GwyDialog *dialog;
    GwyParamTable *table;
    ModuleGUI gui;

    gwy_clear1(gui);
    gui.args = args;
    /* Create an empty graph model just for compatibility check. */
    gui.gmodel = gwy_graph_model_new();
    gwy_graph_model_set_units_from_field(gui.gmodel, args->field, 1, 0, 0, 1);

    args->result = gwy_field_copy(args->field);

    gui.dialog = gwy_dialog_new(_("Align Rows"));
    dialog = GWY_DIALOG(gui.dialog);
    gwy_dialog_add_stock_buttons(dialog, GWY_STOCK_RESET, GWY_STOCK_CANCEL, GWY_STOCK_OK, 0);

    dataview = gwy_create_preview(args->result, NULL, PREVIEW_SIZE);
    gwy_setup_data_view(GWY_DATA_VIEW(dataview), data, GWY_FILE_IMAGE, id,
                        GWY_FILE_ITEM_PALETTE | GWY_FILE_ITEM_REAL_SQUARE | GWY_FILE_ITEM_COLOR_MAPPING);
    hbox = gwy_create_dialog_preview_hbox(GTK_DIALOG(dialog), GWY_DATA_VIEW(dataview), FALSE);

    table = gui.table = gwy_param_table_new(args->params);

    gwy_param_table_append_radio_header(table, PARAM_METHOD);
    gwy_param_table_append_radio_item(table, PARAM_METHOD, LINE_MATCH_MEDIAN);
    gwy_param_table_append_radio_item(table, PARAM_METHOD, LINE_MATCH_MEDIAN_DIFF);
    gwy_param_table_append_radio_item(table, PARAM_METHOD, LINE_MATCH_MODUS);
    gwy_param_table_append_radio_item(table, PARAM_METHOD, LINE_MATCH_MATCH);
    gwy_param_table_append_radio_item(table, PARAM_METHOD, LINE_MATCH_FACET_TILT);
    gwy_param_table_append_radio_item(table, PARAM_METHOD, LINE_MATCH_POLY);
    gwy_param_table_append_slider(table, PARAM_MAX_DEGREE);
    gwy_param_table_append_radio_item(table, PARAM_METHOD, LINE_MATCH_TRIMMED_MEAN);
    gwy_param_table_append_radio_item(table, PARAM_METHOD, LINE_MATCH_TMEAN_DIFF);
    gwy_param_table_append_slider(table, PARAM_TRIM_FRACTION);
    gwy_param_table_slider_set_steps(table, PARAM_TRIM_FRACTION, 0.01, 0.1);
    gwy_param_table_slider_set_factor(table, PARAM_TRIM_FRACTION, 100.0);
    gwy_param_table_set_unitstr(table, PARAM_TRIM_FRACTION, "%");

    gwy_param_table_append_header(table, -1, _("Options"));
    gwy_param_table_append_combo(table, PARAM_DIRECTION);
    gwy_param_table_append_checkbox(table, PARAM_DO_EXTRACT);
    gwy_param_table_append_checkbox(table, PARAM_DO_PLOT);

    gwy_param_table_append_target_graph(table, PARAM_TARGET_GRAPH, gui.gmodel);

    if (args->mask)
        gwy_param_table_append_combo(table, PARAM_MASKING);

    gtk_box_pack_start(GTK_BOX(hbox), gwy_param_table_widget(table), TRUE, TRUE, 0);

    g_signal_connect_swapped(table, "param-changed", G_CALLBACK(param_changed), &gui);
    gwy_dialog_set_preview_func(dialog, GWY_PREVIEW_IMMEDIATE, preview, &gui, NULL);
    outcome = gwy_dialog_run(dialog);

    g_object_unref(gui.gmodel);

    return outcome;
}

static void
execute(ModuleArgs *args)
{
    GwyField *rowfield = args->result, *field = args->field;
    GwyNield *rowmask = args->mask, *mask = args->mask;
    GwyLine *shifts = args->shifts;
    LineMatchMethod method = gwy_params_get_enum(args->params, PARAM_METHOD);
    GwyMaskingType masking = gwy_params_get_masking(args->params, PARAM_MASKING, &mask);
    GwyOrientation direction = gwy_params_get_enum(args->params, PARAM_DIRECTION);
    gdouble trim_fraction = gwy_params_get_double(args->params, PARAM_TRIM_FRACTION);
    gint max_degree = gwy_params_get_int(args->params, PARAM_MAX_DEGREE);

    gwy_field_assign(args->result, field);
    gwy_field_assign(args->bg, field);

    /* Transpose the fields if necessary. */
    if (direction == GWY_ORIENTATION_VERTICAL) {
        rowfield = gwy_field_new_alike(args->result, FALSE);
        gwy_field_transpose(args->result, rowfield, FALSE);
        if (mask) {
            rowmask = gwy_nield_new_alike(mask);
            gwy_nield_transpose(mask, rowmask, FALSE);
        }
    }

    gwy_line_resize(shifts, gwy_field_get_yres(rowfield));
    gwy_line_set_real(shifts, gwy_field_get_yreal(rowfield));

    /* Perform the correction. */
    if (method == LINE_MATCH_POLY) {
        if (max_degree == 0)
            linematch_do_trimmed_mean(rowfield, rowmask, shifts, masking, 0.0);
        else
            gwy_field_row_level_poly(rowfield, rowmask, masking, max_degree, shifts);
    }
    else if (method == LINE_MATCH_MEDIAN)
        linematch_do_trimmed_mean(rowfield, rowmask, shifts, masking, 0.5);
    else if (method == LINE_MATCH_MEDIAN_DIFF)
        linematch_do_trimmed_diff(rowfield, rowmask, shifts, masking, 0.5);
    else if (method == LINE_MATCH_MODUS)
        linematch_do_modus(rowfield, rowmask, shifts, masking);
    else if (method == LINE_MATCH_MATCH)
        linematch_do_match(rowfield, rowmask, shifts, masking);
    else if (method == LINE_MATCH_FACET_TILT)
        linematch_do_facet_tilt(rowfield, rowmask, shifts, masking);
    else if (method == LINE_MATCH_TRIMMED_MEAN)
        linematch_do_trimmed_mean(rowfield, rowmask, shifts, masking, trim_fraction);
    else if (method == LINE_MATCH_TMEAN_DIFF)
        linematch_do_trimmed_diff(rowfield, rowmask, shifts, masking, trim_fraction);
    else {
        g_assert_not_reached();
    }

    /* Transpose back if necessary. */
    if (direction == GWY_ORIENTATION_VERTICAL) {
        g_clear_object(&rowmask);
        gwy_field_transpose(rowfield, args->result, FALSE);
        g_object_unref(rowfield);
    }
    gwy_field_subtract_fields(args->bg, args->bg, args->result);
}

static void
param_changed(ModuleGUI *gui, gint id)
{
    GwyParams *params = gui->args->params;
    GwyParamTable *table = gui->table;

    if (id < 0 || id == PARAM_METHOD) {
        LineMatchMethod method = gwy_params_get_enum(params, PARAM_METHOD);
        gwy_param_table_set_sensitive(table, PARAM_MAX_DEGREE, method == LINE_MATCH_POLY);
        gwy_param_table_set_sensitive(table, PARAM_TRIM_FRACTION,
                                      method == LINE_MATCH_TRIMMED_MEAN || method == LINE_MATCH_TMEAN_DIFF);
    }
    if (id < 0 || id == PARAM_DO_PLOT) {
        gboolean do_plot = gwy_params_get_boolean(params, PARAM_DO_PLOT);
        gwy_param_table_set_sensitive(table, PARAM_TARGET_GRAPH, do_plot);
    }
    if (id != PARAM_DO_PLOT && id != PARAM_DO_EXTRACT && id != PARAM_TARGET_GRAPH)
        gwy_dialog_invalidate(GWY_DIALOG(gui->dialog));
}

static void
preview(gpointer user_data)
{
    ModuleGUI *gui = (ModuleGUI*)user_data;
    ModuleArgs *args = gui->args;

    execute(args);
    gwy_field_data_changed(args->result);
    gwy_dialog_have_result(GWY_DIALOG(gui->dialog));
}

static void
linematch_do_trimmed_mean(GwyField *field, GwyNield *mask, GwyLine *shifts,
                          GwyMaskingType masking, gdouble trimfrac)
{
    GwyLine *myshifts = gwy_field_find_row_shifts_trimmed_mean(field, mask, masking, trimfrac, 0);
    gwy_field_subtract_row_shifts(field, myshifts);
    gwy_line_assign(shifts, myshifts);
    g_object_unref(myshifts);
}

static void
linematch_do_trimmed_diff(GwyField *field, GwyNield *mask, GwyLine *shifts,
                          GwyMaskingType masking, gdouble trimfrac)
{
    GwyLine *myshifts = gwy_field_find_row_shifts_trimmed_diff(field, mask, masking, trimfrac, 0);
    gwy_field_subtract_row_shifts(field, myshifts);
    gwy_line_assign(shifts, myshifts);
    g_object_unref(myshifts);
}

/* 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
linematch_do_modus(GwyField *field, GwyNield *mask, GwyLine *modi,
                   GwyMaskingType masking)
{
    gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
    gdouble total_median = gwy_field_area_median(field, mask, masking, 0, 0, xres, yres);
    gdouble *d = gwy_field_get_data(field);
    const gint *m = mask ? gwy_nield_get_data_const(mask) : NULL;

#ifdef _OPENMP
#pragma omp parallel if(gwy_threads_are_enabled()) default(none) \
            shared(d,m,modi,xres,yres,masking,total_median)
#endif
    {
        GwyLine *line = gwy_line_new(xres, 1.0, FALSE);
        gdouble *buf = gwy_line_get_data(line);
        gint ifrom = gwy_omp_chunk_start(yres), ito = gwy_omp_chunk_end(yres);

        for (gint i = ifrom; i < ito; i++) {
            const gdouble *row = d + i*xres;
            const gint *mrow = m ? m + i*xres : NULL;
            gint count = 0;

            for (gint j = 0; j < xres; j++) {
                if (nielded_included(mrow + j, masking))
                    buf[count++] = row[j];
            }

            gdouble modus;
            if (!count)
                modus = total_median;
            else if (count < 9)
                modus = gwy_math_median(buf, count);
            else {
                gint seglen = GWY_ROUND(sqrt(count)), bestj = 0;
                gdouble diff, bestdiff = G_MAXDOUBLE;

                gwy_math_sort(buf, count);
                for (gint j = 0; j + seglen-1 < count; j++) {
                    diff = buf[j + seglen-1] - buf[j];
                    if (diff < bestdiff) {
                        bestdiff = diff;
                        bestj = j;
                    }
                }
                modus = 0.0;
                count = 0;
                for (gint j = seglen/3; j < seglen - seglen/3; j++, count++)
                    modus += buf[bestj + j];
                modus /= count;
            }

            gwy_line_set_val(modi, i, modus);
        }

        g_object_unref(line);
    }

    zero_level_row_shifts(modi);
    gwy_field_subtract_row_shifts(field, modi);
}

static void
linematch_do_match(GwyField *field, GwyNield *mask, GwyLine *shifts,
                   GwyMaskingType masking)
{
    gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
    gdouble *d = gwy_field_get_data(field);
    const gint *m = mask ? gwy_nield_get_data_const(mask) : NULL;
    gdouble *s = gwy_line_get_data(shifts);

#ifdef _OPENMP
#pragma omp parallel if(gwy_threads_are_enabled()) default(none) \
            shared(d,m,s,xres,yres,masking)
#endif
    {
        gint ifrom = gwy_omp_chunk_start(yres-1) + 1;
        gint ito = gwy_omp_chunk_end(yres-1) + 1;
        gdouble *w = g_new(gdouble, xres-1);

        for (gint i = ifrom; i < ito; i++) {
            const gdouble *a = d + xres*(i - 1);
            const gdouble *b = d + xres*i;
            const gint *ma = m + xres*(i - 1);
            const gint *mb = m + xres*i;

            /* Diffnorm */
            gdouble wsum = 0.0;
            for (gint j = 0; j < xres-1; j++) {
                if (nielded_included(ma + j, masking) && nielded_included(mb + j, masking)) {
                    gdouble x = a[j+1] - a[j] - b[j+1] + b[j];
                    wsum += fabs(x);
                }
            }
            if (wsum == 0) {
                s[i] = 0.0;
                continue;
            }
            gdouble q = wsum/(xres-1);

            /* Weights */
            wsum = 0.0;
            for (gint j = 0; j < xres-1; j++) {
                if (nielded_included(ma + j, masking) && nielded_included(mb + j, masking)) {
                    gdouble x = a[j+1] - a[j] - b[j+1] + b[j];
                    w[j] = exp(-(x*x/(2.0*q)));
                    wsum += w[j];
                }
                else {
                    w[j] = 0.0;
                    continue;
                }
            }

            /* Correction */
            gdouble lambda = (a[0] - b[0])*w[0];
            for (gint j = 1; j < xres-1; j++) {
                if (nielded_included(ma + j, masking) && nielded_included(mb + j, masking))
                    lambda += (a[j] - b[j])*(w[j-1] + w[j]);
            }
            lambda += (a[xres-1] - b[xres-1])*w[xres-2];
            lambda /= 2.0*wsum;

            gwy_debug("%g %g %g", q, wsum, lambda);

            s[i] = -lambda;
        }

        g_free(w);
    }

    s[0] = 0.0;
    for (gint k = 1; k < yres; k++)
        s[k] += s[k-1];

    zero_level_row_shifts(shifts);
    gwy_field_subtract_row_shifts(field, shifts);
}

static gdouble
row_fit_facet_tilt(const gdouble *drow,
                   const gint *mrow,
                   GwyMaskingType masking,
                   guint res, gdouble dx,
                   guint mincount)
{
    const gdouble c = 1.0/200.0;

    gdouble sigma2 = 0.0;
    gint n = 0;
    if (mrow) {
        for (gint i = 0; i < res-1; i++) {
            if (nielded_included(mrow + i, masking) && nielded_included(mrow + i+1, masking)) {
                gdouble vx = (drow[i+1] - drow[i])/dx;
                sigma2 += vx*vx;
                n++;
            }
        }
    }
    else {
        for (gint i = 0; i < res-1; i++) {
            gdouble vx = (drow[i+1] - drow[i])/dx;
            sigma2 += vx*vx;
        }
        n = res-1;
    }
    /* Do not try to level from some random pixel */
    gwy_debug("n=%d", n);
    if (n < mincount)
        return 0.0;

    sigma2 = c*sigma2/n;

    gdouble sumvx = 0.0, sumvz = 0.0;
    if (mrow) {
        for (gint i = 0; i < res-1; i++) {
            if (nielded_included(mrow + i, masking) && nielded_included(mrow + i+1, masking)) {
                gdouble vx = (drow[i+1] - drow[i])/dx;
                gdouble q = exp(vx*vx/sigma2);
                sumvx += vx/q;
                sumvz += 1.0/q;
            }
        }
    }
    else {
        for (gint i = 0; i < res-1; i++) {
            gdouble vx = (drow[i+1] - drow[i])/dx;
            gdouble q = exp(vx*vx/sigma2);
            sumvx += vx/q;
            sumvz += 1.0/q;
        }
    }

    return sumvx/sumvz * dx;
}

static void
untilt_row(gdouble *drow, guint res, gdouble bx)
{
    gdouble x;
    guint i;

    if (!bx)
        return;

    for (i = 0; i < res; i++) {
        x = i - 0.5*(res - 1);
        drow[i] -= bx*x;
    }
}

static void
linematch_do_facet_tilt(GwyField *field, GwyNield *mask, GwyLine *shifts,
                        GwyMaskingType masking)
{
    gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
    gdouble dx = gwy_field_get_dx(field);
    gint mincount = GWY_ROUND(log(xres) + 1);

    gdouble *data = gwy_field_get_data(field);
    const gint *mdata = mask ? gwy_nield_get_data_const(mask) : NULL;
    for (gint i = 0; i < yres; i++) {
        gdouble *drow = data + i*xres;
        const gint *mrow = mdata ? mdata + i*xres : NULL;
        for (gint j = 0; j < 30; j++) {
            gdouble tilt = row_fit_facet_tilt(drow, mrow, masking, xres, dx, mincount);
            untilt_row(drow, xres, tilt);
            if (fabs(tilt/dx) < 1e-6)
                break;
        }
    }

    /* FIXME: Should we put the tilts there to confuse the user?  We need to make sure all functions set the units
     * correctly in such case. */
    gwy_line_clear(shifts);
}

static void
zero_level_row_shifts(GwyLine *shifts)
{
    gwy_line_add(shifts, -gwy_line_mean(shifts));
}

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