/*
 *  $Id: indent_analyze.c 30047 2026-06-11 08:30:45Z yeti-dn $
 *  Copyright (C) 2006-2026 Lukas Chvatal, David Necas (Yeti), Petr Klapetek.
 *  E-mail: chvatal@physics.muni.cz, yeti@gwyddion.net, klapetek@gwyddion.net.
 *
 *  This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public
 *  License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any
 *  later version.
 *
 *  This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
 *  warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
 *  details.
 *
 *  You should have received a copy of the GNU General Public License along with this program; if not, write to the
 *  Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */

#include "config.h"
#include <glib/gi18n-lib.h>
#include <string.h>
#include <gtk/gtk.h>
#include <gwy.h>
#include "../preview.h"

#define RUN_MODES GWY_RUN_INTERACTIVE

enum {
    MAX_PYRAMID_SIDES = 4
};

typedef enum {
    RESULT_NOTHING = 0,
    RESULT_EXTERIOR,
    RESULT_ABOVE,             /* Auxiliary, not shown. */
    RESULT_BELOW,             /* Auxiliary, not shown. */
    RESULT_PLANE,
    RESULT_IMPRINT,
    RESULT_IMPRINT_FACES,
    RESULT_CONTACT_AREA,
    RESULT_PILEUP,
    RESULT_INNER_PILEUP,
    RESULT_OUTER_PILEUP,
    RESULT_FACES_BORDER,
    RESULT_NTYPES
} IndentDisplayType;

typedef enum {
    INDENTER_SPHERE   = 0,
    INDENTER_PYRAMID3 = 3,
    INDENTER_PYRAMID4 = 4,
} GwyIndenterType;

enum {
    PARAM_DO_LEVEL,
    PARAM_EXTERIOR,
    PARAM_PLANE_TOL,
    PARAM_PHI_TOL,
    PARAM_INDENTER,
    PARAM_DISPLAY,
    PARAM_SET_MASK,
    PARAM_MASK_COLOR,
    PARAM_REPORT_STYLE,

    WIDGET_RESULTS,
};

/* p = projected, d = developed (i.e. surface) */
typedef struct {
    gdouble x0;
    gdouble y0;
    gdouble zmin;
    gdouble zmax;
    gdouble Vimp;
    gdouble Vpileup;
    gdouble Acontact;
    gdouble Asurf_imp;
    gdouble Aproj_imp;
    gdouble Asurf_pileup;
    gdouble Aproj_pileup;
    gdouble Asurf_in;
    gdouble Aproj_in;
    gdouble Asurf_out;
    gdouble Aproj_out;
    /* Auxiliary. */
    gdouble rms_base;
    gdouble phi;
} ImprintParameters;

typedef struct {
    GwyXY apex;
    GwyXY side[MAX_PYRAMID_SIDES];
} PyramidParameters;

typedef struct {
    GwyParams *params;
    GwyField *field;
    GwyField *adjusted;
    GwyField *xder;
    GwyField *yder;
    GwyNield *result[RESULT_NTYPES];
    GwySelection *selection;
    ImprintParameters imp;
} ModuleArgs;

typedef struct {
    ModuleArgs *args;
    GtkWidget *dialog;
    GtkWidget *dataview;
    GwyParamTable *table_param;
    GwyParamTable *table_results;
    GwyResults *results;
} 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 GwyResults*      create_results          (ModuleArgs *args,
                                                 GwyFile *data,
                                                 gint id);
static void             param_changed           (ModuleGUI *gui,
                                                 gint id);
static void             preview                 (gpointer user_data);
static void             level_using_exterior    (GwyField *field,
                                                 GwyNield *mask,
                                                 gdouble wfrac,
                                                 gboolean do_level);
static void             compute_slopes          (GwyField *field,
                                                 gint kernel_size,
                                                 GwyField *xder,
                                                 GwyField *yder,
                                                 GwyField *atanfield);
static gdouble          find_prevalent_phi      (GwyField *field,
                                                 GwyNield *exclmask,
                                                 GwyField *xder,
                                                 GwyField *yder,
                                                 GwyField *atanfield);
static void             find_imprint_side_centre(GwyField *field,
                                                 gdouble search_to_z,
                                                 gdouble phi,
                                                 gdouble *xcentre,
                                                 gdouble *ycentre);
static void             calc_mean_normal        (GwyField *xder,
                                                 GwyField *yder,
                                                 gdouble x,
                                                 gdouble y,
                                                 gdouble r,
                                                 GwyXYZ *v);
static void             mark_facet              (GwyField *xder,
                                                 GwyField *yder,
                                                 GwyNield *mask,
                                                 gdouble theta0,
                                                 gdouble phi0,
                                                 gdouble tolerance);
static void             mark_pileup             (GwyNield *above,
                                                 GwyNield *imprint,
                                                 GwyNield *pileup);
