/*
 *  $Id: wpour_mark.c 30111 2026-06-22 11:02:00Z yeti-dn $
 *  Copyright (C) 2014-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 "../preview.h"

#define RUN_MODES (GWY_RUN_IMMEDIATE | GWY_RUN_INTERACTIVE)

#define FWHM2SIGMA (1.0/(2.0*sqrt(2*G_LN2)))

typedef enum {
    IMAGE_PREVIEW_ORIGINAL,
    IMAGE_PREVIEW_PREPROC,
} ImagePreviewType;

typedef enum {
    MASK_PREVIEW_NONE,
    MASK_PREVIEW_MARKED,
} MaskPreviewType;

enum {
    PARAM_INVERTED,
    PARAM_UPDATE,
    PARAM_IMAGE_PREVIEW,
    PARAM_MASK_PREVIEW,
    PARAM_BLUR_FWHM,
    PARAM_BARRIER_LEVEL,
    PARAM_PREFILL_LEVEL,
    PARAM_PREFILL_HEIGHT,
    PARAM_GRADIENT_CONTRIB,
    PARAM_CURVATURE_CONTRIB,
    PARAM_COMBINE_TYPE,
    PARAM_COMBINE,
    PARAM_MASK_COLOR,
};

typedef struct {
    guint size;
    guint len;
    gint *data;
} IntList;

typedef struct {
    GwyParams *params;
    GwyField *field;
    GwyField *preproc;
    GwyNield *mask;
    GwyNield *result;
} ModuleArgs;

typedef struct {
    ModuleArgs *args;
    GtkWidget *dialog;
    GtkWidget *dataview;
    GwyParamTable *table;
} 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             add_slope_contribs      (GwyField *field,
                                                 gdouble gradient_contrib,
                                                 gdouble curvature_contrib);
static void             normal_vector_difference(GwyField *result,
                                                 GwyField *xder,
                                                 GwyField *yder);
static gdouble          create_barriers         (GwyField *field,
                                                 gdouble level);
static void             prefill_minima          (GwyField *field,
                                                 IntList *inqueue,
                                                 IntList *outqueue,
                                                 gdouble depth,
                                                 gdouble height);
static void             replace_value           (GwyField *field,
                                                 gdouble from,
                                                 gdouble to);

static GwyModuleInfo module_info = {
    GWY_MODULE_ABI_VERSION,
    &module_register,
    N_("Segments image using watershed with pre- and postprocessing."),
    "Yeti <yeti@gwyddion.net>",
    "2.2",
    "David Nečas (Yeti)",
    "2014",
};

GWY_MODULE_QUERY2(module_info, wpour_mark)

static gboolean
module_register(void)
{
    gwy_image_func_register("wpour_mark",
                            module_main,
                            N_("/_Grains/_Mark by Segmentation..."),
                            GWY_ICON_GRAINS_WATER,
                            RUN_MODES,
                            GWY_MENU_FLAG_IMAGE,
                            N_("Segment using watershed "));

    return TRUE;
}

static GwyParamDef*
define_module_params(void)
{
    static const GwyEnum image_previews[] = {
        { N_("Original image"),     IMAGE_PREVIEW_ORIGINAL, },
        { N_("Preprocessed image"), IMAGE_PREVIEW_PREPROC,  },
    };
    static const GwyEnum mask_previews[] = {
        { N_("No mask"),       MASK_PREVIEW_NONE,          },
        { N_("Marked"),        MASK_PREVIEW_MARKED,        },
    };
    static GwyParamDef *paramdef = NULL;

    if (paramdef)
        return paramdef;

    paramdef = gwy_param_def_new();
    gwy_param_def_set_function_name(paramdef, gwy_image_func_current());
    gwy_param_def_add_boolean(paramdef, PARAM_INVERTED, "inverted", _("_Invert height"), FALSE);
    gwy_param_def_add_instant_updates(paramdef, PARAM_UPDATE, "update", NULL, FALSE);
    gwy_param_def_add_gwyenum(paramdef, PARAM_IMAGE_PREVIEW, "image_preview", _("_Image preview"),
                              image_previews, G_N_ELEMENTS(image_previews), IMAGE_PREVIEW_ORIGINAL);
    gwy_param_def_add_gwyenum(paramdef, PARAM_MASK_PREVIEW, "mask_preview", _("_Mask preview"),
                              mask_previews, G_N_ELEMENTS(mask_previews), MASK_PREVIEW_MARKED);
    gwy_param_def_add_double(paramdef, PARAM_BLUR_FWHM, "blur_fwhm", _("Gaussian _smoothing"), 0.0, 25.0, 0.0);
    gwy_param_def_add_percentage(paramdef, PARAM_BARRIER_LEVEL, "barrier_level", _("_Barrier level"), 1.0);
    gwy_param_def_add_percentage(paramdef, PARAM_PREFILL_LEVEL, "prefill_level", _("Prefill _level"), 0.0);
    gwy_param_def_add_percentage(paramdef, PARAM_PREFILL_HEIGHT, "prefill_height", _("Pre_fill from minima"), 0.0);
    gwy_param_def_add_percentage(paramdef, PARAM_GRADIENT_CONTRIB, "gradient_contrib", _("Add _gradient"), 0.0);
    gwy_param_def_add_percentage(paramdef, PARAM_CURVATURE_CONTRIB, "curvature_contrib", _("Add _curvature"), 0.0);
    gwy_param_def_add_basic_merge(paramdef, PARAM_COMBINE_TYPE, "combine_type", NULL, FALSE, GWY_MERGE_UNION);
    gwy_param_def_add_boolean(paramdef, PARAM_COMBINE, "combine", NULL, FALSE);
    gwy_param_def_add_mask_color(paramdef, PARAM_MASK_COLOR, NULL, NULL);
    return paramdef;
}

static void
module_main(GwyFile *data, GwyRunModeFlags mode)
{
    GwyDialogOutcome outcome = GWY_DIALOG_PROCEED;
    ModuleArgs args;
    gwy_clear1(args);
    GQuark mquark;
    gint id;

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

    args.result = gwy_field_new_nield_alike(args.field);
    args.preproc = gwy_field_new_alike(args.field, TRUE);
    args.params = gwy_params_new_from_settings(define_module_params());

    if (mode == GWY_RUN_INTERACTIVE) {
        outcome = run_gui(&args, data, id);
        gwy_params_save_to_settings(args.params);
        if (outcome == GWY_DIALOG_CANCEL)
            goto end;
    }
    if (outcome != GWY_DIALOG_HAVE_RESULT)
        execute(&args);

    gwy_app_undo_qcheckpointv(GWY_DICT(data), 1, &mquark);
    gint ngrains = gwy_nield_number_contiguous(args.result);
    gwy_file_set_image_mask(data, id, ngrains ? args.result : NULL);
    gwy_log_add(data, GWY_FILE_IMAGE, id, id);

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

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

    gui.args = args;

    gui.dialog = gwy_dialog_new(_("Segment by Watershed"));
    dialog = GWY_DIALOG(gui.dialog);
    gwy_dialog_add_stock_buttons(dialog, GWY_STOCK_UPDATE, GWY_STOCK_RESET, GWY_STOCK_CANCEL, GWY_STOCK_OK, 0);

    dataview = gui.dataview = gwy_create_preview(args->field, args->result, PREVIEW_SIZE);
    gwy_setup_data_view(GWY_DATA_VIEW(dataview), data, GWY_FILE_IMAGE, id,
                        GWY_FILE_ITEM_PALETTE | GWY_FILE_ITEM_MASK_COLOR
                        | GWY_FILE_ITEM_RANGE | GWY_FILE_ITEM_REAL_SQUARE);
    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_header(table, -1, _("Preprocessing"));
    gwy_param_table_append_slider(table, PARAM_BLUR_FWHM);
    gwy_param_table_slider_add_alt(table, PARAM_BLUR_FWHM);
    gwy_param_table_alt_set_field_pixel_x(table, PARAM_BLUR_FWHM, args->field);
    gwy_param_table_append_slider(table, PARAM_GRADIENT_CONTRIB);
    gwy_param_table_append_slider(table, PARAM_CURVATURE_CONTRIB);
    gwy_param_table_append_slider(table, PARAM_BARRIER_LEVEL);
    gwy_param_table_append_slider(table, PARAM_PREFILL_LEVEL);
    gwy_param_table_append_slider(table, PARAM_PREFILL_HEIGHT);

    /* TODO: Some postprocessing was planned, but the section is currently empty... */

    gwy_param_table_append_header(table, -1, _("Options"));
    gwy_param_table_append_checkbox(table, PARAM_INVERTED);
    gwy_param_table_append_image_mask_color(table, PARAM_MASK_COLOR, GWY_DATA_VIEW(dataview), data, id);
    if (args->mask) {
        gwy_param_table_append_radio_buttons(table, PARAM_COMBINE_TYPE, NULL);
        gwy_param_table_add_enabler(table, PARAM_COMBINE, PARAM_COMBINE_TYPE);
    }
    gwy_param_table_append_combo(table, PARAM_IMAGE_PREVIEW);
    gwy_param_table_append_combo(table, PARAM_MASK_PREVIEW);
    gwy_param_table_append_checkbox(table, PARAM_UPDATE);
    gwy_param_table_set_no_reset(table, PARAM_UPDATE, TRUE);

    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);

    return gwy_dialog_run(dialog);
}

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

    if (id < 0 || id == PARAM_IMAGE_PREVIEW) {
        ImagePreviewType image_preview = gwy_params_get_enum(params, PARAM_IMAGE_PREVIEW);
        GwyField *field = (image_preview == IMAGE_PREVIEW_ORIGINAL ? args->field : args->preproc);
        gwy_data_view_set_field(GWY_DATA_VIEW(gui->dataview), field);
    }
    if (id < 0 || id == PARAM_MASK_PREVIEW) {
        MaskPreviewType mask_preview = gwy_params_get_enum(params, PARAM_MASK_PREVIEW);
        gwy_data_view_set_mask(GWY_DATA_VIEW(gui->dataview), mask_preview == MASK_PREVIEW_MARKED ? args->result : NULL);
    }

    if (id != PARAM_MASK_COLOR && id != PARAM_UPDATE && id != PARAM_IMAGE_PREVIEW && id != PARAM_MASK_PREVIEW)
        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->preproc);
    gwy_nield_data_changed(args->result);
    gwy_dialog_have_result(GWY_DIALOG(gui->dialog));
}

