/*
 *  $Id: results-export.c 29656 2026-03-26 10:21:10Z yeti-dn $
 *  Copyright (C) 2017-2025 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 <string.h>
#include <glib/gi18n-lib.h>

#include "libgwyui/combo-boxes.h"
#include "libgwyui/icons.h"

#include "libgwyapp/app.h"
#include "libgwyapp/menu.h"
#include "libgwyapp/data-browser.h"
#include "libgwyapp/module-utils.h"
#include "libgwyapp/results-export.h"
#include "libgwyapp/sanity.h"

enum {
    SGNL_COPY,
    SGNL_SAVE,
    SGNL_FORMAT_CHANGED,
    NUM_SIGNALS
};

struct _GwyResultsExportPrivate {
    GwyResults *results;

    GtkWidget *format;
    GtkWidget *machine;
    GtkWidget *copy;
    GtkWidget *save;

    GwyResultsReportType report_format;
    GwyResultsExportStyle style;

    gchar *title;
    GwyResultsFormatFunc format_func;
    gpointer format_data;
    gboolean updating;
};

static void       dispose               (GObject *object);
static void       finalize              (GObject *object);
static void       rexport_save          (GwyResultsExport *rexport);
static void       rexport_copy          (GwyResultsExport *rexport);
static void       save_clicked          (GwyResultsExport *rexport);
static void       copy_clicked          (GwyResultsExport *rexport);
static GtkWidget* create_image_button   (GwyResultsExport *rexport,
                                         const gchar *icon_name,
                                         const gchar *tooltip);
static void       update_format_controls(GwyResultsExport *rexport);
static void       machine_changed       (GwyResultsExport *rexport,
                                         GtkToggleButton *toggle);
static void       format_changed        (GtkComboBox *combo,
                                         GwyResultsExport *rexport);

static guint signals[NUM_SIGNALS];

G_DEFINE_TYPE_WITH_CODE(GwyResultsExport, gwy_results_export, GTK_TYPE_BOX,
                        G_ADD_PRIVATE(GwyResultsExport))

static void
gwy_results_export_class_init(GwyResultsExportClass *klass)
{
    GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
    GType type = G_TYPE_FROM_CLASS(klass);

    gobject_class->dispose = dispose;
    gobject_class->finalize = finalize;
    klass->save = rexport_save;
    klass->copy = rexport_copy;

    /* NB: "copy" and "save" signals are created with LAST flag so that user's handlers are run before save_impl() and
     * copy_impl() and have a chance to update the results. */

    /**
     * GwyResultsExport::copy:
     * @rexport: The #GwyResultsExport which received the signal.
     *
     * The ::copy signal is emitted when user presses the Copy button.
     **/
    signals[SGNL_COPY] = g_signal_new("copy", type,
                                      G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
                                      G_STRUCT_OFFSET(GwyResultsExportClass, copy),
                                      NULL, NULL,
                                      g_cclosure_marshal_VOID__VOID,
                                      G_TYPE_NONE, 0);
    g_signal_set_va_marshaller(signals[SGNL_COPY], type, g_cclosure_marshal_VOID__VOIDv);

    /**
     * GwyResultsExport::save:
     * @rexport: The #GwyResultsExport which received the signal.
     *
     * The ::save signal is emitted when user presses the Save button.
     **/
    signals[SGNL_SAVE] = g_signal_new("save", type,
                                      G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
                                      G_STRUCT_OFFSET(GwyResultsExportClass, save),
                                      NULL, NULL,
                                      g_cclosure_marshal_VOID__VOID,
                                      G_TYPE_NONE, 0);
    g_signal_set_va_marshaller(signals[SGNL_SAVE], type, g_cclosure_marshal_VOID__VOIDv);

    /**
     * GwyResultsExport::format-changed:
     * @rexport: The #GwyResultsExport which received the signal.
     *
     * The ::format-changed signal is emitted when the selected format changes.
     **/
    signals[SGNL_FORMAT_CHANGED] = g_signal_new("format-changed", type,
                                                G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
                                                G_STRUCT_OFFSET(GwyResultsExportClass, format_changed),
                                                NULL, NULL,
                                                g_cclosure_marshal_VOID__VOID,
                                                G_TYPE_NONE, 0);
    g_signal_set_va_marshaller(signals[SGNL_FORMAT_CHANGED], type, g_cclosure_marshal_VOID__VOIDv);
}