static void             calc_surface_and_volume (GwyField *field,
                                                 GwyNield *mask,
                                                 gdouble *Aproj,
                                                 gdouble *Asurf,
                                                 gdouble *V);

static GwyModuleInfo module_info = {
    GWY_MODULE_ABI_VERSION,
    &module_register,
    N_("Analyses nanoindentation structure (volumes, surfaces, ...)."),
    "Lukáš Chvátal <chvatal@physics.muni.cz> & Yeti <yeti@physics.muni.cz>",
    "2.0",
    "Lukáš Chvátal",
    "2005",
};

GWY_MODULE_QUERY2(module_info, indent_analyze)

static gboolean
module_register(void)
{
    gwy_image_func_register("indent_analyze",
                            module_main,
                            N_("/SPM M_odes/_Force and Indentation/_Analyze Imprint..."),
                            GWY_ICON_TIP_INDENT_ANALYZE,
                            RUN_MODES,
                            GWY_MENU_FLAG_IMAGE,
                            _("Analyze indentation imprint"));

    return TRUE;
}

static GwyParamDef*
define_module_params(void)
{
    static const GwyEnum indenters[] = {
        { N_("Sphere"),              INDENTER_SPHERE,   },
        { N_("Pyramid (3-sided)"),   INDENTER_PYRAMID3, },
        { N_("Pyramid (rectangle)"), INDENTER_PYRAMID4, },
    };
    static const GwyEnum displays[] = {
        { N_("Nothing"),       RESULT_NOTHING,       },
        { N_("Exterior"),      RESULT_EXTERIOR,      },
        { N_("Plane"),         RESULT_PLANE,         },
        { N_("Imprint"),       RESULT_IMPRINT,       },
        { N_("Imprint faces"), RESULT_IMPRINT_FACES, },
        { N_("Contact area"),  RESULT_CONTACT_AREA,  },
        { N_("Pile-up"),       RESULT_PILEUP,        },
        { N_("Inner pile-up"), RESULT_INNER_PILEUP,  },
        { N_("Outer pile-up"), RESULT_OUTER_PILEUP,  },
        { N_("Faces border"),  RESULT_FACES_BORDER,  },
    };
    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_DO_LEVEL, "do_level", _("Level using imprint exterior"), TRUE);
    gwy_param_def_add_double(paramdef, PARAM_EXTERIOR, "border", _("Exterior width"), 1.0, 40.0, 5.0);
    gwy_param_def_add_double(paramdef, PARAM_PLANE_TOL, "plane_tol", _("Ref. plane _tolerance"), 0.0, 8.0, 2.0);
    gwy_param_def_add_double(paramdef, PARAM_PHI_TOL, "phi_tol", _("_Angle tolerance"), 0.0, G_PI, gwy_deg2rad(8.0));
    gwy_param_def_add_gwyenum(paramdef, PARAM_INDENTER, "indentor", _("_Indenter shape"),
                              indenters, G_N_ELEMENTS(indenters), INDENTER_PYRAMID3);
    gwy_param_def_add_gwyenum(paramdef, PARAM_DISPLAY, "display", C_("verb", "_Display"),
                              displays, G_N_ELEMENTS(displays), RESULT_NOTHING);
    gwy_param_def_add_boolean(paramdef, PARAM_SET_MASK, "set_mask", _("Create _mask"), TRUE);
    gwy_param_def_add_mask_color(paramdef, PARAM_MASK_COLOR, NULL, NULL);
    gwy_param_def_add_report_type(paramdef, PARAM_REPORT_STYLE, "report_style", NULL,
                                  GWY_RESULTS_EXPORT_PARAMETERS, GWY_RESULTS_REPORT_COLON);
    return paramdef;
}

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

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

    if (!gwy_require_image_same_units(args.field, data, id, _("Analyze imprint")))
        return;

    args.adjusted = gwy_field_copy(args.field);
    for (guint i = 1; i < RESULT_NTYPES; i++)
        args.result[i] = gwy_field_new_nield_alike(args.field);
    args.xder = gwy_field_new_alike(args.field, TRUE);
    args.yder = gwy_field_new_alike(args.field, TRUE);
    args.params = gwy_params_new_from_settings(define_module_params());

    outcome = run_gui(&args, data, id);
    gwy_params_save_to_settings(args.params);
    if (outcome == GWY_DIALOG_CANCEL)
        goto end;

    set_mask = gwy_params_get_boolean(args.params, PARAM_SET_MASK);
    display = gwy_params_get_enum(args.params, PARAM_DISPLAY);
    if (set_mask && display != RESULT_NOTHING) {
        if (outcome != GWY_DIALOG_HAVE_RESULT)
            execute(&args);
        gwy_app_undo_qcheckpointv(GWY_DICT(data), 1, &mquark);
        GwyNield *result = args.result[display];
        gboolean nonempty = gwy_nield_flatten(result);
        gwy_file_set_image_mask(data, id, nonempty ? result : NULL);
        gwy_log_add(data, GWY_FILE_IMAGE, id, id);
    }