static inline IntList*
int_list_new(guint prealloc)
{
    IntList *list = g_new0(IntList, 1);
    prealloc = MAX(prealloc, 16);
    list->size = prealloc;
    list->data = g_new(gint, list->size);
    return list;
}

static inline void
int_list_add(IntList *list, gint i)
{
    if (G_UNLIKELY(list->len == list->size)) {
        list->size = MAX(2*list->size, 16);
        list->data = g_renew(gint, list->data, list->size);
    }

    list->data[list->len] = i;
    list->len++;
}

#if 0
static inline void
int_list_add_unique(IntList **plist, gint i)
{
    IntList *list;
    guint j;

    if (!*plist)
        *plist = int_list_new(0);

    list = *plist;
    for (j = 0; j < list->len; j++) {
        if (list->data[j] == i)
            return;
    }
    int_list_add(list, i);
}
#endif

static void
int_list_free(IntList *list)
{
    g_free(list->data);
    g_free(list);
}

static void
execute(ModuleArgs *args)
{
    GwyField *field = args->field, *preproc = args->preproc;
    GwyNield *result = args->result, *mask = args->mask;
    GwyParams *params = args->params;
    gboolean combine = gwy_params_get_boolean(params, PARAM_COMBINE);
    GwyMergeType combine_type = gwy_params_get_enum(params, PARAM_COMBINE_TYPE);
    gboolean inverted = gwy_params_get_boolean(params, PARAM_INVERTED);
    gdouble blur_fwhm = gwy_params_get_double(params, PARAM_BLUR_FWHM);
    gdouble gradient_contrib = gwy_params_get_double(params, PARAM_GRADIENT_CONTRIB);
    gdouble curvature_contrib = gwy_params_get_double(params, PARAM_CURVATURE_CONTRIB);
    gdouble barrier_level = gwy_params_get_double(params, PARAM_BARRIER_LEVEL);
    gdouble prefill_level = gwy_params_get_double(params, PARAM_PREFILL_LEVEL);
    gdouble prefill_height = gwy_params_get_double(params, PARAM_PREFILL_HEIGHT);
    IntList *inqueue = int_list_new(0);
    IntList *outqueue = int_list_new(0);
    guint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);

    gwy_field_copy_data(field, preproc);
    if (inverted)
        gwy_field_invert_value(preproc);

    gwy_field_add(preproc, -gwy_field_max(preproc));
    if (blur_fwhm)
        gwy_field_area_filter_gaussian(preproc, blur_fwhm*FWHM2SIGMA, 0, 0, xres, yres);

    add_slope_contribs(preproc, gradient_contrib, curvature_contrib);
    gdouble barmax = create_barriers(preproc, barrier_level);

    prefill_minima(preproc, inqueue, outqueue, prefill_level, prefill_height);

    /* Replace with G_MAXDOUBLE for maximum barrier. Do not use HUGE_VAL; with fast math and HUGE_VAL being inf, the
     * equality x == HUGE_VAL may not evaluate correctly. */
    replace_value(preproc, barmax, G_MAXDOUBLE);
    gwy_field_waterpour(preproc, result);
    /* Lower the barriers again to avoid G_MAXDOUBLE in the preview. */
    replace_value(preproc, G_MAXDOUBLE, barmax);

    int_list_free(outqueue);
    int_list_free(inqueue);

    if (combine) {
        if (combine_type == GWY_MERGE_SUBTRACT)
            combine_type = GWY_MERGE_REVSUBTRACT;
        gwy_nield_merge(result, mask, combine_type);
    }

