/*
 *  $Id: convolution_filter.c 30047 2026-06-11 08:30:45Z yeti-dn $
 *  Copyright (C) 2003-2025 David Necas (Yeti), Petr Klapetek.
 *  E-mail: 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 <stdlib.h>
#include <gtk/gtk.h>
#include <gwy.h>
#include "convolution-filter.h"
#include "../preview.h"
#include "libgwyapp/sanity.h"

#define RUN_MODES (GWY_RUN_IMMEDIATE | GWY_RUN_INTERACTIVE)

enum {
    PARAM_PRESET,

    PARAM_SIZE,
    PARAM_DIVISOR,
    PARAM_DIVISOR_AUTOMATIC,
    PARAM_HSYMMETRY,
    PARAM_VSYMMETRY,

    WIDGET_COEFFS,
};

enum {
    COLUMN_NAME,
    COLUMN_SIZE,
    COLUMN_HSYM,
    COLUMN_VSYM,
    NCOLUMNS
};

typedef struct {
    GwyField *field;
    GwyField *result;
    GwyParams *params;
    GwyConvolutionFilter *filter;
} ModuleArgs;

typedef struct {
    ModuleArgs *args;
    GtkWidget *dialog;
    GwyParamTable *table;
    GtkWidget *filter_page;
    GtkWidget *view;
    GtkWidget *matrix;
    guint matrix_size;
    GtkWidget **coeff;
    GtkWidget *treeview;
    GtkTreeViewColumn *name_column;
    GtkTreeSelection *selection;
    GwyInventoryStore *presets;
    GtkWidget *delete_preset;
    gulong selected_id;
    gboolean updating_filter;
} ModuleGUI;

static gboolean         module_register          (void);
static void             module_main              (GwyFile *data,
                                                  GwyRunModeFlags mode);
static GwyDialogOutcome run_gui                  (ModuleArgs *args,
                                                  GwyFile *data,
                                                  gint id);
static GtkWidget*       create_filter_tab        (ModuleGUI *gui);
static GtkWidget*       create_preset_tab        (ModuleGUI *gui);
static GtkWidget*       create_matrix            (gpointer user_data);
static void             select_filter            (ModuleGUI *gui,
                                                  const gchar *name,
                                                  GtkTreeIter *iter);
static void             param_changed            (ModuleGUI *gui,
                                                  gint id);
static void             render_filter            (GtkTreeViewColumn *column,
                                                  GtkCellRenderer *renderer,
                                                  GtkTreeModel *model,
                                                  GtkTreeIter *iter,
                                                  gpointer data);
static void             switch_filter            (ModuleGUI *gui);
static void             filter_delete            (ModuleGUI *gui);
static void             filter_duplicate         (ModuleGUI *gui);
static void             filter_new               (ModuleGUI *gui);
static void             filter_copy              (ModuleGUI *gui,
                                                  const gchar *name,
                                                  const gchar *newname);
static void             filter_name_edited       (ModuleGUI *gui,
                                                  const gchar *strpath,
                                                  const gchar *text);
static void             preview                  (gpointer user_data);
static void             execute                  (ModuleArgs *args);
static void             update_current_coeff     (ModuleGUI *gui);
static void             update_divisor           (ModuleGUI *gui);
static void             resize_matrix            (ModuleGUI *gui);
static void             update_matrix_sensitivity(ModuleGUI *gui);
static void             update_matrix            (ModuleGUI *gui);
static void             coeff_changed            (GtkEntry *entry,
                                                  ModuleGUI *gui);
static void             apply_symmetry           (ModuleGUI *gui);
static void             set_one_value            (ModuleGUI *gui,
                                                  guint j,
                                                  guint i,
                                                  gdouble val);
static void             set_value_with_symmetry  (ModuleGUI *gui,
                                                  guint j,
                                                  guint i,
                                                  gdouble val);

static const GwyEnum symmetries[] = {
    { gwy_NC("symmetry", "None"), CONVOLUTION_FILTER_SYMMETRY_NONE, },
    { gwy_NC("symmetry", "Even"), CONVOLUTION_FILTER_SYMMETRY_EVEN, },
    { gwy_NC("symmetry", "Odd"),  CONVOLUTION_FILTER_SYMMETRY_ODD,  },
};

static GwyModuleInfo module_info = {
    GWY_MODULE_ABI_VERSION,
    &module_register,
    N_("Generic convolution filter with a user-defined matrix."),
    "Yeti <yeti@gwyddion.net>",
    "2.0",
    "David Nečas (Yeti) & Petr Klapetek",
    "2006",
};

static G_DEFINE_QUARK(column-id, column_id)
static G_DEFINE_QUARK(position, position)

GWY_MODULE_QUERY2(module_info, convolution_filter)

static gboolean
module_register(void)
{
    GwyResourceClass *klass = g_type_class_ref(GWY_TYPE_CONVOLUTION_FILTER);
    gwy_convolution_filter_class_setup_presets();
    gwy_resource_class_load(klass);

    gwy_image_func_register("convolution_filter",
                            module_main,
                            N_("/_Integral Transforms/Con_volution Filter..."),
                            GWY_ICON_CONVOLUTION,
                            RUN_MODES,
                            GWY_MENU_FLAG_IMAGE,
                            N_("General convolution filter"));

    return TRUE;
}

static GwyParamDef*
define_module_params(void)
{
    static GwyEnum sizes[CONVOLUTION_NSIZES];
    static GwyParamDef *paramdef = NULL;

    if (paramdef)
        return paramdef;

    for (guint i = 0; i < CONVOLUTION_NSIZES; i++) {
        sizes[i].value = CONVOLUTION_MIN_SIZE + 2*i;
        sizes[i].name = g_strdup_printf("%u × %u", sizes[i].value, sizes[i].value);
    }

    paramdef = gwy_param_def_new();
    gwy_param_def_set_function_name(paramdef, gwy_image_func_current());
    gwy_param_def_add_resource(paramdef, PARAM_PRESET, "preset", NULL,
                               gwy_resource_class_get_inventory(g_type_class_peek(GWY_TYPE_CONVOLUTION_FILTER)),
                               GWY_CONVOLUTION_FILTER_DEFAULT);
    /* Filter parameters (GUI-only). */
    gwy_param_def_add_gwyenum(paramdef, PARAM_SIZE, NULL, _("_Size"),
                              sizes, CONVOLUTION_NSIZES, convolutionfilterdata_default.size);
    gwy_param_def_add_double(paramdef, PARAM_DIVISOR, NULL, _("_Divisor"), -G_MAXDOUBLE, G_MAXDOUBLE, 1.0);
    gwy_param_def_add_boolean(paramdef, PARAM_DIVISOR_AUTOMATIC, NULL, _("_Automatic divisor"), TRUE);
    gwy_param_def_add_gwyenum(paramdef, PARAM_HSYMMETRY, NULL, _("_Horizontal symmetry"),
                              symmetries, G_N_ELEMENTS(symmetries), CONVOLUTION_FILTER_SYMMETRY_EVEN);
    gwy_param_def_add_gwyenum(paramdef, PARAM_VSYMMETRY, NULL, _("_Vertical symmetry"),
                              symmetries, G_N_ELEMENTS(symmetries), CONVOLUTION_FILTER_SYMMETRY_EVEN);

    return paramdef;
}