end:
    for (guint i = 1; i < RESULT_NTYPES; i++)
        g_object_unref(args.result[i]);
    g_object_unref(args.params);
    g_object_unref(args.adjusted);
    g_object_unref(args.xder);
    g_object_unref(args.yder);
    /* TODO: We can set the selection on the image. */
    g_clear_object(&args.selection);
}

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;
    gui.results = create_results(args, data, id);

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

    dataview = gui.dataview = gwy_create_preview(args->adjusted, NULL, PREVIEW_SIZE);
    gwy_setup_data_view(GWY_DATA_VIEW(dataview), data, GWY_FILE_IMAGE, id,
                        GWY_FILE_ITEM_PALETTE | GWY_FILE_ITEM_RANGE
                        | GWY_FILE_ITEM_MASK_COLOR | GWY_FILE_ITEM_REAL_SQUARE);
    args->selection = gwy_create_preview_vector_layer(GWY_DATA_VIEW(dataview), GWY_TYPE_LAYER_POINT, 5, FALSE);
    g_object_ref(args->selection);
    hbox = gwy_create_dialog_preview_hbox(GTK_DIALOG(dialog), GWY_DATA_VIEW(dataview), FALSE);

    table = gui.table_param = gwy_param_table_new(args->params);
    gwy_param_table_append_header(table, -1, _("Leveling"));
    gwy_param_table_append_checkbox(table, PARAM_DO_LEVEL);
    gwy_param_table_append_slider(table, PARAM_EXTERIOR);
    gwy_param_table_set_unitstr(table, PARAM_EXTERIOR, "%");

    gwy_param_table_append_header(table, -1, _("Marking"));
    gwy_param_table_append_combo(table, PARAM_INDENTER);
    gwy_param_table_append_slider(table, PARAM_PLANE_TOL);
    gwy_param_table_set_unitstr(table, PARAM_PLANE_TOL, _("RMS"));
    gwy_param_table_append_slider(table, PARAM_PHI_TOL);
    gwy_param_table_slider_set_factor(table, PARAM_PHI_TOL, 180.0/G_PI);
    gwy_param_table_set_unitstr(table, PARAM_PHI_TOL, _("deg"));
    gwy_param_table_append_radio(table, PARAM_DISPLAY);
    gwy_param_table_append_separator(table);
    gwy_param_table_append_image_mask_color(table, PARAM_MASK_COLOR, GWY_DATA_VIEW(dataview), data, id);

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

    table = gui.table_results = gwy_param_table_new(args->params);
    gwy_param_table_append_header(table, -1, _("Result"));
    gwy_param_table_append_results(table, WIDGET_RESULTS, gui.results,
                                   "x0", "y0", "zmin", "zmax",
                                   "Vimp", "Asurf_imp", "Aproj_imp", "Acontact",
                                   "Vpileup", "Asurf_pileup", "Aproj_pileup",
                                   "Asurf_in", "Aproj_in", "Asurf_out", "Aproj_out",
                                   NULL);
    gwy_param_table_append_report(table, PARAM_REPORT_STYLE);
    gwy_param_table_report_set_results(table, PARAM_REPORT_STYLE, gui.results);
    gwy_param_table_append_separator(table);
    gwy_param_table_append_header(table, -1, _("Output"));
    gwy_param_table_append_checkbox(table, PARAM_SET_MASK);

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

    g_signal_connect_swapped(gui.table_param, "param-changed", G_CALLBACK(param_changed), &gui);
    g_signal_connect_swapped(gui.table_results, "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.results);

    return outcome;
}