#if 0
    /* FIXME: neither grains nor gnetwork used for anything at this moment. */
    ngrains = gwy_nield_number_contiguous(result);
    gnetwork = analyse_grain_network(preproc, result);
    g_hash_table_destroy(gnetwork);
#endif
}

static void
add_slope_contribs(GwyField *field,
                   gdouble gradient_contrib, gdouble curvature_contrib)
{
    gdouble r, rg, rc;

    if (!gradient_contrib && !curvature_contrib)
        return;

    r = gwy_field_rms(field);
    if (!r)
        return;

    GwyField *workspace = gwy_field_new_alike(field, FALSE);
    GwyField *xder = gwy_field_new_alike(field, FALSE);
    GwyField *yder = gwy_field_new_alike(field, FALSE);

    gwy_field_derivative_filter(field, xder, GWY_DERIVATIVE_FILTER_XDER, TRUE);
    gwy_field_derivative_filter(field, yder, GWY_DERIVATIVE_FILTER_YDER, TRUE);
    gwy_field_multiply(field, 1.0 - MAX(gradient_contrib, curvature_contrib));

    /* We need this for both operations. */
    gwy_field_hypot_of_fields(workspace, xder, yder);
    rg = gwy_field_rms(workspace);

    if (gradient_contrib) {
        gwy_field_multiply(workspace, gradient_contrib * r/rg);
        gwy_field_sum_fields(field, field, workspace);
    }

    if (curvature_contrib) {
        gwy_field_multiply(xder, 1.0/rg);
        gwy_field_multiply(yder, 1.0/rg);
        normal_vector_difference(workspace, xder, yder);
        rc = gwy_field_rms(workspace);
        if (rc) {
            gwy_field_multiply(workspace, curvature_contrib * r/rc);
            gwy_field_sum_fields(field, field, workspace);
        }
    }

    g_object_unref(yder);
    g_object_unref(xder);
    g_object_unref(workspace);

    gwy_field_invalidate(field);
}

