/*
 *  $Id: crop.c 30036 2026-06-10 14:57:46Z yeti-dn $
 *  Copyright (C) 2003-2026 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 <gtk/gtk.h>
#include <gwy.h>

enum {
    PARAM_KEEP_OFFSETS,
    PARAM_ALL,
    PARAM_NEW_CHANNEL,
    PARAM_HOLD_SELECTION,
};

#define GWY_TYPE_TOOL_CROP            (gwy_tool_crop_get_type())
#define GWY_TOOL_CROP(obj)            (G_TYPE_CHECK_INSTANCE_CAST((obj), GWY_TYPE_TOOL_CROP, GwyToolCrop))
#define GWY_IS_TOOL_CROP(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), GWY_TYPE_TOOL_CROP))
#define GWY_TOOL_CROP_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS((obj), GWY_TYPE_TOOL_CROP, GwyToolCropClass))

typedef struct _GwyToolCrop      GwyToolCrop;
typedef struct _GwyToolCropClass GwyToolCropClass;

struct _GwyToolCrop {
    GwyPlainTool parent_instance;

    GwyParams *params;

    GwyRectSelectionLabels *rlabels;
    GwyParamTable *table;
    gdouble rsel[4];
    gint isel[4];
};

struct _GwyToolCropClass {
    GwyPlainToolClass parent_class;
};

static gboolean     module_register                (void);
static GwyParamDef* define_module_params           (void);
static GType        gwy_tool_crop_get_type         (void)                      G_GNUC_CONST;
static void         gwy_tool_crop_finalize         (GObject *object);
static void         gwy_tool_crop_init_dialog      (GwyToolCrop *tool);
static void         gwy_tool_crop_data_switched    (GwyTool *gwytool,
                                                    GwyDataView *data_view);
static void         gwy_tool_crop_data_changed     (GwyPlainTool *plain_tool);
static void         gwy_tool_crop_response         (GwyTool *tool,
                                                    gint response_id);
static void         gwy_tool_crop_selection_changed(GwyPlainTool *plain_tool,
                                                    gint hint);
static void         gwy_tool_crop_apply            (GwyToolCrop *tool);
static void         param_changed                  (GwyToolCrop *tool,
                                                    gint id);
static void         update_sensitivity             (GwyToolCrop *tool);
static void         update_selected_rectangle      (GwyToolCrop *tool);

static GwyModuleInfo module_info = {
    GWY_MODULE_ABI_VERSION,
    &module_register,
    N_("Crop tool, crops data to smaller size."),
    "Yeti <yeti@gwyddion.net>",
    "3.3",
    "David Nečas (Yeti) & Petr Klapetek",
    "2003",
};

GWY_MODULE_QUERY2(module_info, crop)

G_DEFINE_TYPE(GwyToolCrop, gwy_tool_crop, GWY_TYPE_PLAIN_TOOL)

static gboolean
module_register(void)
{
    gwy_tool_func_register(GWY_TYPE_TOOL_CROP);

    return TRUE;
}

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

    if (paramdef)
        return paramdef;

    paramdef = gwy_param_def_new();
    gwy_param_def_set_function_name(paramdef, "crop");
    gwy_param_def_add_boolean(paramdef, PARAM_KEEP_OFFSETS, "keep_offsets", _("Keep lateral offsets"), FALSE);
    gwy_param_def_add_boolean(paramdef, PARAM_ALL, "all", _("Crop all compatible images"), FALSE);
    gwy_param_def_add_boolean(paramdef, PARAM_NEW_CHANNEL, "new_channel", _("Create new image"), TRUE);
    gwy_param_def_add_hold_selection(paramdef, PARAM_HOLD_SELECTION, "hold_selection", NULL);

    return paramdef;
}

static void
gwy_tool_crop_class_init(GwyToolCropClass *klass)
{
    GwyPlainToolClass *ptool_class = GWY_PLAIN_TOOL_CLASS(klass);
    GwyToolClass *tool_class = GWY_TOOL_CLASS(klass);
    GObjectClass *gobject_class = G_OBJECT_CLASS(klass);

    gobject_class->finalize = gwy_tool_crop_finalize;

    tool_class->icon_name = GWY_ICON_CROP;
    tool_class->title = _("Crop");
    tool_class->tooltip = _("Crop data");
    tool_class->prefix = "/module/crop";
    tool_class->data_switched = gwy_tool_crop_data_switched;
    tool_class->response = gwy_tool_crop_response;

    ptool_class->data_changed = gwy_tool_crop_data_changed;
    ptool_class->selection_changed = gwy_tool_crop_selection_changed;
}

static void
gwy_tool_crop_finalize(GObject *object)
{
    GwyToolCrop *tool = GWY_TOOL_CROP(object);

    gwy_params_save_to_settings(tool->params);
    g_clear_object(&tool->params);

    G_OBJECT_CLASS(gwy_tool_crop_parent_class)->finalize(object);
}

static void
gwy_tool_crop_init(GwyToolCrop *tool)
{
    GwyPlainTool *plain_tool = GWY_PLAIN_TOOL(tool);

    tool->params = gwy_params_new_from_settings(define_module_params());

    plain_tool->lazy_updates = TRUE;
    gwy_plain_tool_connect_selection(plain_tool, GWY_TYPE_LAYER_RECTANGLE, "rectangle");
    gwy_plain_tool_enable_selection_holding(plain_tool);

    gwy_tool_crop_init_dialog(tool);
}

static void
gwy_tool_crop_rect_updated(GwyToolCrop *tool)
{
    GwyPlainTool *plain_tool = GWY_PLAIN_TOOL(tool);

    gwy_rect_selection_labels_select(tool->rlabels, plain_tool->selection, plain_tool->field);
}

static void
gwy_tool_crop_init_dialog(GwyToolCrop *tool)
{
    GtkDialog *dialog = GTK_DIALOG(GWY_TOOL(tool)->dialog);
    GwyPlainTool *plain_tool = GWY_PLAIN_TOOL(tool);
    GwyParamTable *table;

    tool->rlabels = gwy_rect_selection_labels_new(TRUE, G_CALLBACK(gwy_tool_crop_rect_updated), tool);
    gtk_box_pack_start(GTK_BOX(gtk_dialog_get_content_area(dialog)),
                       gwy_rect_selection_labels_get_table(tool->rlabels), FALSE, FALSE, 0);

    table = tool->table = gwy_param_table_new(tool->params);
    gwy_param_table_append_checkbox(table, PARAM_KEEP_OFFSETS);
    gwy_param_table_append_checkbox(table, PARAM_ALL);
    gwy_param_table_append_checkbox(table, PARAM_NEW_CHANNEL);
    gwy_param_table_append_hold_selection(table, PARAM_HOLD_SELECTION);
    gwy_plain_tool_add_param_table(plain_tool, table);
    gtk_box_pack_start(GTK_BOX(gtk_dialog_get_content_area(dialog)), gwy_param_table_widget(table), FALSE, FALSE, 0);

    gwy_plain_tool_add_clear_button(plain_tool);
    gwy_tool_add_hide_button(GWY_TOOL(tool), FALSE);
    gwy_plain_tool_add_apply_button(plain_tool);
    gtk_dialog_set_response_sensitive(dialog, GTK_RESPONSE_APPLY, FALSE);
    gwy_help_add_to_tool_dialog(dialog, GWY_TOOL(tool), GWY_HELP_NO_BUTTON);

    update_sensitivity(tool);

    g_signal_connect_swapped(tool->table, "param-changed", G_CALLBACK(param_changed), tool);

    gtk_widget_show_all(gtk_dialog_get_content_area(dialog));
}

static void
gwy_tool_crop_data_switched(GwyTool *gwytool,
                            GwyDataView *data_view)
{
    GwyPlainTool *plain_tool = GWY_PLAIN_TOOL(gwytool);
    GwyToolCrop *tool = GWY_TOOL_CROP(gwytool);
    gboolean ignore = (data_view == plain_tool->data_view);

    GWY_TOOL_CLASS(gwy_tool_crop_parent_class)->data_switched(gwytool, data_view);

    if (ignore || plain_tool->init_failed)
        return;

    if (data_view) {
        gwy_object_set_or_reset(plain_tool->layer, GWY_TYPE_LAYER_RECTANGLE,
                                "draw-as-crop", TRUE,
                                "editable", TRUE,
                                "focus", -1,
                                NULL);
        gwy_selection_set_max_objects(plain_tool->selection, 1);
        gwy_plain_tool_hold_selection(plain_tool, gwy_params_get_flags(tool->params, PARAM_HOLD_SELECTION));
    }
}

static void
gwy_tool_crop_data_changed(GwyPlainTool *plain_tool)
{
    update_selected_rectangle(GWY_TOOL_CROP(plain_tool));
}

static void
param_changed(GwyToolCrop *tool, gint id)
{
    if (id < 0 || id == PARAM_ALL)
        update_sensitivity(tool);
}

static void
update_sensitivity(GwyToolCrop *tool)
{
    gwy_param_table_set_sensitive(tool->table, PARAM_NEW_CHANNEL, !gwy_params_get_boolean(tool->params, PARAM_ALL));
}

static void
gwy_tool_crop_response(GwyTool *tool,
                       gint response_id)
{
    GWY_TOOL_CLASS(gwy_tool_crop_parent_class)->response(tool, response_id);

    if (response_id == GTK_RESPONSE_APPLY)
        gwy_tool_crop_apply(GWY_TOOL_CROP(tool));
}

static void
gwy_tool_crop_selection_changed(GwyPlainTool *plain_tool,
                                gint hint)
{
    g_return_if_fail(hint <= 0);
    update_selected_rectangle(GWY_TOOL_CROP(plain_tool));
}

static void
set_offsets(GwyField *field, const gdouble *sel, gdouble xoff, gdouble yoff, gboolean keep_off)
{
    if (keep_off) {
        gwy_field_set_xoffset(field, xoff + sel[0]);
        gwy_field_set_yoffset(field, yoff + sel[1]);
    }
}

static void
crop_into_new_channel(GwyToolCrop *tool)
{
    GwyPlainTool *plain_tool = GWY_PLAIN_TOOL(tool);
    GwyFile *file = GWY_FILE(plain_tool->container);
    gboolean keep_off = gwy_params_get_boolean(tool->params, PARAM_KEEP_OFFSETS);
    GwyField *field = plain_tool->field, *show = plain_tool->show_field, *newfield;
    GwyNield *mask = plain_tool->mask_field;
    gint id, oldid = plain_tool->id;
    gint col = tool->isel[0], row = tool->isel[1];
    gint width = tool->isel[2]+1 - tool->isel[0], height = tool->isel[3]+1 - tool->isel[1];
    gdouble xoff = gwy_field_get_xoffset(field), yoff = gwy_field_get_yoffset(field);
    gdouble rsel[4];

    gwy_assign(rsel, tool->rsel, 4);

    newfield = gwy_field_area_extract(field, col, row, width, height);
    set_offsets(newfield, tool->rsel, xoff, yoff, keep_off);
    id = gwy_file_add_image(file, newfield);
    gwy_file_set_visible(file, GWY_FILE_IMAGE, id, TRUE);
    g_object_unref(field);
    gwy_file_sync_items(file, GWY_FILE_IMAGE, oldid,
                        file, GWY_FILE_IMAGE, id,
                        GWY_FILE_ITEM_PALETTE | GWY_FILE_ITEM_COLOR_MAPPING
                        | GWY_FILE_ITEM_MASK_COLOR | GWY_FILE_ITEM_REAL_SQUARE, FALSE);
    gwy_file_set_title(file, GWY_FILE_IMAGE, id, _("Detail"), TRUE);
    gwy_log_add_full(file, GWY_FILE_IMAGE, oldid, id, "tool::GwyToolCrop", NULL);

    if (mask) {
        mask = gwy_nield_area_extract(mask, col, row, width, height);
        gwy_file_pass_image_mask(file, id, mask);
    }

    if (show) {
        newfield = gwy_field_area_extract(show, col, row, width, height);
        set_offsets(newfield, tool->rsel, xoff, yoff, keep_off);
        gwy_dict_pass_object(GWY_DICT(file), gwy_file_key_image_picture(id), show);
    }
}

static void
crop_in_place_maybe_all(GwyToolCrop *tool)
{
    GwyPlainTool *plain_tool = GWY_PLAIN_TOOL(tool);
    GwyFile *file = GWY_FILE(plain_tool->container);
    GwyField *field = plain_tool->field;
    gboolean keep_off = gwy_params_get_boolean(tool->params, PARAM_KEEP_OFFSETS);
    gboolean crop_all = gwy_params_get_boolean(tool->params, PARAM_ALL);
    gint *ids;
    gchar *qualname;
    gint col = tool->isel[0], row = tool->isel[1];
    gint width = tool->isel[2]+1 - tool->isel[0], height = tool->isel[3]+1 - tool->isel[1];
    gdouble rsel[4];

    gwy_assign(rsel, tool->rsel, 4);

    if (!crop_all) {
        ids = g_new(gint, 2);
        ids[0] = plain_tool->id;
        ids[1] = -1;
    }
    else
        ids = gwy_file_get_ids(file, GWY_FILE_IMAGE);

    GArray *undo_quarks = g_array_new(FALSE, FALSE, sizeof(GQuark));
    gint n = 0;
    for (gint i = 0; ids[i] > -1; i++) {
        GQuark quark = gwy_file_key_image(ids[i]);
        GwyField *otherfield = gwy_file_get_image(file, ids[i]);
        if (gwy_field_is_incompatible(field, otherfield, GWY_DATA_MISMATCH_LATERAL))
            continue;

        g_array_append_val(undo_quarks, quark);

        quark = gwy_file_key_image_mask(ids[i]);
        if (gwy_dict_contains(plain_tool->container, quark))
            g_array_append_val(undo_quarks, quark);

        quark = gwy_file_key_image_picture(ids[i]);
        if (gwy_dict_contains(plain_tool->container, quark))
            g_array_append_val(undo_quarks, quark);

        ids[n++] = ids[i];
    }
    ids[n] = -1;
    gwy_app_undo_qcheckpointv(plain_tool->container, undo_quarks->len, &g_array_index(undo_quarks, GQuark, 0));

    for (gint i = 0; i < undo_quarks->len; i++) {
        GObject *object = gwy_dict_get_object(plain_tool->container, g_array_index(undo_quarks, GQuark, i));
        if (GWY_IS_FIELD(object)) {
            field = GWY_FIELD(object);
            gdouble xoff = gwy_field_get_xoffset(field), yoff = gwy_field_get_yoffset(field);
            gwy_field_crop(field, col, row, width, height);
            set_offsets(field, tool->rsel, xoff, yoff, keep_off);
            gwy_field_data_changed(field);
        }
        else if (GWY_IS_NIELD(object)) {
            GwyNield *nield = GWY_NIELD(object);
            gwy_nield_crop(nield, col, row, width, height);
            gwy_nield_data_changed(nield);
        }
    }
    qualname = g_strconcat("tool::", G_OBJECT_TYPE_NAME(plain_tool), NULL);
    for (gint i = 0; ids[i] > -1; i++) {
        gwy_file_remove_selections(file, GWY_FILE_IMAGE, ids[i]);
        gwy_log_add_full(file, GWY_FILE_IMAGE, ids[i], ids[i], qualname, NULL);
    }
    g_free(qualname);
}

static void
gwy_tool_crop_apply(GwyToolCrop *tool)
{
    GwyPlainTool *plain_tool = GWY_PLAIN_TOOL(tool);

    g_return_if_fail(plain_tool->id >= 0 && plain_tool->field != NULL);

    if (!gwy_selection_get_data(plain_tool->selection, NULL)) {
        g_warning("Apply invoked when no selection is present");
        return;
    }

    gwy_params_save_to_settings(tool->params);   /* Ensure correct parameters in the log. */
    if (!gwy_params_get_boolean(tool->params, PARAM_ALL) && gwy_params_get_boolean(tool->params, PARAM_NEW_CHANNEL))
        crop_into_new_channel(tool);
    else
        crop_in_place_maybe_all(tool);
}