static GwyResults*
create_results(ModuleArgs *args, GwyFile *data, gint id)
{
    GwyResults *results = gwy_results_new();

    gwy_results_add_header(results, N_("Indentation"));
    gwy_results_add_value_str(results, "file", N_("File"));
    gwy_results_add_value_str(results, "image", N_("Image"));
    gwy_results_add_separator(results);

    gwy_results_add_value_x(results, "x0", N_("Imprint center x"));
    gwy_results_add_value_x(results, "y0", N_("Imprint center y"));
    gwy_results_add_value_z(results, "zmin", N_("Center value"));
    gwy_results_add_value_z(results, "zmax", N_("Maximum"));
    /* The units must be all the same anyway... */
    gwy_results_add_value(results, "Asurf_imp", N_("Imprint surface area"), "power-x", 2, NULL);
    gwy_results_add_value(results, "Aproj_imp", N_("Imprint projected area"), "power-x", 2, NULL);
    gwy_results_add_value(results, "Acontact", N_("Contact area"), "power-x", 2, NULL);
    gwy_results_add_value(results, "Vimp", N_("Imprint volume"), "power-x", 2, "power-z", 1, NULL);
    gwy_results_add_value(results, "Vpileup", N_("Pile-up volume"), "power-x", 2, "power-z", 1, NULL);
    gwy_results_add_value(results, "Asurf_pileup", N_("Pile-up surface area"), "power-x", 2, NULL);
    gwy_results_add_value(results, "Aproj_pileup", N_("Pile-up projected area"), "power-x", 2, NULL);
    gwy_results_add_value(results, "Asurf_in", N_("Inner pile-up surface area"), "power-x", 2, NULL);
    gwy_results_add_value(results, "Aproj_in", N_("Inner pile-up projected area"), "power-x", 2, NULL);
    gwy_results_add_value(results, "Asurf_out", N_("Outer pile-up surface area"), "power-x", 2, NULL);
    gwy_results_add_value(results, "Aproj_out", N_("Outer pile-up projected area"), "power-x", 2, NULL);

    gwy_results_bind_formats(results, "x0", "y0", NULL);
    gwy_results_bind_formats(results, "zmin", "zmax", NULL);
    gwy_results_bind_formats(results,
                             "Asurf_imp", "Aproj_imp", "Acontact", "Asurf_pileup", "Aproj_pileup",
                             "Asurf_in", "Aproj_in", "Asurf_out", "Aproj_out",
                             NULL);
    gwy_results_bind_formats(results, "Vimp", "Vpileup", NULL);

    gwy_results_set_unit(results, "x", gwy_field_get_unit_xy(args->field));
    gwy_results_set_unit(results, "z", gwy_field_get_unit_z(args->field));
    gwy_results_fill_filename(results, "file", data);
    gwy_results_fill_data_name(results, "image", data, GWY_FILE_IMAGE, id);

    return results;
}

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

    if (id < 0 || id == PARAM_DISPLAY) {
        IndentDisplayType display = gwy_params_get_enum(params, PARAM_DISPLAY);
        GwyDataView *dataview = GWY_DATA_VIEW(gui->dataview);

        if (display == RESULT_NOTHING)
            gwy_data_view_set_mask(dataview, NULL);
        else
            gwy_data_view_set_mask(dataview, args->result[display]);
    }
    if (id < 0 || id == PARAM_INDENTER) {
        GwyIndenterType indenter = gwy_params_get_enum(params, PARAM_INDENTER);
        gint nsides = indenter;

        gwy_param_table_set_sensitive(gui->table_param, PARAM_PHI_TOL, !!nsides);
        if (nsides)
            gwy_param_table_slider_restrict_range(gui->table_param, PARAM_PHI_TOL, 0.0, G_PI/nsides);
    }

    if (id != PARAM_MASK_COLOR && id != PARAM_REPORT_STYLE && id != PARAM_DISPLAY)
        gwy_dialog_invalidate(GWY_DIALOG(gui->dialog));
}

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

    execute(args);
    gwy_field_data_changed(args->adjusted);
    for (guint i = 1; i < RESULT_NTYPES; i++)
        gwy_nield_data_changed(args->result[i]);
    gwy_results_fill_values(gui->results,
                            "x0", imp->x0, "y0", imp->y0, "zmin", imp->zmin, "zmax", imp->zmax,
                            "Aproj_imp", imp->Aproj_imp, "Asurf_imp", imp->Asurf_imp, "Acontact", imp->Acontact,
                            "Vimp", imp->Vimp, "Vpileup", imp->Vpileup,
                            "Asurf_pileup", imp->Asurf_pileup, "Aproj_pileup", imp->Aproj_pileup,
                            "Aproj_in", imp->Aproj_in, "Asurf_in", imp->Asurf_in,
                            "Aproj_out", imp->Aproj_out, "Asurf_out", imp->Asurf_out,
                            NULL);
    gwy_param_table_results_fill(gui->table_results, WIDGET_RESULTS);
    gwy_dialog_have_result(GWY_DIALOG(gui->dialog));
}

