/*
 *  $Id: grain_filter.c 30065 2026-06-15 09:52:12Z 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)

enum {
    NQUANTITIES = 3,
};

enum {
    RESPONSE_SET_A = GWY_RESPONSE_USER,
    RESPONSE_SET_B,
    RESPONSE_SET_C,
};

enum {
    PARAM_LOGICAL,
    PARAM_QUANTITY,
    PARAM_QUANTITY_A,
    PARAM_QUANTITY_B,
    PARAM_QUANTITY_C,
    PARAM_UPDATE,
    PARAM_EXPANDED,
    PARAM_MASK_COLOR,
    PARAM_RANGE_A_LOWER,
    PARAM_RANGE_A_UPPER,
    PARAM_RANGE_B_LOWER,
    PARAM_RANGE_B_UPPER,
    PARAM_RANGE_C_LOWER,
    PARAM_RANGE_C_UPPER,

    HEADER_A,
    HEADER_B,
    HEADER_C,
    BUTTON_SET_A,
    BUTTON_SET_B,
    BUTTON_SET_C,
};

typedef enum {
    GRAIN_LOGICAL_A,
    GRAIN_LOGICAL_A_AND_B,
    GRAIN_LOGICAL_A_OR_B,
    GRAIN_LOGICAL_A_AND_B_AND_C,
    GRAIN_LOGICAL_A_OR_B_OR_C,
    GRAIN_LOGICAL_A_AND_B__OR__C,
    GRAIN_LOGICAL_A_OR_B__AND__C,
    GRAIN_LOGICAL_A__AND__B_OR_C,
    GRAIN_LOGICAL_A__OR__B_AND_C,
    GRAIN_LOGICAL_NTYPES,
} GrainLogical;

typedef struct {
    const gchar *name;
    gint quantity;
    gint lower;
    gint upper;
    gint button;
    gint response;
    gint header;
} QuantityIds;

typedef struct {
    const gchar *quantity;
    gdouble lower;
    gdouble upper;
} RangeRecord;

typedef struct {
    GwyParams *params;
    GwyField *field;
    GwyNield *grains;
    GwyNield *result;

    GHashTable *range_records;
    gboolean units_equal;

    /* (Pre-)Calculated data.
     * FIXME: The sorted & unqiue values were only used for the highly non-linear sliders which mover over all
     * values occurring in the grain value set. We currently use plain linear sliders and could just cache the
     * minimum and maximum for each quantity. */
    GPtrArray *valuedata;
    GPtrArray *sortedvaluedata;
    guint *nuniqvalues;
    guint ngrains;
} ModuleArgs;

typedef struct {
    ModuleArgs *args;

    GtkWidget *dialog;
    GtkWidget *dataview;
    GtkWidget *values;
    GwyParamTable *table;

    gboolean setting_up_quantity;
} 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             quantity_selected   (ModuleGUI *gui,
                                             GtkTreeSelection *selection);
static void             dialog_response     (ModuleGUI *gui,
                                             gint response);
static void             preview             (gpointer user_data);
static void             set_up_quantity     (ModuleGUI *gui,
                                             guint i);
static void             init_grains         (ModuleArgs *args);
static void             ensure_grain_values (ModuleArgs *args,
                                             guint i);
static GHashTable*      load_range_records  (void);
static void             save_range_records  (GHashTable *range_records);

static const GrainLogical logical_limits[NQUANTITIES+1] = {
    GRAIN_LOGICAL_A,
    GRAIN_LOGICAL_A_AND_B,
    GRAIN_LOGICAL_A_AND_B_AND_C,
};

static const QuantityIds qinfo[NQUANTITIES] = {
    { "A", PARAM_QUANTITY_A, PARAM_RANGE_A_LOWER, PARAM_RANGE_A_UPPER, BUTTON_SET_A, RESPONSE_SET_A, HEADER_A },
    { "B", PARAM_QUANTITY_B, PARAM_RANGE_B_LOWER, PARAM_RANGE_B_UPPER, BUTTON_SET_B, RESPONSE_SET_B, HEADER_B },
    { "C", PARAM_QUANTITY_C, PARAM_RANGE_C_LOWER, PARAM_RANGE_C_UPPER, BUTTON_SET_C, RESPONSE_SET_C, HEADER_C },
};