static void
normal_vector(gdouble bx, gdouble by, gdouble *nx, gdouble *ny, gdouble *nz)
{
    gdouble b = sqrt(1.0 + bx*bx + by*by);

    *nx = -bx/b;
    *ny = -by/b;
    *nz = 1.0/b;
}

static void
normal_vector_difference(GwyField *result, GwyField *xder, GwyField *yder)
{
    const gdouble *bx, *by;
    guint xres, yres, i, j;
    gdouble *d;

    gwy_field_clear(result);
    xres = result->xres;
    yres = result->yres;
    d = gwy_field_get_data(result);
    bx = gwy_field_get_data_const(xder);
    by = gwy_field_get_data_const(yder);

    for (i = 0; i < yres; i++) {
        gdouble *row = d + i*xres, *next = row + xres;
        const gdouble *bxrow = bx + i*xres, *nextbx = bxrow + xres;
        const gdouble *byrow = by + i*xres, *nextby = byrow + xres;

        for (j = 0; j < xres; j++) {
            gdouble nx, ny, nz;
            gdouble nxr, nyr, nzr;
            gdouble nxd, nyd, nzd;
            gdouble ch, cv;

            normal_vector(bxrow[j], byrow[j], &nx, &ny, &nz);
            if (j < xres-1) {
                normal_vector(bxrow[j+1], byrow[j+1], &nxr, &nyr, &nzr);
                ch = nxr - nx;
                row[j] += ch;
                row[j+1] += ch;
            }

            if (i < yres-1) {
                normal_vector(nextbx[j], nextby[j], &nxd, &nyd, &nzd);
                cv = nyd - ny;
                row[j] += cv;
                next[j] += cv;
            }
        }
    }
}