static void
gwy_results_export_init(GwyResultsExport *rexport)
{
    GwyResultsExportPrivate *priv;

    priv = rexport->priv = gwy_results_export_get_instance_private(rexport);
    priv->report_format = GWY_RESULTS_REPORT_COLON;
    priv->style = GWY_RESULTS_EXPORT_PARAMETERS;

    gtk_orientable_set_orientation(GTK_ORIENTABLE(rexport), GTK_ORIENTATION_HORIZONTAL);

    priv->save = create_image_button(rexport, GWY_ICON_GTK_SAVE, _("Save results to a file"));
    priv->copy = create_image_button(rexport, GWY_ICON_GTK_COPY, _("Copy results to clipboard"));
    g_signal_connect_swapped(priv->save, "clicked", G_CALLBACK(save_clicked), rexport);
    g_signal_connect_swapped(priv->copy, "clicked", G_CALLBACK(copy_clicked), rexport);

    priv->updating = TRUE;
    update_format_controls(rexport);
    priv->updating = FALSE;
}

static GtkWidget*
create_image_button(GwyResultsExport *rexport,
                    const gchar *icon_name, const gchar *tooltip)
{
    GtkWidget *button;

    button = gtk_button_new();
    gtk_button_set_relief(GTK_BUTTON(button), GTK_RELIEF_NONE);
    gtk_widget_set_tooltip_text(button, tooltip);
    gtk_container_add(GTK_CONTAINER(button), gtk_image_new_from_icon_name(icon_name, GTK_ICON_SIZE_SMALL_TOOLBAR));
    gtk_box_pack_end(GTK_BOX(rexport), button, FALSE, FALSE, 0);

    return button;
}

static void
dispose(GObject *object)
{
    GwyResultsExport *rexport = GWY_RESULTS_EXPORT(object);
    g_clear_object(&rexport->priv->results);
    /* FIXME: Can we have the file save dialogue open when we get here? */
}

static void
finalize(GObject *object)
{
    GwyResultsExport *rexport = GWY_RESULTS_EXPORT(object);
    g_free(rexport->priv->title);
}

static GtkWidget*
create_image_toggle(GwyResultsExport *rexport,
                    const gchar *icon_name, const gchar *tooltip,
                    gboolean active)
{
    GtkWidget *button;

    button = gtk_toggle_button_new();
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(button), active);
    gtk_button_set_relief(GTK_BUTTON(button), GTK_RELIEF_NONE);
    gtk_widget_set_tooltip_text(button, tooltip);
    gtk_container_add(GTK_CONTAINER(button), gtk_image_new_from_icon_name(icon_name, GTK_ICON_SIZE_SMALL_TOOLBAR));
    gtk_box_pack_end(GTK_BOX(rexport), button, FALSE, FALSE, 0);

    return button;
}

static void
save_clicked(GwyResultsExport *rexport)
{
    g_signal_emit(rexport, signals[SGNL_SAVE], 0);
}

static void
copy_clicked(GwyResultsExport *rexport)
{
    g_signal_emit(rexport, signals[SGNL_COPY], 0);
}

static void
machine_changed(GwyResultsExport *rexport, GtkToggleButton *toggle)
{
    GwyResultsExportPrivate *priv = rexport->priv;
    if (rexport->priv->updating)
        return;

    if (gtk_toggle_button_get_active(toggle))
        priv->report_format |= GWY_RESULTS_REPORT_MACHINE;
    else
        priv->report_format &= ~GWY_RESULTS_REPORT_MACHINE;

    g_signal_emit(rexport, signals[SGNL_FORMAT_CHANGED], 0);
}