static void
module_main(GwyFile *data, GwyRunModeFlags mode)
{
    ModuleArgs args;
    GQuark dquark;
    gint id;

    g_return_if_fail(mode & RUN_MODES);

    gwy_clear1(args);
    gwy_data_browser_get_current(GWY_APP_FIELD_KEY, &dquark,
                                 GWY_APP_FIELD, &args.field,
                                 GWY_APP_FIELD_ID, &id,
                                 0);
    g_return_if_fail(args.field && dquark);
    args.result = gwy_field_copy(args.field);

    GwyResourceClass *rklass = g_type_class_peek(GWY_TYPE_CONVOLUTION_FILTER);
    gwy_resource_class_mkdir(rklass);

    args.params = gwy_params_new_from_settings(define_module_params());
    args.filter = GWY_CONVOLUTION_FILTER(gwy_params_get_resource(args.params, PARAM_PRESET));

    GwyDialogOutcome outcome = GWY_DIALOG_PROCEED;
    if (mode == GWY_RUN_INTERACTIVE) {
        outcome = run_gui(&args, data, id);
        gwy_params_save_to_settings(args.params);
        if (gwy_resource_is_modified(GWY_RESOURCE(args.filter)))
            gwy_resource_save(GWY_RESOURCE(args.filter), NULL);
        if (outcome == GWY_DIALOG_CANCEL)
            goto end;
    }
    if (outcome != GWY_DIALOG_HAVE_RESULT)
        execute(&args);

    gwy_app_undo_qcheckpointv(GWY_DICT(data), 1, &dquark);
    gwy_file_set_image(data, id, args.result);
    gwy_log_add(data, GWY_FILE_IMAGE, id, id);

end:
    g_object_unref(args.result);
    g_object_unref(args.params);
}

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

    gwy_clear1(gui);
    gui.args = args;
    gui.presets = gwy_inventory_store_new(gwy_convolution_filters());
    gui.coeff = g_new0(GtkWidget*, CONVOLUTION_MAX_SIZE*CONVOLUTION_MAX_SIZE);

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

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

    GtkWidget *notebook = gtk_notebook_new();
    gtk_box_pack_start(GTK_BOX(hbox), notebook, TRUE, TRUE, 4);

    gui.filter_page = create_filter_tab(&gui);
    gtk_notebook_append_page(GTK_NOTEBOOK(notebook), gui.filter_page, gtk_label_new(_("Filter")));
    gtk_notebook_append_page(GTK_NOTEBOOK(notebook), create_preset_tab(&gui), gtk_label_new(_("Presets")));

    gwy_dialog_set_preview_func(dialog, GWY_PREVIEW_UPON_REQUEST, preview, &gui, NULL);
    g_signal_connect_swapped(gui.table, "param-changed", G_CALLBACK(param_changed), &gui);

    /* Ensure switch_filter() is called – exactly once – to load the filter data. */
    g_signal_handler_block(gui.selection, gui.selected_id);
    select_filter(&gui, NULL, NULL);
    g_signal_handler_unblock(gui.selection, gui.selected_id);
    switch_filter(&gui);

    GwyDialogOutcome outcome = gwy_dialog_run(dialog);

    g_object_unref(gui.presets);
    g_free(gui.coeff);

    return outcome;
}