static void
execute(ModuleArgs *args)
{
    GwyParams *params = args->params;
    ImprintParameters *imp = &args->imp;
    GwyField *adjusted = args->adjusted, *xder = args->xder, *yder = args->yder;
    GwyNield *imprint = args->result[RESULT_IMPRINT];
    GwyNield *above = args->result[RESULT_ABOVE];
    GwyNield *below = args->result[RESULT_BELOW];
    GwyNield *impfaces = args->result[RESULT_IMPRINT_FACES];
    GwyNield *facesborder = args->result[RESULT_FACES_BORDER];
    GwyNield *pileup = args->result[RESULT_PILEUP];
    GwyNield *innerpileup = args->result[RESULT_INNER_PILEUP];
    GwyNield *outerpileup = args->result[RESULT_OUTER_PILEUP];
    gboolean do_level = gwy_params_get_boolean(params, PARAM_DO_LEVEL);
    gdouble exterior = gwy_params_get_double(params, PARAM_EXTERIOR);
    gdouble plane_tol = gwy_params_get_double(params, PARAM_PLANE_TOL);
    gdouble phi_tol = gwy_params_get_double(params, PARAM_PHI_TOL);
    GwyIndenterType indenter = gwy_params_get_enum(params, PARAM_INDENTER);
    gint xres = gwy_field_get_xres(args->field), yres = gwy_field_get_yres(args->field);
    gint nsides = indenter;
    gdouble points[2*5];
    gdouble dx = gwy_field_get_dx(adjusted), dy = gwy_field_get_dy(adjusted);

    /* Level (if requested) using exterior and find the base plane and its rms. */
    gwy_field_copy_data(args->field, adjusted);
    level_using_exterior(adjusted, args->result[RESULT_EXTERIOR], exterior/100.0, do_level);
    if (!gwy_field_get_local_minima_list(adjusted, &imp->x0, &imp->y0, &imp->zmin, 1, 0, G_MAXDOUBLE, TRUE)) {
        g_printerr("cannot find any minimum! fixme to handle this!\n");
        return;
    }
    imp->zmax = gwy_field_max(adjusted);
    imp->rms_base = gwy_field_area_rms(adjusted, args->result[RESULT_EXTERIOR], GWY_MASK_INCLUDE, 0, 0, xres, yres);
    plane_tol *= imp->rms_base;

    /* Calculate slopes and find the imprint face.
     * Use the FACES_BORDER mask as a temporary buffer. */
    GwyField *atanfield = gwy_field_new_alike(args->field, FALSE);
    GwyNield *maskbuf = facesborder;
    compute_slopes(adjusted, 5, xder, yder, atanfield);
    gwy_nield_clear(impfaces);
    imp->phi = find_prevalent_phi(adjusted, impfaces, xder, yder, atanfield);
    gdouble search_to_z = imp->zmin - 0.3*imp->zmin;
    gwy_debug("search side from to %g within [%g..0.0]", search_to_z, imp->zmin);
    if (nsides) {
        for (gint i = 0; i < nsides; i++) {
            gdouble phi = gwy_canonicalize_angle(imp->phi + 2.0*G_PI/nsides*i, TRUE, TRUE);
            gdouble xc = imp->x0, yc = imp->y0, theta0, phi0;
            GwyXYZ v;

            find_imprint_side_centre(adjusted, search_to_z, phi, &xc, &yc);
            points[2*i + 2] = xc*dx;
            points[2*i + 3] = yc*dy;
            calc_mean_normal(xder, yder, xc, yc, 4.5, &v);
            phi0 = atan2(v.y, v.x);
            theta0 = atan(hypot(v.x, v.y));
            gwy_debug("mean normal phi0=%g, theta0=%g", gwy_rad2deg(phi0), gwy_rad2deg(theta0));
            mark_facet(xder, yder, maskbuf, theta0, phi0, phi_tol);
            gwy_nield_isolate_at(maskbuf, (gint)floor(xc), (gint)floor(yc), NULL);
            gwy_nield_fill_voids(maskbuf, TRUE);
            gwy_nield_union(impfaces, maskbuf);
        }
    }
    g_object_unref(atanfield);

    /* Mark various derived regions. */
    gwy_nield_mark_by_threshold(above, adjusted, plane_tol, TRUE);
    gwy_nield_mark_by_threshold(below, adjusted, -plane_tol, FALSE);
    gwy_nield_copy_data(above, args->result[RESULT_PLANE]);
    gwy_nield_union(args->result[RESULT_PLANE], below);
    gwy_nield_invert(args->result[RESULT_PLANE]);

    gwy_nield_copy_data(below, imprint);
    gwy_nield_isolate_at(imprint, (gint)floor(imp->x0), (gint)floor(imp->y0), NULL);
    gwy_nield_fill_voids(imprint, TRUE);
    /* FIXME */
    if (!nsides)
        gwy_nield_copy_data(imprint, impfaces);

    mark_pileup(above, imprint, pileup);
    gwy_nield_copy_data(pileup, innerpileup);
    gwy_nield_intersect(innerpileup, impfaces);
    gwy_nield_copy_data(pileup, outerpileup);
    gwy_nield_merge(outerpileup, impfaces, GWY_MERGE_SUBTRACT);

    gint ksize = GWY_ROUND(sqrt(xres*yres)/150.0);
    ksize = MAX(ksize, 2);
    GwyNield *kernel = gwy_nield_new(ksize, ksize);
    gwy_nield_elliptic_fill(kernel);
    GwyField *contact = gwy_field_new(xres, yres, xres, yres, FALSE);
    gwy_field_fill_mask(contact, impfaces, 0.0, 1.0);
    gwy_field_filter_min_max(contact, kernel, GWY_MIN_MAX_FILTER_CLOSING);
    g_object_unref(kernel);
    gwy_nield_mark_by_threshold(args->result[RESULT_CONTACT_AREA], contact, 0.5, TRUE);
    gwy_nield_fill_voids(args->result[RESULT_CONTACT_AREA], TRUE);
    g_object_unref(contact);

    gwy_nield_copy_data(impfaces, facesborder);
    gwy_nield_grow(facesborder, 1.0, GWY_DISTANCE_TRANSFORM_CHESS, FALSE);
    gwy_nield_merge(facesborder, impfaces, GWY_MERGE_SUBTRACT);

    /* Calculate surfaces and volumes. */
    calc_surface_and_volume(adjusted, impfaces, &imp->Aproj_imp, &imp->Asurf_imp, &imp->Vimp);
    imp->Vimp *= -1.0;
    calc_surface_and_volume(adjusted, pileup, &imp->Aproj_pileup, &imp->Asurf_pileup, &imp->Vpileup);
    calc_surface_and_volume(adjusted, args->result[RESULT_INNER_PILEUP], &imp->Aproj_in, &imp->Asurf_in, NULL);
    calc_surface_and_volume(adjusted, args->result[RESULT_OUTER_PILEUP], &imp->Aproj_out, &imp->Asurf_out, NULL);
    calc_surface_and_volume(adjusted, args->result[RESULT_CONTACT_AREA], &imp->Acontact, NULL, NULL);

    /* Convert pixel locations to real coordinates. */
    imp->x0 *= dx;
    imp->y0 *= dy;

    if (args->selection) {
        points[0] = imp->x0;
        points[1] = imp->y0;
        gwy_selection_set_data(args->selection, 1 + nsides, points);
    }
}