static void
format_changed(GtkComboBox *combo, GwyResultsExport *rexport)
{
    GwyResultsExportPrivate *priv = rexport->priv;
    if (priv->updating)
        return;

    guint flags = (priv->report_format & GWY_RESULTS_REPORT_MACHINE);
    priv->report_format = gwy_enum_combo_box_get_active(combo);
    priv->report_format |= flags;

    g_signal_emit(rexport, signals[SGNL_FORMAT_CHANGED], 0);
}

static gchar*
format_report(GwyResultsExport *rexport)
{
    GwyResultsExportPrivate *priv = rexport->priv;

    if (priv->format_func)
        return priv->format_func(priv->format_data);
    if (priv->results)
        return gwy_results_create_report(rexport->priv->results, priv->report_format);
    return NULL;
}

static void
rexport_save(GwyResultsExport *rexport)
{
    GtkWindow *window;
    const gchar *title;
    gchar *text;

    if (!(text = format_report(rexport)))
        return;

    if ((window = (GtkWindow*)gtk_widget_get_toplevel(GTK_WIDGET(rexport)))) {
        if (!GTK_IS_WINDOW(window))
            window = NULL;
    }
    GwyResultsExportPrivate *priv = rexport->priv;
    title = priv->title ? priv->title : _("Save Results to File");
    gwy_save_auxiliary_data(title, window, text, -1, FALSE);
    g_free(text);
}

static void
rexport_copy(GwyResultsExport *rexport)
{
    GtkClipboard *clipboard;
    GdkDisplay *display;
    gchar *text;

    if (!(text = format_report(rexport)))
        return;

    display = gtk_widget_get_display(GTK_WIDGET(rexport));
    clipboard = gtk_clipboard_get_for_display(display, GDK_SELECTION_CLIPBOARD);
    gtk_clipboard_set_text(clipboard, text, -1);
    g_free(text);
}

/**
 * gwy_results_export_new:
 * @format: Format to select.
 *
 * Creates new controls for result set export.
 *
 * Returns: A newly created result set export widget.
 **/
GtkWidget*
gwy_results_export_new(GwyResultsReportType format)
{
    GwyResultsExport *rexport = g_object_new(GWY_TYPE_RESULTS_EXPORT, NULL);
    GwyResultsExportPrivate *priv = rexport->priv;

    priv->report_format = format;
    priv->updating = TRUE;
    update_format_controls(rexport);
    priv->updating = FALSE;

    return (GtkWidget*)rexport;
}

static void
update_format_controls(GwyResultsExport *rexport)
{
    static const GwyEnum formats[] = {
        { N_("Colon:"), GWY_RESULTS_REPORT_COLON,  },
        { N_("TAB"),    GWY_RESULTS_REPORT_TABSEP, },
        { N_("CSV"),    GWY_RESULTS_REPORT_CSV,    },
    };

    GwyResultsExportPrivate *priv = rexport->priv;
    GtkWidget *combo;
    GtkTreeModel *model;
    gint off, copypos, wantnformats, nformats = 0;
    guint base_format;
    gboolean is_machine;

    if (priv->style == GWY_RESULTS_EXPORT_FIXED_FORMAT) {
        if (priv->format) {
            gtk_widget_destroy(priv->format);
            priv->format = NULL;
        }
        if (priv->machine) {
            gtk_widget_destroy(priv->machine);
            priv->machine = NULL;
        }
        return;
    }

    gtk_container_child_get(GTK_CONTAINER(rexport), priv->copy, "position", &copypos, NULL);
    if (priv->format) {
        model = gtk_combo_box_get_model(GTK_COMBO_BOX(priv->format));
        nformats = gtk_tree_model_iter_n_children(model, NULL);
    }

    base_format = (priv->report_format & ~GWY_RESULTS_REPORT_MACHINE);
    if (priv->style == GWY_RESULTS_EXPORT_TABULAR_DATA) {
        wantnformats = 2;
        if (base_format == GWY_RESULTS_REPORT_COLON)
            base_format = GWY_RESULTS_REPORT_TABSEP;
    }
    else
        wantnformats = 3;

    if (wantnformats != nformats) {
        off = (wantnformats == 2) ? 1 : 0;
        if (priv->format)
            gtk_widget_destroy(priv->format);
        combo = gwy_enum_combo_box_new(formats + off, wantnformats, G_CALLBACK(format_changed), rexport,
                                       base_format, TRUE);
        priv->format = combo;
        gtk_widget_set_tooltip_text(combo, _("Result formatting"));
        gtk_box_pack_end(GTK_BOX(rexport), combo, FALSE, FALSE, 0);
        /* A bit confusing when packing from end, but +1 seems to work. */
        gtk_box_reorder_child(GTK_BOX(rexport), combo, copypos+1);
    }

    if (!priv->machine) {
        is_machine = priv->report_format & GWY_RESULTS_REPORT_MACHINE;
        priv->machine = create_image_toggle(rexport, GWY_ICON_SCIENTIFIC_NUMBER_FORMAT,
                                            _("Machine-readable format"), is_machine);
        g_signal_connect_swapped(priv->machine, "toggled", G_CALLBACK(machine_changed), rexport);
    }

    /* The format might not changed but was kind of undefined.  Emit a signal to make sure listeneres set the format
     * now. */
    if (!priv->updating)
        g_signal_emit(rexport, signals[SGNL_FORMAT_CHANGED], 0);
}