static GwyModuleInfo module_info = {
    GWY_MODULE_ABI_VERSION,
    &module_register,
    N_("Filters grains by their properties, using logical expressions and thresholds."),
    "Yeti <yeti@gwyddion.net>",
    "2.1",
    "David Nečas (Yeti)",
    "2014",
};

GWY_MODULE_QUERY2(module_info, grain_filter)

static gboolean
module_register(void)
{
    gwy_image_func_register("grain_filter",
                            module_main,
                            N_("/_Grains/_Filter..."),
                            GWY_ICON_GRAINS_REMOVE,
                            RUN_MODES,
                            GWY_MENU_FLAG_IMAGE | GWY_MENU_FLAG_IMAGE_MASK,
                            N_("Filter grains by their properties"));

    return TRUE;
}

static GwyParamDef*
define_module_params(void)
{
    static const GwyEnum logicals[] = {
        { "A",           GRAIN_LOGICAL_A,              },
        { "A ∧ B",       GRAIN_LOGICAL_A_AND_B,        },
        { "A ∨ B",       GRAIN_LOGICAL_A_OR_B,         },
        { "A ∧ B ∧ C",   GRAIN_LOGICAL_A_AND_B_AND_C,  },
        { "A ∨ B ∨ C",   GRAIN_LOGICAL_A_OR_B_OR_C,    },
        { "(A ∧ B) ∨ C", GRAIN_LOGICAL_A_AND_B__OR__C, },
        { "(A ∨ B) ∧ C", GRAIN_LOGICAL_A_OR_B__AND__C, },
        { "A ∧ (B ∨ C)", GRAIN_LOGICAL_A__AND__B_OR_C, },
        { "A ∨ (B ∧ C)", GRAIN_LOGICAL_A__OR__B_AND_C, },
    };
    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_gwyenum(paramdef, PARAM_LOGICAL, "logical", _("Keep grains satisfying"),
                              logicals, G_N_ELEMENTS(logicals), GRAIN_LOGICAL_A);
    /* This is simply what is selected in the quantity list. */
    gwy_param_def_add_resource(paramdef, PARAM_QUANTITY, "quantity", NULL, gwy_grain_values(), "Pixel area");
    /* These are the three quantities we use for the filtering. */
    gwy_param_def_add_resource(paramdef, PARAM_QUANTITY_A, "quantityA", NULL, gwy_grain_values(), "Pixel area");
    gwy_param_def_add_resource(paramdef, PARAM_QUANTITY_B, "quantityB", NULL, gwy_grain_values(), "Pixel area");
    gwy_param_def_add_resource(paramdef, PARAM_QUANTITY_C, "quantityC", NULL, gwy_grain_values(), "Pixel area");
    gwy_param_def_add_instant_updates(paramdef, PARAM_UPDATE, "update", NULL, FALSE);
    gwy_param_def_add_grain_groups(paramdef, PARAM_EXPANDED, "expanded", NULL, 1 << GWY_GRAIN_GROUP_AREA);
    gwy_param_def_add_mask_color(paramdef, PARAM_MASK_COLOR, NULL, NULL);
    /* These are not saved to settings. We remember ranges of all quantities in rr data. */
    for (guint i = 0; i < NQUANTITIES; i++) {
        gwy_param_def_add_double(paramdef, qinfo[i].lower, NULL, _("Lower threshold"),
                                 -G_MAXDOUBLE, G_MAXDOUBLE, 0.0);
        gwy_param_def_add_double(paramdef, qinfo[i].upper, NULL, _("Upper threshold"),
                                 -G_MAXDOUBLE, G_MAXDOUBLE, G_MAXDOUBLE);
    }
    return paramdef;
}