/* Always make average height over the exterior zero.  Plane-level only if requested. */
static void
level_using_exterior(GwyField *field, GwyNield *mask, gdouble wfrac, gboolean do_level)
{
    gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
    gint wx = MAX(GWY_ROUND(xres*wfrac), 1);
    gint wy = MAX(GWY_ROUND(yres*wfrac), 1);
    gdouble a, bx, by;

    gwy_nield_clear(mask);
    gwy_nield_area_fill(mask, NULL, GWY_MASK_IGNORE, 0, 0, xres, wy, 1);
    gwy_nield_area_fill(mask, NULL, GWY_MASK_IGNORE, 0, 0, wx, yres, 1);
    gwy_nield_area_fill(mask, NULL, GWY_MASK_IGNORE, 0, yres-wy, xres, wy, 1);
    gwy_nield_area_fill(mask, NULL, GWY_MASK_IGNORE, xres-wx, 0, wx, yres, 1);
    if (do_level) {
        gwy_field_area_fit_plane(field, mask, GWY_MASK_INCLUDE, 0, 0, xres, yres, &a, &bx, &by);
        gwy_debug("fitted plane bx=%g, by=%g", bx, by);
        gwy_field_plane_level(field, a, bx, by);
    }
    a = gwy_field_area_mean(field, mask, GWY_MASK_INCLUDE, 0, 0, xres, yres);
    gwy_field_add(field, -a);
}

static void
compute_slopes(GwyField *field, gint kernel_size,
               GwyField *xder, GwyField *yder, GwyField *atanfield)
{
    gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
    GwyPlaneFitQuantity quantites[2] = { GWY_PLANE_FIT_BX, GWY_PLANE_FIT_BY };
    GwyField *fields[2] = { xder, yder };
    gint n = xres*yres;

    if (!kernel_size) {
        gwy_field_derivative_filter(field, xder, GWY_DERIVATIVE_FILTER_XDER, TRUE);
        gwy_field_derivative_filter(field, yder, GWY_DERIVATIVE_FILTER_YDER, TRUE);
    }
    else {
        gwy_field_fit_local_planes(field, kernel_size, 2, quantites, fields);
        gwy_field_multiply(xder, 1.0/gwy_field_get_dx(field));
        gwy_field_multiply(yder, 1.0/gwy_field_get_dy(field));
    }

    const gdouble *xd = gwy_field_get_data_const(xder);
    const gdouble *yd = gwy_field_get_data_const(yder);
    gdouble *a = gwy_field_get_data(atanfield);
#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            shared(a,xd,yd,n)
#endif
    for (gint i = 0; i < n; i++) {
        a[i] = gwy_canonicalize_angle(atan2(-yd[i], xd[i]), TRUE, TRUE);
    }
}