static gdouble
create_barriers(GwyField *field, gdouble level)
{
    gdouble min, max, barmax;

    gwy_field_invalidate(field);
    gwy_field_min_max(field, &min, &max);
    barmax = max + 0.01*fmax(fabs(min), fabs(max));
    if (min == max)
        return barmax;

    if (level < 1.0) {
        gdouble threshold = level*(max - min) + min;
        gdouble *d = gwy_field_get_data(field);

        barmax = max;
        guint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
        for (guint k = 0; k < xres*yres; k++) {
            if (d[k] >= threshold)
                d[k] = barmax;
        }
    }

    return barmax;
}

static void
prefill_minima(GwyField *field,
               IntList *inqueue, IntList *outqueue,
               gdouble depth, gdouble height)
{
    guint i, j, k, m, xres = field->xres, yres = field->yres;
    gdouble min, max;

    gwy_field_min_max(field, &min, &max);
    if (min == max)
        return;

    /* Simple absolute prefilling corresponding to plain mark-by-threshold. */
    if (depth > 0.0) {
        gdouble depththreshold = depth*(max - min) + min;
        gdouble *d = gwy_field_get_data(field);

        for (k = 0; k < xres*yres; k++) {
            if (d[k] < depththreshold)
                d[k] = depththreshold;
        }
    }

    /* Simple height prefilling which floods all pixels with heights only little above the minimum. */
    if (height > 0.0) {
        gdouble heightthreshold = height*(max - min);
        gdouble *d = gwy_field_get_data(field);
        GwyNield *workspace = gwy_field_new_nield_alike(field);
        gint *w = gwy_nield_get_data(workspace);

        gwy_field_mark_extrema(field, workspace, FALSE);

        inqueue->len = 0;
        for (k = 0; k < xres*yres; k++) {
            if (w[k] > 0)
                int_list_add(inqueue, k);
        }
        g_object_unref(workspace);

        while (inqueue->len) {
            outqueue->len = 0;
            for (m = 0; m < inqueue->len; m++) {
                gdouble z, zth;

                k = inqueue->data[m];
                i = k/xres;
                j = k % xres;
                z = d[k];
                zth = z + heightthreshold*fabs(z)/(max - min);

                if (i > 0 && d[k-xres] > z && d[k-xres] < zth) {
                    d[k-xres] = z;
                    int_list_add(outqueue, k-xres);
                }
                if (j > 0 && d[k-1] > z && d[k-1] < zth) {
                    d[k-1] = z;
                    int_list_add(outqueue, k-1);
                }
                if (j < xres-1 && d[k+1] > z && d[k+1] < zth) {
                    d[k+1] = z;
                    int_list_add(outqueue, k+1);
                }
                if (i < yres-1 && d[k+xres] > z && d[k+xres] < zth) {
                    d[k+xres] = z;
                    int_list_add(outqueue, k+xres);
                }
            }

            GWY_SWAP(IntList*, inqueue, outqueue);
        }

        gwy_field_invalidate(field);
    }
}

static void
replace_value(GwyField *field, gdouble from, gdouble to)
{
    guint k, xres = field->xres, yres = field->yres;
    gdouble *d = gwy_field_get_data(field);

    for (k = 0; k < xres*yres; k++) {
        if (d[k] == from)
            d[k] = to;
    }
}

#if 0
typedef struct {
    guint a;
    guint b;
} UIntPair;

typedef struct {
    gdouble min_barrier;
    gdouble min_bsum;
} GrainNeighbour;

static guint
uint_pair_hash(gconstpointer p)
{
    const UIntPair *pair = (const UIntPair*)p;
    return pair->a*1103515245 + pair->b;
}

static gboolean
uint_pair_equal(gconstpointer pa, gconstpointer pb)
{
    const UIntPair *paira = (const UIntPair*)pa;
    const UIntPair *pairb = (const UIntPair*)pb;
    return paira->a == pairb->a && paira->b == pairb->b;
}