static void
module_main(GwyFile *data, GwyRunModeFlags mode)
{
    g_return_if_fail(mode & RUN_MODES);

    ModuleArgs args;
    gwy_clear1(args);

    GQuark mquark;
    gint id;
    gwy_data_browser_get_current(GWY_APP_FIELD, &args.field,
                                 GWY_APP_MASK_FIELD, &args.grains,
                                 GWY_APP_MASK_FIELD_KEY, &mquark,
                                 GWY_APP_FIELD_ID, &id,
                                 0);
    g_return_if_fail(args.field && args.grains);

    if (gwy_nield_max(args.grains) < 1) {
        GtkWidget *dialog = gtk_message_dialog_new(gwy_data_browser_get_window_for_data(data, GWY_FILE_IMAGE, id),
                                                   GTK_DIALOG_DESTROY_WITH_PARENT,
                                                   GTK_MESSAGE_ERROR,
                                                   GTK_BUTTONS_OK,
                                                   _("There are no grains to filter."));
        if (gtk_dialog_run(GTK_DIALOG(dialog)) != GTK_RESPONSE_NONE)
            gtk_widget_destroy(dialog);
        return;
    }

    args.result = gwy_nield_new_alike(args.grains);
    GwyParams *params = args.params = gwy_params_new_from_settings(define_module_params());

    args.range_records = load_range_records();
    for (guint i = 0; i < NQUANTITIES; i++) {
        const gchar *quantity_name = gwy_params_get_string(params, qinfo[i].quantity);
        RangeRecord *rr = g_hash_table_lookup(args.range_records, quantity_name);
        if (rr) {
            gwy_params_set_double(params, qinfo[i].lower, rr->lower);
            gwy_params_set_double(params, qinfo[i].upper, rr->upper);
        }
    }

    GwyUnit *siunitxy = gwy_field_get_unit_xy(args.field);
    GwyUnit *siunitz = gwy_field_get_unit_z(args.field);
    args.units_equal = gwy_unit_equal(siunitxy, siunitz);

    init_grains(&args);

    GwyDialogOutcome outcome = GWY_DIALOG_PROCEED;
    if (mode == GWY_RUN_INTERACTIVE) {
        outcome = run_gui(&args, data, id);
        save_range_records(args.range_records);
        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);
    gwy_dict_set_object(GWY_DICT(data), mquark, args.result);
    gwy_log_add(data, GWY_FILE_IMAGE, id, id);

end:
    g_object_unref(args.params);
    g_object_unref(args.result);
    g_hash_table_destroy(args.range_records);
    g_ptr_array_free(args.valuedata, TRUE);
    g_ptr_array_free(args.sortedvaluedata, TRUE);
    g_free(args.nuniqvalues);
}