static GtkWidget*
create_filter_tab(ModuleGUI *gui)
{
    GwyParamTable *table = gui->table = gwy_param_table_new(gui->args->params);

    gwy_param_table_append_radio_row(table, PARAM_SIZE);
    gwy_param_table_append_header(table, -1, _("Coefficient Matrix"));
    gwy_param_table_append_foreign(table, WIDGET_COEFFS, create_matrix, gui, NULL);
    gwy_param_table_append_checkbox(table, PARAM_DIVISOR_AUTOMATIC);
    gwy_param_table_append_entry(table, PARAM_DIVISOR);
    gwy_param_table_append_separator(table);
    gwy_param_table_append_radio_row(table, PARAM_HSYMMETRY);
    gwy_param_table_append_radio_row(table, PARAM_VSYMMETRY);

    return gwy_param_table_widget(table);
}

static GtkWidget*
create_matrix(gpointer user_data)
{
    ModuleGUI *gui = (ModuleGUI*)user_data;
    return gui->matrix = gtk_grid_new();
}

static void
render_filter(GtkTreeViewColumn *column,
              GtkCellRenderer *renderer,
              GtkTreeModel *model,
              GtkTreeIter *iter,
              G_GNUC_UNUSED gpointer data)
{
    guint id = GPOINTER_TO_UINT(g_object_get_qdata(G_OBJECT(column), column_id_quark()));
    GwyConvolutionFilter *filter;

    gtk_tree_model_get(model, iter, 0, &filter, -1);
    if (id == COLUMN_NAME)
        g_object_set(renderer, "editable", gwy_resource_is_modifiable(GWY_RESOURCE(filter)), NULL);
    else if (id == COLUMN_SIZE) {
        gchar *s = g_strdup_printf("%u", filter->data.size);
        g_object_set(renderer, "text", s, NULL);
        g_free(s);
    }
    else if (id == COLUMN_HSYM || id == COLUMN_VSYM) {
        ConvolutionFilterSymmetryType sym = (id == COLUMN_VSYM ? filter->vsym : filter->hsym);
        const gchar *s = gwy_enum_to_string(sym, symmetries, G_N_ELEMENTS(symmetries));
        g_object_set(renderer, "text", gwy_C(s), NULL);
    }
    else {
        g_assert_not_reached();
    }
}