static void
update_selected_rectangle(GwyToolCrop *tool)
{
    GwyPlainTool *plain_tool = GWY_PLAIN_TOOL(tool);
    GwySelection *selection = plain_tool->selection;
    GwyField *field = plain_tool->field;
    gint n = selection ? gwy_selection_get_data(selection, NULL) : 0;
    gint xres, yres;

    gtk_dialog_set_response_sensitive(GTK_DIALOG(GWY_TOOL(tool)->dialog), GTK_RESPONSE_APPLY, FALSE);
    if (n != 1 || !field) {
        gwy_rect_selection_labels_fill(tool->rlabels, NULL, NULL, tool->rsel, tool->isel);
        return;
    }

    gwy_rect_selection_labels_fill(tool->rlabels, selection, field, tool->rsel, tool->isel);

    /* Make Apply insensitive when the full image is selected, for whatever reason. There is nothting to crop then. */
    xres = gwy_field_get_xres(field);
    yres = gwy_field_get_yres(field);
    if (tool->isel[2] - tool->isel[0] == xres-1 && tool->isel[3] - tool->isel[1] == yres-1)
        return;

    gtk_dialog_set_response_sensitive(GTK_DIALOG(GWY_TOOL(tool)->dialog), GTK_RESPONSE_APPLY, TRUE);
}

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