/**
 * gwy_results_export_set_format:
 * @rexport: Controls for result set export.
 * @format: Format to select.
 *
 * Sets the selected format in result set export controls.
 **/
void
gwy_results_export_set_format(GwyResultsExport *rexport,
                              GwyResultsReportType format)
{
    g_return_if_fail(GWY_IS_RESULTS_EXPORT(rexport));

    GwyResultsExportPrivate *priv = rexport->priv;
    if (format == priv->report_format)
        return;

    gboolean for_machine = !!(format & GWY_RESULTS_REPORT_MACHINE);
    gint base_format = (format & ~GWY_RESULTS_REPORT_MACHINE);
    g_return_if_fail(base_format == GWY_RESULTS_REPORT_COLON
                     || base_format == GWY_RESULTS_REPORT_TABSEP
                     || base_format == GWY_RESULTS_REPORT_CSV);

    g_return_if_fail(!rexport->priv->updating);
    priv->report_format = format;
    rexport->priv->updating = TRUE;
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(priv->machine), for_machine);
    gwy_enum_combo_box_set_active(GTK_COMBO_BOX(priv->format), base_format);
    rexport->priv->updating = FALSE;
}

/**
 * gwy_results_export_get_format:
 * @rexport: Controls for result set export.
 *
 * Gets the selected format in result set export controls.
 *
 * Returns: Currently selected format.
 **/
GwyResultsReportType
gwy_results_export_get_format(GwyResultsExport *rexport)
{
    g_return_val_if_fail(GWY_IS_RESULTS_EXPORT(rexport), 0);
    return rexport->priv->report_format;
}

/**
 * gwy_results_export_set_results:
 * @rexport: Controls for result set export.
 * @results: Set of reported scalar values.  May be %NULL to unset.
 *
 * Sets the set of scalar values to save or copy by result set export controls.
 *
 * If you supply a result set to save or copy @rexport handles the Save and Copy buttons itself.  So you do not need
 * to connect to the "copy" and "save" signals, except if you need to ensure @results are updated.
 **/
void
gwy_results_export_set_results(GwyResultsExport *rexport,
                               GwyResults *results)
{
    g_return_if_fail(GWY_IS_RESULTS_EXPORT(rexport));
    gwy_set_member_object(rexport, results, GWY_TYPE_RESULTS, &rexport->priv->results, NULL);
}

/**
 * gwy_results_export_get_results:
 * @rexport: Controls for result set export.
 *
 * Gets the set of scalar values to save or copy by result set export controls.
 *
 * Returns: The #GwyResults object, or %NULL.
 **/