static GtkWidget*
create_preset_tab(ModuleGUI *gui)
{
    static const struct {
        const gchar *icon_name;
        const gchar *tooltip;
        GCallback callback;
    }
    toolbar_buttons[] = {
        { GWY_ICON_GTK_NEW,    N_("Create a new item"),       G_CALLBACK(filter_new),       },
        { GWY_ICON_GTK_COPY,   N_("Replicate selected item"), G_CALLBACK(filter_duplicate), },
        { GWY_ICON_GTK_DELETE, N_("Delete selected item"),    G_CALLBACK(filter_delete),    },
    };
    static const gchar *column_headers[NCOLUMNS] = { N_("Name"), N_("Size"), N_("HSym"), N_("VSym") };

    GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);

    GtkWidget *treeview = gtk_tree_view_new_with_model(GTK_TREE_MODEL(gui->presets));
    gui->treeview = treeview;
    gtk_box_pack_start(GTK_BOX(vbox), treeview, TRUE, TRUE, 0);

    /* Name */
    for (guint i = 0; i < NCOLUMNS; i++) {
        GtkCellRenderer *renderer = gtk_cell_renderer_text_new();
        GtkTreeViewColumn *column = gtk_tree_view_column_new_with_attributes(_(column_headers[i]), renderer, NULL);
        g_object_set_qdata(G_OBJECT(column), column_id_quark(), GUINT_TO_POINTER(i));

        if (i == COLUMN_NAME) {
            guint iattr = gwy_inventory_store_get_column_by_name(gui->presets, "name");
            gtk_tree_view_column_add_attribute(column, renderer, "text", iattr);
            gtk_tree_view_column_set_expand(column, TRUE);
            g_object_set(renderer, "editable-set", TRUE, NULL);
            g_signal_connect_swapped(renderer, "edited", G_CALLBACK(filter_name_edited), gui);
            gui->name_column = column;
        }
        gtk_tree_view_column_set_cell_data_func(column, renderer, render_filter, NULL, NULL);
        gtk_tree_view_append_column(GTK_TREE_VIEW(treeview), column);
    }

    /* Selection */
    gui->selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(treeview));
    gtk_tree_selection_set_mode(gui->selection, GTK_SELECTION_BROWSE);
    gui->selected_id = g_signal_connect_swapped(gui->selection, "changed", G_CALLBACK(switch_filter), gui);

    /* Controls */
    GtkWidget *hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
    gtk_box_set_homogeneous(GTK_BOX(hbox), TRUE);
    gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 0);
    for (guint i = 0; i < G_N_ELEMENTS(toolbar_buttons); i++) {
        GtkWidget *image = gtk_image_new_from_icon_name(toolbar_buttons[i].icon_name, GTK_ICON_SIZE_LARGE_TOOLBAR);
        GtkWidget *button = gtk_button_new();
        gtk_container_add(GTK_CONTAINER(button), image);
        gtk_box_pack_start(GTK_BOX(hbox), button, TRUE, TRUE, 0);
        gtk_widget_set_tooltip_text(button, toolbar_buttons[i].tooltip);
        g_signal_connect_swapped(button, "clicked", toolbar_buttons[i].callback, gui);

        if (toolbar_buttons[i].callback == G_CALLBACK(filter_delete))
            gui->delete_preset = button;
    }

    return vbox;
}