static GwyDialogOutcome
run_gui(ModuleArgs *args, GwyFile *data, gint id)
{
    ModuleGUI gui;
    gwy_clear1(gui);
    gui.args = args;

    gui.dialog = gwy_dialog_new(_("Filter Grains"));
    GwyDialog *dialog = GWY_DIALOG(gui.dialog);
    gwy_dialog_add_stock_buttons(dialog, GWY_STOCK_UPDATE, GWY_STOCK_CANCEL, GWY_STOCK_OK, 0);

    gui.dataview = gwy_create_preview(args->field, args->result, PREVIEW_SIZE);
    gwy_setup_data_view(GWY_DATA_VIEW(gui.dataview), data, GWY_FILE_IMAGE, id,
                        GWY_FILE_ITEM_PALETTE | GWY_FILE_ITEM_MASK_COLOR
                        | GWY_FILE_ITEM_RANGE | GWY_FILE_ITEM_REAL_SQUARE);
    GtkWidget *hbox = gwy_create_dialog_preview_hbox(GTK_DIALOG(dialog), GWY_DATA_VIEW(gui.dataview), FALSE);

    GtkWidget *scwin = gtk_scrolled_window_new(NULL, NULL);
    gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scwin), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
    gtk_box_pack_start(GTK_BOX(hbox), scwin, TRUE, TRUE, 0);

    gui.values = gwy_grain_value_tree_view_new(FALSE, "name", "symbol_markup", NULL);
    GtkTreeView *treeview = GTK_TREE_VIEW(gui.values);
    gtk_widget_set_size_request(scwin, 240, -1);
    gtk_tree_view_set_headers_visible(treeview, FALSE);
    GtkTreeSelection *selection = gtk_tree_view_get_selection(treeview);
    gtk_tree_selection_set_mode(selection, GTK_SELECTION_BROWSE);
    gwy_grain_value_tree_view_set_same_units(treeview, args->units_equal);
    gwy_grain_value_tree_view_set_expanded_groups(treeview, gwy_params_get_flags(args->params, PARAM_EXPANDED));
    gwy_grain_value_tree_view_select(treeview, GWY_GRAIN_VALUE(gwy_params_get_resource(args->params, PARAM_QUANTITY)));
    gtk_container_add(GTK_CONTAINER(scwin), gui.values);

    GwyParamTable *table = gui.table = gwy_param_table_new(args->params);
    for (guint i = 0; i < NQUANTITIES; i++) {
        gwy_param_table_append_button(table, qinfo[i].button, i ? qinfo[i-1].button : -1,
                                      qinfo[i].response, qinfo[i].name);
    }
    gwy_param_table_set_label(table, qinfo[0].button, _("Set selected as"));

    gwy_param_table_append_combo(table, PARAM_LOGICAL);
    for (guint i = 0; i < NQUANTITIES; i++) {
        gwy_param_table_append_separator(table);
        gwy_param_table_append_header(table, qinfo[i].header, qinfo[i].name);
        gwy_param_table_append_slider(table, qinfo[i].lower);
        gwy_param_table_append_slider(table, qinfo[i].upper);
    }

    gwy_param_table_append_separator(table);
    gwy_param_table_append_checkbox(table, PARAM_UPDATE);
    gwy_param_table_append_image_mask_color(table, PARAM_MASK_COLOR, GWY_DATA_VIEW(gui.dataview), data, id);

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

    for (guint i = 0; i < NQUANTITIES; i++)
        set_up_quantity(&gui, i);

    g_signal_connect_swapped(table, "param-changed", G_CALLBACK(param_changed), &gui);
    g_signal_connect_swapped(dialog, "response", G_CALLBACK(dialog_response), &gui);
    g_signal_connect_swapped(selection, "changed", G_CALLBACK(quantity_selected), &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)
{
    if (gui->setting_up_quantity)
        return;

    ModuleArgs *args = gui->args;
    GwyParams *params = args->params;
    GwyParamTable *table = gui->table;

    if (id < 0 || id == PARAM_LOGICAL) {
        GrainLogical logical = gwy_params_get_enum(params, PARAM_LOGICAL);
        for (guint i = 0; i < NQUANTITIES; i++) {
            gboolean sens = (logical >= logical_limits[i]);

            gwy_param_table_set_sensitive(table, qinfo[i].button, sens);
            gwy_param_table_set_sensitive(table, qinfo[i].lower, sens);
            gwy_param_table_set_sensitive(table, qinfo[i].upper, sens);
            gwy_param_table_set_sensitive(table, qinfo[i].header, sens);
        }
        gwy_dialog_invalidate(GWY_DIALOG(gui->dialog));
    }

    for (guint i = 0; i < NQUANTITIES; i++) {
        if (id == qinfo[i].lower || id == qinfo[i].upper) {
            GwyResource *resource = gwy_params_get_resource(params, qinfo[i].quantity);
            const gchar *name = gwy_resource_get_name(resource);
            RangeRecord *rr = g_hash_table_lookup(args->range_records, (gpointer)name);
            if (!rr) {
                rr = g_new(RangeRecord, 1);
                rr->quantity = name;
                g_hash_table_insert(args->range_records, (gpointer)name, rr);
            }
            rr->lower = gwy_params_get_double(params, qinfo[i].lower);
            rr->upper = gwy_params_get_double(params, qinfo[i].upper);

            gwy_dialog_invalidate(GWY_DIALOG(gui->dialog));
            break;
        }
    }
}

static void
quantity_selected(ModuleGUI *gui, GtkTreeSelection *selection)
{
    GwyParams *params = gui->args->params;
    gwy_params_set_flags(params, PARAM_EXPANDED,
                         gwy_grain_value_tree_view_get_expanded_groups(GTK_TREE_VIEW(gui->values)));

    GtkTreeModel *model;
    GtkTreeIter iter;
    if (!gtk_tree_selection_get_selected(selection, &model, &iter))
        return;

    GwyGrainValue *gvalue;
    gtk_tree_model_get(model, &iter, GWY_GRAIN_STORE_COLUMN_ITEM, &gvalue, -1);
    gwy_params_set_resource(params, PARAM_QUANTITY, gwy_resource_get_name(GWY_RESOURCE(gvalue)));
    g_object_unref(gvalue);
}