static gdouble
find_prevalent_phi(GwyField *field, GwyNield *exclmask,
                   GwyField *xder, GwyField *yder, GwyField *atanfield)
{
    gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
    const gdouble *xd = gwy_field_get_data_const(xder);
    const gdouble *yd = gwy_field_get_data_const(yder);
    const gint *m = gwy_nield_get_data_const(exclmask);
    const gdouble *a = gwy_field_get_data_const(atanfield);

    gint n = xres*yres;
    gint size = n - gwy_nield_count(exclmask);
    size = (gint)floor(5.49*cbrt(size) + 0.5);
    size = MAX(size, 24);
    gwy_debug("phi dist size %d", size);

    GwyLine *phidist = gwy_line_new(size, 2.0*G_PI, TRUE);
    gdouble *data = gwy_line_get_data(phidist);
    for (gint i = 0; i < n; i++) {
        if (m[i] <= 0) {
            gdouble d = xd[i]*xd[i] + yd[i]*yd[i];
            gint iphi = floor(size*a[i]/(2.0*G_PI));
            iphi = CLAMP(iphi, 0, size-1);
            data[iphi] += sqrt(d);
        }
    }
    gint i = (gint)gwy_line_max_pos_i(phidist);

    gwy_debug("phi maximum at %d (%g deg)", i, 360.0*(i + 0.5)/size);
    gdouble y[3];
    y[0] = gwy_line_get_val(phidist, (i + size-1) % size);
    y[1] = gwy_line_get_val(phidist, i);
    y[2] = gwy_line_get_val(phidist, (i+1) % size);
    gdouble x = 0.0;
    gwy_math_refine_maximum_1d(y, &x);
    gdouble phi = gwy_line_get_dx(phidist)*(i + 0.5 + x);
    gwy_debug("refined phi to %g deg", gwy_rad2deg(phi));

    g_object_unref(phidist);

    return gwy_canonicalize_angle(phi + G_PI, TRUE, TRUE);
}

static void
find_imprint_side_centre(GwyField *field,
                         gdouble search_to_z, gdouble phi,
                         gdouble *xcentre, gdouble *ycentre)
{
    gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
    gdouble x, y, cphi = cos(phi), sphi = sin(phi);
    const gdouble *d = gwy_field_get_data_const(field);
    gint i, j, k;

    gwy_debug("direction %g deg", gwy_rad2deg(phi));

    /* Go from the minimum until we reach search_to_z. */
    k = 1;
    while (TRUE) {
        x = *xcentre - 0.5*cphi*k;
        y = *ycentre + 0.5*sphi*k;
        j = (gint)floor(x);
        i = (gint)floor(y);
        if (i < 0 || i >= yres || j < 0 || j >= xres)
            break;
        if (d[i*xres + j] >= search_to_z)
            break;
        k++;
    }

    k--;
    *xcentre = *xcentre - 0.5*cphi*k;
    *ycentre = *ycentre + 0.5*sphi*k;
}

static void
calc_mean_normal(GwyField *xder, GwyField *yder, gdouble x, gdouble y, gdouble r, GwyXYZ *v)
{
    const gdouble *xd = gwy_field_get_data_const(xder);
    const gdouble *yd = gwy_field_get_data_const(yder);
    gint xres = gwy_field_get_xres(xder), yres = gwy_field_get_yres(yder);
    gint j = (gint)floor(x);
    gint i = (gint)floor(y);
    gint ii, jj, k, G_GNUC_UNUSED n;
    gdouble s;

    /* Calculate average normal around the point. */
    gwy_clear(v, 1);
    n = 0;
    for (ii = -r-1; ii <= r+1; ii++) {
        if (ii + i < 0 || ii + i >= yres)
            continue;
        for (jj = -r-1; jj <= r+1; jj++) {
            if (jj + j < 0 || jj + j >= xres)
                continue;
            if ((jj + j - x)*(jj + j - x) + (ii + i - y)*(ii + i - y) > r)
                continue;

            k = (ii + i)*xres + (jj + j);
            v->x += xd[k];
            v->y += yd[k];
            v->z += 1.0;
            n++;
        }
    }
    s = sqrt(v->x*v->x + v->y*v->y + v->z*v->z);
    v->x /= s;
    v->y /= s;
    v->z /= s;
    gwy_debug("mean local normal (%g,%g,%g) direction %g deg (%d samples)",
              v->x, v->y, v->z, gwy_rad2deg(gwy_canonicalize_angle(atan2(-v->y, v->x), TRUE, TRUE)), n);
}

static void
mark_facet(GwyField *xder, GwyField *yder, GwyNield *mask,
           gdouble theta0, gdouble phi0, gdouble tolerance)
{
    gdouble ctol = cos(tolerance), cth0 = cos(theta0), sth0 = sin(theta0), cphi0 = cos(phi0), sphi0 = sin(phi0);
    const gdouble *xd = gwy_field_get_data_const(xder);
    const gdouble *yd = gwy_field_get_data_const(yder);
    gint *md = gwy_nield_get_data(mask);
    guint n = gwy_field_get_xres(xder)*gwy_field_get_yres(xder);

#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            shared(xd,yd,md,n,cth0,sth0,cphi0,sphi0,ctol)
#endif
    for (gint i = 0; i < n; i++) {
        gdouble stheta2 = xd[i]*xd[i] + yd[i]*yd[i];
        gdouble stheta = sqrt(stheta2);
        gdouble ctheta = sqrt(1.0 - fmin(stheta2, 1.0));
        gdouble cphi = xd[i]/stheta, sphi = yd[i]/stheta;
        gdouble cro = cth0*ctheta + sth0*stheta*(cphi*cphi0 + sphi*sphi0);
        md[i] = (cro >= ctol);
    }
}