static void
switch_filter(ModuleGUI *gui)
{
    ModuleArgs *args = gui->args;

    /* Can this hapen in a transitional state? */
    GtkTreeIter iter;
    GtkTreeModel *model;
    if (!gtk_tree_selection_get_selected(gui->selection, &model, &iter))
        return;

    GwyResource *resource = GWY_RESOURCE(args->filter);
    if (gwy_resource_is_modified(resource))
        gwy_resource_save(resource, NULL);

    GwyConvolutionFilter *filter;
    gtk_tree_model_get(model, &iter, 0, &filter, -1);
    args->filter = filter;
    gwy_params_set_resource(args->params, PARAM_PRESET, gwy_resource_get_name(GWY_RESOURCE(filter)));

    gui->updating_filter = TRUE;
    gwy_param_table_set_enum(gui->table, PARAM_SIZE, filter->data.size);
    gwy_param_table_set_enum(gui->table, PARAM_HSYMMETRY, filter->hsym);
    gwy_param_table_set_enum(gui->table, PARAM_VSYMMETRY, filter->vsym);
    gwy_param_table_set_double(gui->table, PARAM_DIVISOR, filter->data.divisor);
    gui->updating_filter = FALSE;
    gwy_param_table_set_boolean(gui->table, PARAM_DIVISOR_AUTOMATIC, filter->data.auto_divisor);

    resize_matrix(gui);
    update_matrix(gui);
    update_matrix_sensitivity(gui);

    gboolean sensitive = gwy_resource_is_modifiable(GWY_RESOURCE(filter));
    gtk_widget_set_sensitive(gui->filter_page, sensitive);
    gtk_widget_set_sensitive(gui->delete_preset, sensitive);
}

static void
filter_delete(ModuleGUI *gui)
{
    gwy_resource_delete(GWY_RESOURCE(gui->args->filter));
}

static void
filter_new(ModuleGUI *gui)
{
    filter_copy(gui, GWY_CONVOLUTION_FILTER_DEFAULT, _("Untitled"));
}

static void
filter_duplicate(ModuleGUI *gui)
{
    const gchar *name = gwy_resource_get_name(GWY_RESOURCE(gui->args->filter));
    filter_copy(gui, name, NULL);
}

static void
filter_copy(ModuleGUI *gui, const gchar *name, const gchar *newname)
{
    gwy_debug("<%s> -> <%s>", name, newname);
    GwyResource *resource = gwy_inventory_new_item(gwy_convolution_filters(), name, newname);
    if (gwy_resource_is_modified(resource))
        gwy_resource_save(resource, NULL);

    /* Start editing. */
    gtk_widget_grab_focus(gui->treeview);
    GtkTreeIter iter;
    select_filter(gui, gwy_resource_get_name(resource), &iter);
    GtkTreePath *path = gtk_tree_model_get_path(GTK_TREE_MODEL(gui->presets), &iter);
    gtk_tree_view_set_cursor(GTK_TREE_VIEW(gui->treeview), path, gui->name_column, TRUE);
}

static void
filter_name_edited(ModuleGUI *gui, const gchar *strpath, const gchar *text)
{
    GwyInventory *presets = gwy_convolution_filters();
    if (gwy_inventory_get_item(presets, text))
        return;

    GtkTreeModel *model = GTK_TREE_MODEL(gui->presets);
    GtkTreePath *path = gtk_tree_path_new_from_string(strpath);
    GtkTreeIter iter;
    gtk_tree_model_get_iter(model, &iter, path);
    gtk_tree_path_free(path);
    GwyResource *resource;
    gtk_tree_model_get(model, &iter, 0, &resource, -1);

    if (gwy_resource_is_modified(resource))
        gwy_resource_save(resource, NULL);

    if (gwy_resource_rename(resource, text))
        select_filter(gui, text, NULL);
}