static void
uint_pair_free(gpointer p)
{
    g_free(p);
}

static void
grain_neighbour_free(gpointer p)
{
    g_free(p);
}

static inline gboolean
is_merge_pixel(const gint *gc, gint *pg1, gint *pg2)
{
    gint gnn[4], g1, g2;
    guint n, i;

    n = 0;
    for (i = 0; i < 4; i++) {
        if (gc[i])
            gnn[n++] = gc[i];
    }

    if (n < 2)
        return FALSE;

    g1 = gnn[0];
    g2 = gnn[1];
    if (n >= 3) {
        if (g1 == g2) {
            g2 = gnn[2];
            if (n == 4 && g1 == g2)
                g2 = gnn[3];
        }
        else {
            if (gnn[2] != g1 && gnn[2] != g2)
                return FALSE;
            if (n == 4 && gnn[3] != g1 && gnn[3] != g2)
                return FALSE;
        }
    }

    if (g1 == g2)
        return FALSE;

    *pg1 = g1;
    *pg2 = g2;
    return TRUE;
}

static GHashTable*
analyse_grain_network(GwyField *field, const gint *grains)
{
    GHashTable *gnetwork;
    guint xres = field->xres, yres = field->yres;
    const gdouble *data = field->data;
    guint i, j, k;

    gnetwork = g_hash_table_new_full(uint_pair_hash, uint_pair_equal, uint_pair_free, grain_neighbour_free);

    /* Scan possible merge-pixles.  A pixel touching more than 2 different grains is not a possible merge-pixel if
     * don't do simultaneous multi-grain merging.  We never merge through a dialonal; mergeable grains always touch
     * via a 4-connected pixel. */
    k = 0;
    for (i = 0; i < yres; i++) {
        for (j = 0; j < xres; j++, k++) {
            gint g1, g2, gc[4], n = 0, m;
            gdouble min_barrier, min_bsum, z, zc[4];
            GrainNeighbour *neighbour;
            UIntPair pair;

            if (grains[k])
                continue;

            /* Figure out how many different grains touch this non-grain pixel.
             */
            gc[0] = i ? grains[k - xres] : 0;
            gc[1] = j ? grains[k-1] : 0;
            gc[2] = j < xres-1 ? grains[k+1] : 0;
            gc[3] = i < yres-1 ? grains[k + xres] : 0;
            if (!is_merge_pixel(gc, &g1, &g2))
                continue;

            /* Now we know we have a non-grain pixel that can connect exactly
             * two grains together.  So we will be definitely adding it.
             * Find the barrier. */
            min_barrier = min_bsum = G_MAXDOUBLE;
            z = data[k];
            zc[0] = i ? fabs(z - data[k - xres]) : G_MAXDOUBLE;
            zc[1] = j ? fabs(z - data[k-1]) : G_MAXDOUBLE;
            zc[2] = j < xres-1 ? fabs(z - data[k+1]) : G_MAXDOUBLE;
            zc[3] = i < yres-1 ? fabs(z - data[k + xres]) : G_MAXDOUBLE;
            for (n = 1; n < 4; n++) {
                if (!gc[n])
                    continue;

                min_barrier = MIN(min_barrier, zc[n]);
                for (m = 0; m < n; m++) {
                    if (!gc[m] || gc[m] == gc[n])
                        continue;

                    min_bsum = MIN(min_bsum, zc[m] + zc[n]);
                }
            }

            pair.a = MIN(g1, g2);
            pair.b = MAX(g1, g2);
            neighbour = g_hash_table_lookup(gnetwork, &pair);
            if (neighbour) {
                neighbour->min_barrier = MIN(neighbour->min_barrier, min_barrier);
                neighbour->min_bsum = MIN(neighbour->min_bsum, min_bsum);
            }
            else {
                UIntPair *ppair = g_memdup2(&pair, sizeof(UIntPair));
                neighbour = g_new(GrainNeighbour, 1);
                neighbour->min_barrier = min_barrier;
                neighbour->min_bsum = min_bsum;
                g_hash_table_insert(gnetwork, ppair, neighbour);
            }
        }
    }

    /* Now we have the symmetrical irreflexive neighbour relation encoded
     * in gnetwork together with barriers between the neighbour grains. */
    return gnetwork;
}
#endif

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