GwyResults*
gwy_results_export_get_results(GwyResultsExport *rexport)
{
    g_return_val_if_fail(GWY_IS_RESULTS_EXPORT(rexport), NULL);
    return rexport->priv->results;
}

/**
 * gwy_results_export_set_format_func:
 * @rexport: Controls for result set export.
 * @func: The formatting function.
 * @user_data: Data passed to @func.
 *
 * Sets the report formatting function for result set export controls.
 *
 * Setting the formatting function allows automated handling of Copy and Save buttons, even when there is not
 * #GwyResults object. It is intended to be used with %GWY_RESULTS_EXPORT_FIXED_FORMAT. However, it takes precedence
 * even when a #GwyResults object is set.
 **/
void
gwy_results_export_set_format_func(GwyResultsExport *rexport,
                                   GwyResultsFormatFunc func,
                                   gpointer user_data)
{
    g_return_if_fail(GWY_IS_RESULTS_EXPORT(rexport));
    rexport->priv->format_func = func;
    rexport->priv->format_data = user_data;
}

/**
 * gwy_results_export_set_title:
 * @rexport: Controls for result set export.
 * @title: File save dialogue title (or %NULL to use the default).
 *
 * Sets the title of file save dialogue for result set export controls.
 **/
void
gwy_results_export_set_title(GwyResultsExport *rexport,
                             const gchar *title)
{
    g_return_if_fail(GWY_IS_RESULTS_EXPORT(rexport));
    gwy_assign_string(&rexport->priv->title, title);
}

/**
 * gwy_results_export_set_style:
 * @rexport: Controls for result set export.
 * @style: Report style.
 *
 * Sets the report style for result set export controls.
 *
 * The report style determines which controls will be shown.  For %GWY_RESULTS_EXPORT_FIXED_FORMAT only the action
 * buttons are shown. Tabular data (%GWY_RESULTS_EXPORT_TABULAR_DATA) do not have the %GWY_RESULTS_REPORT_COLON
 * option.  The default style %GWY_RESULTS_EXPORT_PARAMETERS have the full controls.
 **/
void
gwy_results_export_set_style(GwyResultsExport *rexport,
                             GwyResultsExportStyle style)
{
    g_return_if_fail(GWY_IS_RESULTS_EXPORT(rexport));
    GwyResultsExportPrivate *priv = rexport->priv;
    if (priv->style == style)
        return;
    priv->style = style;
    update_format_controls(rexport);
}

/**
 * gwy_results_export_get_style:
 * @rexport: Controls for result set export.
 *
 * Gets the report style for result set export controls.
 *
 * See gwy_results_export_set_style() for discussion.
 *
 * Returns: The style of the results export controls.
 **/
GwyResultsExportStyle
gwy_results_export_get_style(GwyResultsExport *rexport)
{
    g_return_val_if_fail(GWY_IS_RESULTS_EXPORT(rexport), GWY_RESULTS_EXPORT_PARAMETERS);
    return rexport->priv->style;
}

/**
 * gwy_results_export_set_actions_sensitive:
 * @rexport: Controls for result set export.
 * @sensitive: %TRUE to make the action buttons sensitive, %FALSE to make them insensitive.
 *
 * Makes the action buttons (copy and save) sensitive or insensitive.
 *
 * This is generally preferred to making the entire widget insensitive as the format can be selected independently on
 * whether there is already anything to export.
 **/
void
gwy_results_export_set_actions_sensitive(GwyResultsExport *rexport,
                                         gboolean sensitive)
{
    g_return_if_fail(GWY_IS_RESULTS_EXPORT(rexport));
    GwyResultsExportPrivate *priv = rexport->priv;
    gtk_widget_set_sensitive(priv->copy, sensitive);
    gtk_widget_set_sensitive(priv->save, sensitive);
}

/**
 * gwy_results_fill_filename:
 * @results: Set of reported scalar values.
 * @resid: Value identifier.
 * @file: Data container corresponding to a file.
 *
 * Fills data file name in a set of reported scalar values.
 *
 * This is a helper function for #GwyResults.  If @file has no file name
 * associated the value @id is set to N.A.
 **/