static void
select_filter(ModuleGUI *gui, const gchar *name, GtkTreeIter *piter)
{
    if (!name)
        name = gwy_resource_get_name(GWY_RESOURCE(gui->args->filter));

    GtkTreeIter iter;
    gwy_inventory_store_get_iter(gui->presets, name, &iter);
    gtk_tree_selection_select_iter(gui->selection, &iter);
    if (piter)
        *piter = iter;
}

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

    if (gui->updating_filter)
        return;

    GwyConvolutionFilter *filter = args->filter;
    if (id < 0 || id == PARAM_SIZE) {
        guint newsize = gwy_params_get_enum(params, PARAM_SIZE);
        gwy_convolution_filter_data_resize(&filter->data, newsize);
        resize_matrix(gui);
        update_matrix(gui);
        apply_symmetry(gui);
        update_matrix_sensitivity(gui);
    }
    if (id < 0 || id == PARAM_HSYMMETRY || id == PARAM_VSYMMETRY) {
        filter->hsym = gwy_params_get_enum(params, PARAM_HSYMMETRY);
        filter->vsym = gwy_params_get_enum(params, PARAM_VSYMMETRY);
        apply_symmetry(gui);
        update_matrix_sensitivity(gui);
    }
    if (id < 0 || id == PARAM_DIVISOR)
        filter->data.divisor = gwy_params_get_double(params, PARAM_DIVISOR);
    if (id < 0 || id == PARAM_DIVISOR_AUTOMATIC) {
        gboolean autodiv = gwy_params_get_boolean(params, PARAM_DIVISOR_AUTOMATIC);
        filter->data.auto_divisor = autodiv;
        gwy_param_table_set_sensitive(gui->table, PARAM_DIVISOR, !autodiv);
        if (autodiv) {
            gwy_convolution_filter_data_autodiv(&filter->data);
            update_divisor(gui);
        }
    }

    /* An everything-change means loading a new filter. The loading takes care of updating all the controls. Do not
     * attempt to save a constant resource here. */
    if (id >= 0)
        gwy_resource_data_changed(GWY_RESOURCE(filter));

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

static void
preview(gpointer user_data)
{
    ModuleGUI *gui = (ModuleGUI*)user_data;
    update_current_coeff(gui);
    execute(gui->args);
    gwy_field_data_changed(gui->args->result);
    gwy_dialog_have_result(GWY_DIALOG(gui->dialog));
}

static void
execute(ModuleArgs *args)
{
    GwyConvolutionFilterData *pdata = &args->filter->data;
    GwyField *kernel = gwy_field_new(pdata->size, pdata->size, 1.0, 1.0, FALSE);
    gwy_assign(gwy_field_get_data(kernel), pdata->matrix, pdata->size*pdata->size);
    if (pdata->divisor != 0.0)
        gwy_field_multiply(kernel, 1.0/pdata->divisor);
    gwy_field_copy_data(args->field, args->result);
    gwy_field_convolve(args->result, kernel);
    g_object_unref(kernel);
}

static void
update_current_coeff(ModuleGUI *gui)
{
    GtkWidget *entry = gtk_window_get_focus(GTK_WINDOW(gui->dialog));
    if (entry && GTK_IS_ENTRY(entry) && gtk_widget_get_parent(entry) == gui->matrix)
        coeff_changed(GTK_ENTRY(entry), gui);
}

static void
update_divisor(ModuleGUI *gui)
{
    gwy_param_table_set_double(gui->table, PARAM_DIVISOR, gui->args->filter->data.divisor);
}