static void
preview(gpointer user_data)
{
    ModuleGUI *gui = (ModuleGUI*)user_data;

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

static void
dialog_response(ModuleGUI *gui, gint response)
{
    GwyParams *params = gui->args->params;

    for (guint i = 0; i < NQUANTITIES; i++) {
        if (response == qinfo[i].response) {
            gwy_params_set_resource(params, qinfo[i].quantity, gwy_params_get_string(params, PARAM_QUANTITY));
            set_up_quantity(gui, i);
            return;
        }
    }
}

static void
set_up_quantity(ModuleGUI *gui, guint i)
{
    GwyParamTable *table = gui->table;
    ModuleArgs *args = gui->args;
    GwyInventory *inventory = gwy_grain_values();
    GwyResource *resource = gwy_params_get_resource(args->params, qinfo[i].quantity);
    const gchar *name = gwy_resource_get_name(resource);
    GwyGrainValue *gvalue = GWY_GRAIN_VALUE(resource);

    gui->setting_up_quantity++;

    ensure_grain_values(args, gwy_inventory_get_item_position(inventory, name));

    /* TRANSLATORS: %s is replaced with quantity label A, B or C. */
    gchar *s = g_strdup_printf(_("Condition %s: %s"), qinfo[i].name, _(name));
    gwy_param_table_set_label(table, qinfo[i].header, s);
    g_free(s);

    gdouble lower = -G_MAXDOUBLE, upper = G_MAXDOUBLE;
    RangeRecord *rr = g_hash_table_lookup(args->range_records, (gpointer)name);
    if (rr) {
        lower = rr->lower;
        upper = rr->upper;
    }

    guint qi = gwy_inventory_get_item_position(inventory, name);
    guint nuniq = args->nuniqvalues[qi];
    const gdouble *v = g_ptr_array_index(args->sortedvaluedata, qi);
    gdouble vmin = v[0], vmax = v[nuniq-1];
    lower = CLAMP(lower, vmin, vmax);
    upper = CLAMP(upper, vmin, vmax);

    GwyValueFormat *vf;
    if (gwy_strequal(name, "Pixel area")) {
        /* Special-case the pixel area format. */
        vf = gwy_value_format_new(1.0, 0, _("px"));
    }
    else {
        GwyField *field = args->field;
        GwyUnit *xyunit = gwy_field_get_unit_xy(field);
        GwyUnit *zunit = gwy_field_get_unit_z(field);
        GwyUnit *unit = gwy_unit_power_multiply(gwy_unit_new(NULL),
                                                xyunit, gwy_grain_value_get_power_xy(gvalue),
                                                zunit, gwy_grain_value_get_power_z(gvalue));
        vf = gwy_unit_get_format_with_digits(unit, GWY_UNIT_FORMAT_VFMARKUP, fmax(fabs(vmin), fabs(vmax)), 4,
                                             NULL);
        g_object_unref(unit);
    }

    /* FIXME: We used to have a non-linear transformation with an underlying integer parameter, just shown as a real
     * number in the entry. It is probably still possible with GwyParamTable sliders, but is it so helpful? */
    gwy_param_table_slider_set_factor(table, qinfo[i].lower, 1.0/vf->magnitude);
    gwy_param_table_slider_restrict_range(table, qinfo[i].lower, vmin, vmax);
    gwy_param_table_set_unitstr(table, qinfo[i].lower, vf->units);
    gwy_param_table_set_double(table, qinfo[i].lower, lower);
    if (rr)
        rr->lower = lower;

    gwy_param_table_slider_set_factor(table, qinfo[i].upper, 1.0/vf->magnitude);
    gwy_param_table_slider_restrict_range(table, qinfo[i].upper, vmin, vmax);
    gwy_param_table_set_unitstr(table, qinfo[i].upper, vf->units);
    gwy_param_table_set_double(table, qinfo[i].upper, upper);
    if (rr)
        rr->upper = upper;

    gwy_value_format_free(vf);

    gui->setting_up_quantity--;

    gwy_dialog_invalidate(GWY_DIALOG(gui->dialog));
}

static void
init_grains(ModuleArgs *args)
{
    GwyNield *grains = args->grains;

    args->ngrains = gwy_nield_max(grains);

    GwyInventory *inventory = gwy_grain_values();
    guint n = gwy_inventory_get_n_items(inventory);

    args->valuedata = g_ptr_array_new_with_free_func(g_free);
    g_ptr_array_set_size(args->valuedata, n);

    args->nuniqvalues = g_new0(guint, n);
    args->sortedvaluedata = g_ptr_array_new_with_free_func(g_free);
    g_ptr_array_set_size(args->sortedvaluedata, n);
}

static guint
uniq_values(gdouble *values, guint n)
{
    guint i, j;

    for (i = j = 0; i < n; i++) {
        if (i && (fabs(values[i] - values[j-1]) < 1e-12*(fabs(values[i]) + fabs(values[j-1]))))
            continue;

        values[j++] = values[i];
    }

    return j;
}

static void
ensure_grain_values(ModuleArgs *args, guint i)
{
    if (args->nuniqvalues[i])
        return;

    guint ngrains = args->ngrains;
    GwyGrainValue *gvalue = gwy_inventory_get_nth_item(gwy_grain_values(), i);
    gdouble *valdata = g_ptr_array_index(args->valuedata, i) = g_new(gdouble, ngrains + 1);
    gwy_nield_grain_values(args->grains, args->field, &gvalue, 1, &valdata);

    /* +1 to avoid the zeroth non-grain */
    gdouble *sorted = g_ptr_array_index(args->sortedvaluedata, i) = g_memdup(valdata + 1, ngrains*sizeof(gdouble));
    gwy_math_sort(sorted, ngrains);
    args->nuniqvalues[i] = uniq_values(sorted, ngrains);
}

static inline gboolean
check_threshold(gdouble v, gdouble lower, gdouble upper)
{
    return (lower <= upper ? v >= lower && v <= upper : v >= lower || v <= upper);
}

static void
execute(ModuleArgs *args)
{
    GwyParams *params = args->params;
    GwyNield *result = args->result;
    GrainLogical logical = gwy_params_get_enum(params, PARAM_LOGICAL);

    GwyInventory *inventory = gwy_grain_values();
    const gdouble *v[NQUANTITIES];
    RangeRecord ranges[NQUANTITIES];
    for (guint i = 0; i < NQUANTITIES; i++) {
        const gchar *quantity_name = gwy_params_get_string(params, qinfo[i].quantity);
        guint k = gwy_inventory_get_item_position(inventory, quantity_name);
        ensure_grain_values(args, k);
        g_return_if_fail(k < args->valuedata->len);
        v[i] = g_ptr_array_index(args->valuedata, k);
        ranges[i].quantity = quantity_name;
        ranges[i].lower = gwy_params_get_double(params, qinfo[i].lower);
        ranges[i].upper = gwy_params_get_double(params, qinfo[i].upper);
        RangeRecord *rr = g_hash_table_lookup(args->range_records, quantity_name);
        // Extend the range slightly when it seems the slider is indistinguishably close to the endpoints to prevent
        // failing to include the last/first grain.
        gdouble rrange = rr->upper - rr->lower;
        if (ranges[i].lower - rr->lower < 1e-6*rrange)
            ranges[i].lower = rr->lower - 1e-6*rrange;
        if (rr->upper - ranges[i].upper < 1e-6*rrange)
            ranges[i].upper = rr->upper + 1e-6*rrange;
    }

    guint ngrains = args->ngrains;
    gboolean *keep_grain = g_new(gboolean, ngrains+1);
    keep_grain[0] = FALSE;
    for (guint gno = 1; gno <= ngrains; gno++) {
        gboolean is_ok[NQUANTITIES];

        for (guint i = 0; i < NQUANTITIES; i++)
             is_ok[i] = check_threshold(v[i][gno], ranges[i].lower, ranges[i].upper);

        if (logical == GRAIN_LOGICAL_A)
            keep_grain[gno] = is_ok[0];
        else if (logical == GRAIN_LOGICAL_A_AND_B)
            keep_grain[gno] = is_ok[0] && is_ok[1];
        else if (logical == GRAIN_LOGICAL_A_OR_B)
            keep_grain[gno] = is_ok[0] || is_ok[1];
        else if (logical == GRAIN_LOGICAL_A_AND_B_AND_C)
            keep_grain[gno] = is_ok[0] && is_ok[1] && is_ok[2];
        else if (logical == GRAIN_LOGICAL_A_OR_B_OR_C)
            keep_grain[gno] = is_ok[0] || is_ok[1] || is_ok[2];
        else if (logical == GRAIN_LOGICAL_A_AND_B__OR__C)
            keep_grain[gno] = (is_ok[0] && is_ok[1]) || is_ok[2];
        else if (logical == GRAIN_LOGICAL_A_OR_B__AND__C)
            keep_grain[gno] = (is_ok[0] || is_ok[1]) && is_ok[2];
        else if (logical == GRAIN_LOGICAL_A__AND__B_OR_C)
            keep_grain[gno] = is_ok[0] && (is_ok[1] || is_ok[2]);
        else if (logical == GRAIN_LOGICAL_A__OR__B_AND_C)
            keep_grain[gno] = is_ok[0] || (is_ok[1] && is_ok[2]);
        else {
            g_assert_not_reached();
        }
    }

    gwy_nield_copy_data(args->grains, args->result);
    guint n = gwy_nield_get_xres(result) * gwy_nield_get_yres(result);
    gint *m = gwy_nield_get_data(result);
    for (guint k = 0; k < n; k++)
        m[k] = keep_grain[MAX(m[k], 0)];
    g_free(keep_grain);
    gwy_nield_number_contiguous(result);
}

static GHashTable*
load_range_records(void)
{
    GHashTable *range_records = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, g_free);

    gchar *buffer;
    gsize size;
    if (!gwy_module_data_load("grain_filter", "ranges", &buffer, &size, NULL))
        return range_records;

    GwyInventory *inventory = gwy_grain_values();
    gchar *p = buffer, *line;
    for (line = gwy_str_next_line(&p); line; line = gwy_str_next_line(&p)) {
        g_strstrip(line);
        if (!*line)
            continue;

        gchar *s = line, *end;
        gdouble lower = g_ascii_strtod(s, &end);
        s = end;
        gdouble upper = g_ascii_strtod(s, &end);
        if (end == s) {
            g_warning("Invalid grain_filter range record: %s.", line);
            continue;
        }
        s = end;
        g_strstrip(s);
        GwyGrainValue *gvalue;
        if (!(gvalue = gwy_inventory_get_item(inventory, s))) {
            g_warning("Invalid grain_filter range record: %s.", line);
            continue;
        }

        RangeRecord *rr = g_new(RangeRecord, 1);
        rr->lower = lower;
        rr->upper = upper;
        rr->quantity = gwy_resource_get_name(GWY_RESOURCE(gvalue));
        g_hash_table_insert(range_records, (gpointer)rr->quantity, rr);
    }
    g_free(buffer);

    return range_records;
}

static void
save_range(G_GNUC_UNUSED gpointer key, gpointer data, gpointer user_data)
{
    RangeRecord *rr = (RangeRecord*)data;
    FILE *fh = user_data;
    gchar lower[G_ASCII_DTOSTR_BUF_SIZE], upper[G_ASCII_DTOSTR_BUF_SIZE];

    g_ascii_dtostr(lower, G_ASCII_DTOSTR_BUF_SIZE, rr->lower);
    g_ascii_dtostr(upper, G_ASCII_DTOSTR_BUF_SIZE, rr->upper);
    gwy_fprintf(fh, "%s %s %s\n", lower, upper, rr->quantity);
}

static void
save_range_records(GHashTable *range_records)
{
    FILE *fh;
    if ((fh = gwy_module_data_fopen("grain_filter", "ranges", "w", NULL))) {
        g_hash_table_foreach(range_records, save_range, fh);
        fclose(fh);
    }
}

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