#if 0
static gboolean
fit_pyramid(GwyField *field, GwyField *impfaces,
            GwyIndenterType indenter, PyramidParameters *pyrpar)
{
    static const GwyEnum indenters[] = {
        //{ N_("Sphere"),              INDENTER_SPHERE,   },
        { N_("Pyramid (3-sided)"),   INDENTER_PYRAMID3, },
        { N_("Pyramid (rectangle)"), INDENTER_PYRAMID4, },
    };
    const gchar *name;
    GwyShapeFitPreset *preset;
    GwyNLFitter *fitter = NULL;
    GwySurface *surface = NULL;
    gdouble *params = NULL;
    gint npoints, nparams, i, nsides = indenter;
    const GwyXYZ *points;
    gdouble rss, L, a, phi;
    gboolean ok = FALSE;

    if (!(name = gwy_enum_to_string(indenter, indenters, G_N_ELEMENTS(indenters))))
        return FALSE;
    if (!(preset = gwy_inventory_get_item(gwy_shape_fit_presets(), name)))
        return FALSE;

    surface = gwy_surface_new();
    gwy_surface_set_from_field(surface, field);
    points = gwy_surface_get_data_const(surface);
    npoints = gwy_surface_get_n_points(surface);
    nparams = gwy_shape_fit_preset_get_n_params(preset);
    params = g_new(gdouble, nparams);
    gwy_shape_fit_preset_setup(preset, points, npoints, params);
    if (!gwy_shape_fit_preset_guess(preset, points, npoints, params)) {
        gwy_debug("guess failed");
        goto end;
    }
    gwy_debug("guess apex (%g,%g) L=%g, h=%g, phi=%g", params[0], params[1], params[4], params[3], params[6]);
    fitter = gwy_shape_fit_preset_quick_fit(preset, NULL, points, npoints, params, NULL, &rss);
    if (!fitter || rss < 0.0) {
        gwy_debug("quick fit failed");
        goto end;
    }
    gwy_debug("quick apex (%g,%g) L=%g, h=%g, phi=%g", params[0], params[1], params[4], params[3], params[6]);

    pyrpar->apex.x = params[0];
    pyrpar->apex.y = params[1];
    L = params[3];
    a = params[5];
    phi = params[6];
    for (i = 0; i < nsides; i++) {
    }

    ok = TRUE;

end:
    if (fitter)
        gwy_nl_fitter_free(fitter);
    g_clear_object(&surface);
    g_free(params);

    return ok;
}
#endif

/* Keep connected components not too far from imprint in pileup. */
static void
mark_pileup(GwyNield *above, GwyNield *imprint, GwyNield *pileup)
{
    gint xres = gwy_nield_get_xres(imprint), yres = gwy_nield_get_yres(imprint);
    gint n = xres*yres;

    gdouble maxdist = fmax(0.02*sqrt(gwy_nield_count(imprint)), 2.1);
    gwy_debug("using maxdist %g", maxdist);

    gwy_nield_copy_data(imprint, pileup);
    gwy_nield_invert(pileup);
    GwyField *distances = gwy_field_new(xres, yres, xres, yres, FALSE);
    gwy_nield_distance_transform(pileup, distances, GWY_DISTANCE_TRANSFORM_EUCLIDEAN, TRUE, FALSE);
    const gdouble *d = gwy_field_get_data(distances);

    gint ngrains = gwy_nield_number_contiguous(above);
    gint *grains = gwy_nield_get_data(above);
    gint *keep = g_new0(gint, ngrains+1);
    for (gint i = 0; i < n; i++)
        keep[grains[i]] |= (d[i] > 0.0 && d[i] < maxdist);
    keep[0] = 0;
    g_object_unref(distances);

    gint *m = gwy_nield_get_data(pileup);
    for (gint i = 0; i < n; i++)
        m[i] = keep[grains[i]];

    g_free(keep);
}

static void
calc_surface_and_volume(GwyField *field, GwyNield *mask,
                        gdouble *Aproj, gdouble *Asurf, gdouble *V)
{
    gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
    gdouble dA = gwy_field_get_dx(field)*gwy_field_get_dy(field);

    if (Aproj)
        *Aproj = dA*gwy_nield_count(mask);
    if (Asurf)
        *Asurf = gwy_field_area_surface_area(field, mask, GWY_MASK_INCLUDE, 0, 0, xres, yres);
    if (V)
        *V = gwy_field_area_volume(field, mask, GWY_MASK_INCLUDE, GWY_FIELD_VOLUME_DEFAULT, 0, 0, xres, yres);
}

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