static void
resize_matrix(ModuleGUI *gui)
{
    GwyConvolutionFilter *filter = gui->args->filter;
    guint oldsize = gui->matrix_size;
    guint newsize = filter->data.size;
    GtkWidget **coeff = gui->coeff;

    if (newsize < oldsize) {
        for (guint i = 0; i < oldsize; i++) {
            for (guint j = 0; j < oldsize; j++) {
                if (i >= newsize || j >= newsize) {
                    guint k = i*CONVOLUTION_MAX_SIZE + j;
                    gtk_widget_destroy(coeff[k]);
                    coeff[k] = NULL;
                }
            }
        }
    }
    else if (newsize > oldsize) {
        for (guint i = 0; i < newsize; i++) {
            for (guint j = 0; j < newsize; j++) {
                guint k = i*CONVOLUTION_MAX_SIZE + j;
                if (coeff[k])
                    continue;

                GtkWidget *entry = gtk_entry_new();
                g_object_set_qdata(G_OBJECT(entry), position_quark(), GUINT_TO_POINTER(k));
                gtk_entry_set_width_chars(GTK_ENTRY(entry), 5);
                gtk_widget_set_hexpand(entry, TRUE);
                gwy_widget_set_activate_on_unfocus(entry, TRUE);
                gtk_grid_attach(GTK_GRID(gui->matrix), entry, j, i, 1, 1);
                coeff[k] = entry;
                g_signal_connect(entry, "activate", G_CALLBACK(coeff_changed), gui);
            }
        }
        gtk_widget_show_all(gui->matrix);
    }
    gui->matrix_size = newsize;
}

static void
update_matrix_sensitivity(ModuleGUI *gui)
{
    ModuleArgs *args = gui->args;
    GwyResource *resource = GWY_RESOURCE(args->filter);
    gboolean is_const = !gwy_resource_is_modifiable(resource);
    guint size = args->filter->data.size;
    GtkWidget **coeff = gui->coeff;

    gboolean vsens = (args->filter->vsym != CONVOLUTION_FILTER_SYMMETRY_ODD);
    gboolean hsens = (args->filter->hsym != CONVOLUTION_FILTER_SYMMETRY_ODD);
    for (guint i = 0; i < size; i++) {
        for (guint j = 0; j < size; j++) {
            guint k = i*CONVOLUTION_MAX_SIZE + j;
            if (is_const)
                gtk_widget_set_sensitive(coeff[k], FALSE);
            else
                gtk_widget_set_sensitive(coeff[k], (j != size/2 || hsens) && (i != size/2 || vsens));
        }
    }
}

static void
coeff_changed(GtkEntry *entry, ModuleGUI *gui)
{
    if (gui->updating_filter)
        return;

    GwyConvolutionFilter *filter = gui->args->filter;
    gdouble *matrix = filter->data.matrix;
    guint size = filter->data.size;

    guint k = GPOINTER_TO_UINT(g_object_get_qdata(G_OBJECT(entry), position_quark()));
    guint i = k/CONVOLUTION_MAX_SIZE, j = k % CONVOLUTION_MAX_SIZE;
    gdouble val = g_strtod(gtk_entry_get_text(entry), NULL);
    if (val == matrix[i*size + j])
        return;

    gui->updating_filter = TRUE;
    set_value_with_symmetry(gui, j, i, val);
    gui->updating_filter = FALSE;
    gwy_dialog_invalidate(GWY_DIALOG(gui->dialog));
    gwy_resource_data_changed(GWY_RESOURCE(filter));

    if (filter->data.auto_divisor) {
        gwy_convolution_filter_data_autodiv(&filter->data);
        update_divisor(gui);
    }
}