void
gwy_results_fill_filename(GwyResults *results,
                          const gchar *resid,
                          GwyFile *file)
{
    g_return_if_fail(GWY_IS_RESULTS(results));
    g_return_if_fail(resid);
    g_return_if_fail(GWY_IS_FILE(file));

    const gchar *name;
    if (gwy_dict_gis_string(GWY_DICT(file), gwy_file_key_filename(), &name))
        gwy_results_fill_values(results, resid, name, NULL);
    else
        gwy_results_set_na(results, resid, NULL);
}

/**
 * gwy_results_fill_data_name:
 * @results: Set of reported scalar values.
 * @resid: String value identifier.
 * @file: Data container corresponding to a file.
 * @data_kind: Type of the data object.
 * @dataid: Numerical id of the object.
 *
 * Fills the name of a data object in a set of reported scalar values.
 *
 * This is a helper function for #GwyResults to fill names of images or graphs. If the data object has no title, the
 * associated the value @id is set to N.A.
 **/
void
gwy_results_fill_data_name(GwyResults *results,
                           const gchar *resid,
                           GwyFile *file,
                           GwyDataKind data_kind,
                           gint dataid)
{
    g_return_if_fail(GWY_IS_RESULTS(results));
    g_return_if_fail(resid);
    g_return_if_fail(GWY_IS_FILE(file));

    const gchar *name;
    if ((name = gwy_file_get_title(file, data_kind, dataid)))
        gwy_results_fill_values(results, resid, name, NULL);
    else
        gwy_results_set_na(results, resid, NULL);
}

/**
 * gwy_results_fill_graph_curve:
 * @results: Set of reported scalar values.
 * @resid: String value identifier.
 * @curvemodel: A graph curve model.
 *
 * Fills graph curve description in a set of reported scalar values.
 *
 * This is a helper function for #GwyResults.
 **/
void
gwy_results_fill_graph_curve(GwyResults *results,
                             const gchar *resid,
                             GwyGraphCurveModel *curvemodel)
{
    gchar *title = NULL;

    g_return_if_fail(GWY_IS_GRAPH_CURVE_MODEL(curvemodel));
    g_return_if_fail(GWY_IS_RESULTS(results));
    g_return_if_fail(resid);

    g_object_get(curvemodel, "description", &title, NULL);
    gwy_results_fill_values(results, resid, title, NULL);
    g_free(title);
}

/**
 * gwy_results_fill_lawn_curve:
 * @results: Set of reported scalar values.
 * @resid: String value identifier.
 * @lawn: Curve map data object.
 * @i: Curve number in @lawn.
 *
 * Fills lawn curve description in a set of reported scalar values.
 *
 * This is a helper function for #GwyResults.
 **/
void
gwy_results_fill_lawn_curve(GwyResults *results,
                            const gchar *resid,
                            GwyLawn *lawn,
                            gint i)
{
    const gchar *title;
    gchar *s;

    g_return_if_fail(GWY_IS_LAWN(lawn));
    g_return_if_fail(GWY_IS_RESULTS(results));
    g_return_if_fail(resid);
    g_return_if_fail(i >= 0 && i < gwy_lawn_get_n_curves(lawn));

    if ((title = gwy_lawn_get_curve_label(lawn, i)))
        gwy_results_fill_values(results, resid, title, NULL);
    else {
        s = g_strdup_printf("%s %d", _("Untitled"), i);
        gwy_results_fill_values(results, resid, s, NULL);
        g_free(s);
    }
}

/**
 * SECTION: results-export
 * @title: GwyResultsExport
 * @short_description: Controls for value set export
 **/

/**
 * GwyResultsFormatFunc:
 * @user_data: Data passed to gwy_results_export_set_format_func().
 *
 * The type of result formatting function.
 *
 * Returns: Formatted report as a newly allocated string. #GwyResultsExport will free it using g_free() after saving
 *          it to a file or copying to the clipboard.
 **/

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