static void
apply_symmetry(ModuleGUI *gui)
{
    GwyConvolutionFilter *filter = gui->args->filter;
    ConvolutionFilterSymmetryType vsym = filter->vsym;
    ConvolutionFilterSymmetryType hsym = filter->hsym;
    gdouble *matrix = filter->data.matrix;
    guint size = filter->data.size;

    gui->updating_filter = TRUE;
    if (hsym) {
        if (vsym) {
            for (guint i = 0; i <= size/2; i++) {
                for (guint j = 0; j <= size/2; j++) {
                    gdouble val = matrix[i*size + j];
                    if (hsym == CONVOLUTION_FILTER_SYMMETRY_ODD && j == size/2)
                        val = 0.0;
                    if (vsym == CONVOLUTION_FILTER_SYMMETRY_ODD && i == size/2)
                        val = 0.0;
                    set_value_with_symmetry(gui, j, i, val);
                }
            }
        }
        else {
            for (guint i = 0; i < size; i++) {
                for (guint j = 0; j <= size/2; j++) {
                    gdouble val = matrix[i*size + j];
                    if (hsym == CONVOLUTION_FILTER_SYMMETRY_ODD && j == size/2)
                        val = 0.0;
                    set_value_with_symmetry(gui, j, i, val);
                }
            }
        }
    }
    else {
        if (vsym) {
            for (guint i = 0; i <= size/2; i++) {
                for (guint j = 0; j < size; j++) {
                    gdouble val = matrix[i*size + j];
                    if (vsym == CONVOLUTION_FILTER_SYMMETRY_ODD && i == size/2)
                        val = 0.0;
                    set_value_with_symmetry(gui, j, i, val);
                }
            }
        }
        else {
            /* Do nothing */
        }
    }
    gui->updating_filter = FALSE;
}

static void
update_matrix(ModuleGUI *gui)
{
    GwyConvolutionFilter *filter = gui->args->filter;
    gdouble *matrix = filter->data.matrix;
    guint size = filter->data.size;

    gui->updating_filter = TRUE;
    for (guint i = 0; i < size; i++) {
        for (guint j = 0; j < size; j++)
            set_one_value(gui, j, i, matrix[size*i + j]);
    }
    gui->updating_filter = FALSE;
}

static void
set_one_value(ModuleGUI *gui,
              guint j, guint i,
              gdouble val)
{
    GwyConvolutionFilter *filter = gui->args->filter;
    gdouble *matrix = filter->data.matrix;
    guint size = filter->data.size;

    g_return_if_fail(i < size);
    g_return_if_fail(j < size);

    /* Fix `negative zeroes' */
    if (val == -val)
        val = 0.0;

    matrix[i*size + j] = val;
    gchar *s = g_strdup_printf("%.8g", val);
    gtk_entry_set_text(GTK_ENTRY(gui->coeff[i*CONVOLUTION_MAX_SIZE + j]), s);
    g_free(s);
}

static void
set_value_with_symmetry(ModuleGUI *gui,
                        guint j, guint i,
                        gdouble val)
{
    GwyConvolutionFilter *filter = gui->args->filter;
    ConvolutionFilterSymmetryType vsym = filter->vsym;
    ConvolutionFilterSymmetryType hsym = filter->hsym;
    guint size = filter->data.size;
    guint iprime = size-1 - i, jprime = size-1 - j;

    set_one_value(gui, j, i, val);
    if (hsym == CONVOLUTION_FILTER_SYMMETRY_EVEN) {
        set_one_value(gui, jprime, i, val);
        if (vsym == CONVOLUTION_FILTER_SYMMETRY_EVEN) {
            set_one_value(gui, j, iprime, val);
            set_one_value(gui, jprime, iprime, val);
        }
        else if (vsym == CONVOLUTION_FILTER_SYMMETRY_ODD) {
            set_one_value(gui, j, iprime, -val);
            set_one_value(gui, jprime, iprime, -val);
        }
    }
    else if (hsym == CONVOLUTION_FILTER_SYMMETRY_ODD) {
        set_one_value(gui, jprime, i, -val);
        if (vsym == CONVOLUTION_FILTER_SYMMETRY_EVEN) {
            set_one_value(gui, j, iprime, val);
            set_one_value(gui, jprime, iprime, -val);
        }
        else if (vsym == CONVOLUTION_FILTER_SYMMETRY_ODD) {
            set_one_value(gui, j, iprime, -val);
            set_one_value(gui, jprime, iprime, val);
        }
    }
    else {
        if (vsym == CONVOLUTION_FILTER_SYMMETRY_EVEN)
            set_one_value(gui, j, iprime, val);
        else if (vsym == CONVOLUTION_FILTER_SYMMETRY_ODD)
            set_one_value(gui, j, iprime, -val);
    }
}

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