/*
 *  $Id: graph_export_image.c 30047 2026-06-11 08:30:45Z yeti-dn $
 *  Copyright (C) 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 <glib/gi18n-lib.h>
#include <glib/gstdio.h>
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
#include <cairo.h>

#ifdef CAIRO_HAS_PDF_SURFACE
#include <cairo-pdf.h>
#endif

#ifdef CAIRO_HAS_SVG_SURFACE
#include <cairo-svg.h>
#endif

#ifdef CAIRO_HAS_PS_SURFACE
#include <cairo-ps.h>
#endif

#include <gtk/gtk.h>
#include <gwy.h>
#include "../file/err.h"

#include "libgwyapp/sanity.h"

enum {
    PARAM_XRES,
    PARAM_YRES,
    PARAM_SCREEN_SIZE,
    PARAM_SCALE,
    PARAM_CURVE,
    PARAM_ALL_CURVES,
    PARAM_AXES_VISIBLE,
    PARAM_OVERWRITE,
};

typedef struct {
    GwyParams *params;
    GwyGraphModel *gmodel;
    GdkPixbuf *pixbuf;
    GByteArray *buffer;
    /* Cached input properties. */
    gint graph_xres;
    gint graph_yres;
    gint area_xres;
    gint area_yres;
} ModuleArgs;

typedef struct {
    ModuleArgs *args;
    GtkWidget *dialog;
    GwyParamTable *table;
} ModuleGUI;

static gboolean         module_register    (void);
static void             module_main        (GwyGraph *graph);
static GwyDialogOutcome run_gui            (ModuleArgs *args);
static void             param_changed      (ModuleGUI *gui,
                                            gint id);
static void             render_graph       (ModuleArgs *args,
                                            const gchar *name);
static cairo_status_t   write_to_byte_array(void *user_data,
                                            const unsigned char *data,
                                            unsigned int length);
static void             save_image         (ModuleArgs *args);

static GwyModuleInfo module_info = {
    GWY_MODULE_ABI_VERSION,
    &module_register,
    N_("Export graph to raster or vector image formats"),
    "Yeti <yeti@gwyddion.net>",
    "1.0",
    "David Nečas (Yeti) & Petr Klapetek",
    "2025",
};

GWY_MODULE_QUERY2(module_info, graph_export_image)

static gboolean
module_register(void)
{
    gwy_graph_func_register("graph_export_raster",
                            module_main,
                            N_("/_Export/Pixel _Image..."),
                            GWY_ICON_GRAPH_EXPORT_PNG,
                            GWY_MENU_FLAG_GRAPH_CURVE,
                            N_("Export graph to a raster image"));
#if CAIRO_HAS_SVG_SURFACE
    gwy_graph_func_register("graph_export_svg",
                            module_main,
                            N_("/_Export/_Scalable Vector Graphics..."),
                            GWY_ICON_GRAPH_EXPORT_VECTOR,
                            GWY_MENU_FLAG_GRAPH_CURVE,
                            N_("Export graph to Scalable Vector Graphics (SVG)"));
#endif
#if CAIRO_HAS_PDF_SURFACE
    gwy_graph_func_register("graph_export_pdf",
                            module_main,
                            N_("/_Export/_Portable Document Format..."),
                            GWY_ICON_GRAPH_EXPORT_VECTOR,
                            GWY_MENU_FLAG_GRAPH_CURVE,
                            N_("Export graph to Portable Document Format (PDF)"));
#endif
#if CAIRO_HAS_PS_SURFACE
    gwy_graph_func_register("graph_export_ps",
                            module_main,
                            N_("/_Export/PostScrip_t..."),
                            GWY_ICON_GRAPH_EXPORT_VECTOR,
                            GWY_MENU_FLAG_GRAPH_CURVE,
                            N_("Export graph to PostScript"));
#endif

    return TRUE;
}

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

    if (paramdef)
        return paramdef;

    paramdef = gwy_param_def_new();
    /* Share settings among the export functions. */
    gwy_param_def_set_function_name(paramdef, "graph_export_image");
    gwy_param_def_add_int(paramdef, PARAM_XRES, "xres", _("_Horizontal size"), 200, 8000, 800);
    gwy_param_def_add_int(paramdef, PARAM_YRES, "yres", _("_Vertical size"), 160, 6000, 600);
    gwy_param_def_add_boolean(paramdef, PARAM_SCREEN_SIZE, "screen-size", _("Use _screen size"), TRUE);
    gwy_param_def_add_double(paramdef, PARAM_SCALE, "scale", _("_Scale"), 0.25, 8.0, 1.0);
    gwy_param_def_add_graph_curve(paramdef, PARAM_CURVE, "curve", NULL);
    gwy_param_def_add_boolean(paramdef, PARAM_ALL_CURVES, "all-curves", _("All curves"), TRUE);
    gwy_param_def_add_boolean(paramdef, PARAM_AXES_VISIBLE, "axes-visible", _("Axes visible"), TRUE);
    gwy_param_def_add_boolean(paramdef, PARAM_OVERWRITE, "overwrite", _("Overwrite output file without asking"), FALSE);

    return paramdef;
}

static void
module_main(GwyGraph *graph)
{
    const gchar *name = gwy_graph_func_current();
    ModuleArgs args;
    gwy_clear1(args);

    args.params = gwy_params_new_from_settings(define_module_params());
    args.gmodel = gwy_graph_get_model(graph);

    GdkRectangle allocation;
    gtk_widget_get_allocation(GTK_WIDGET(graph), &allocation);
    args.graph_xres = allocation.width;
    args.graph_yres = allocation.height;
    gtk_widget_get_allocation(gwy_graph_get_area(graph), &allocation);
    args.area_xres = allocation.width;
    args.area_yres = allocation.height;

    GwyDialogOutcome outcome = run_gui(&args);
    gwy_params_save_to_settings(args.params);
    if (outcome == GWY_DIALOG_CANCEL)
        goto end;

    render_graph(&args, name);
    g_assert(!args.pixbuf ^ !args.buffer);
    save_image(&args);

end:
    if (args.buffer)
        g_byte_array_free(args.buffer, TRUE);
    g_clear_object(&args.pixbuf);
    g_object_unref(args.params);
}

static GwyDialogOutcome
run_gui(ModuleArgs *args)
{
    ModuleGUI gui;
    gwy_clear1(gui);

    gui.args = args;

    gui.dialog = gwy_dialog_new(_("Export Graph to Image"));
    GwyDialog *dialog = GWY_DIALOG(gui.dialog);
    gwy_dialog_add_stock_buttons(dialog, GWY_STOCK_CANCEL, GWY_STOCK_OK, 0);

    GwyParamTable *table = gui.table = gwy_param_table_new(args->params);
    gwy_param_table_append_header(table, -1, _("Resolution"));
    gwy_param_table_append_checkbox(table, PARAM_SCREEN_SIZE);
    gwy_param_table_append_slider(table, PARAM_XRES);
    gwy_param_table_append_slider(table, PARAM_YRES);
    gwy_param_table_append_slider(table, PARAM_SCALE);
    gwy_param_table_append_header(table, -1, _("Options"));
    gwy_param_table_append_checkbox(table, PARAM_AXES_VISIBLE);
    gwy_param_table_append_checkbox(table, PARAM_ALL_CURVES);
    gwy_param_table_append_graph_curve(table, PARAM_CURVE, args->gmodel);
    gwy_param_table_append_separator(table);
    gwy_param_table_append_checkbox(table, PARAM_OVERWRITE);

    gwy_dialog_add_content(GWY_DIALOG(gui.dialog), gwy_param_table_widget(table), TRUE, TRUE, 0);
    g_signal_connect_swapped(table, "param-changed", G_CALLBACK(param_changed), &gui);

    return gwy_dialog_run(dialog);
}

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

    if (id < 0 || id == PARAM_SCREEN_SIZE) {
        gboolean use_screen_size = gwy_params_get_boolean(params, PARAM_SCREEN_SIZE);
        gwy_param_table_set_sensitive(table, PARAM_XRES, !use_screen_size);
        gwy_param_table_set_sensitive(table, PARAM_YRES, !use_screen_size);
        gwy_param_table_set_sensitive(table, PARAM_SCALE, !use_screen_size);
    }
    if (id < 0 || id == PARAM_SCREEN_SIZE || id == PARAM_AXES_VISIBLE) {
        gboolean use_screen_size = gwy_params_get_boolean(params, PARAM_SCREEN_SIZE);
        if (use_screen_size) {
            gboolean axes_visible = gwy_params_get_boolean(params, PARAM_AXES_VISIBLE);
            gwy_param_table_set_int(table, PARAM_XRES, axes_visible ? args->graph_xres : args->area_xres);
            gwy_param_table_set_int(table, PARAM_YRES, axes_visible ? args->graph_yres : args->area_yres);
            gwy_param_table_set_double(table, PARAM_SCALE, 1.0);
        }
    }
    if (id < 0 || id == PARAM_ALL_CURVES) {
        gboolean all_curves = gwy_params_get_boolean(params, PARAM_ALL_CURVES);
        gwy_param_table_set_sensitive(table, PARAM_CURVE, !all_curves);
    }
}

static void
render_graph(ModuleArgs *args, const gchar *name)
{
    GwyParams *params = args->params;
    gint width = gwy_params_get_int(params, PARAM_XRES);
    gint height = gwy_params_get_int(params, PARAM_YRES);
    gboolean all_curves = gwy_params_get_boolean(params, PARAM_ALL_CURVES);
    gboolean axes_visible = gwy_params_get_boolean(params, PARAM_AXES_VISIBLE);
    gdouble scale = gwy_params_get_double(params, PARAM_SCALE);
    gint curve = gwy_params_get_int(params, PARAM_CURVE);

    GwyGraphModel *gmodel = gwy_graph_model_new_alike(args->gmodel);
    gint ncurves = gwy_graph_model_get_n_curves(args->gmodel);
    if (all_curves) {
        for (gint i = 0; i < ncurves; i++)
            gwy_graph_model_add_curve(gmodel, gwy_graph_model_get_curve(args->gmodel, i));
    }
    else
        gwy_graph_model_add_curve(gmodel, gwy_graph_model_get_curve(args->gmodel, curve));

    /* Replicate the ranges, even if the graph would choose differently because of size. */
    gdouble min, max;
    gwy_graph_model_get_x_range(args->gmodel, &min, &max);
    g_object_set(gmodel,
                 "x-min", min, "x-min-set", TRUE,
                 "x-max", max, "x-max-set", TRUE,
                 NULL);
    gwy_graph_model_get_y_range(args->gmodel, &min, &max);
    g_object_set(gmodel,
                 "y-min", min, "y-min-set", TRUE,
                 "y-max", max, "y-max-set", TRUE,
                 NULL);

    GwyGraph *graph = GWY_GRAPH(gwy_graph_new(gmodel));
    g_object_ref_sink(graph);

    gwy_graph_set_axis_visible(graph, GTK_POS_LEFT, axes_visible);
    gwy_graph_set_axis_visible(graph, GTK_POS_BOTTOM, axes_visible);
    gwy_graph_set_axis_visible(graph, GTK_POS_TOP, FALSE);
    gwy_graph_set_axis_visible(graph, GTK_POS_RIGHT, FALSE);

    gtk_widget_show_all(GTK_WIDGET(graph));

    GdkRectangle allocation = { .x = 0, .y = 0, .width = width, .height = height };
    if (scale != 1.0) {
        allocation.width = GWY_ROUND(allocation.width/scale);
        allocation.height = GWY_ROUND(allocation.height/scale);
    }
    gtk_widget_size_allocate(GTK_WIDGET(graph), &allocation);

    GPtrArray *widgets = g_ptr_array_new();
    g_ptr_array_add(widgets, gwy_graph_get_area(graph));
    if (axes_visible) {
        g_ptr_array_add(widgets, gwy_graph_get_axis(graph, GTK_POS_LEFT));
        g_ptr_array_add(widgets, gwy_graph_get_axis(graph, GTK_POS_BOTTOM));
        g_ptr_array_add(widgets, gtk_grid_get_child_at(GTK_GRID(graph), 0, 2));
    }

    gboolean key_visible = TRUE;
    g_object_get(gmodel, "label-visible", &key_visible, NULL);
    if (key_visible)
        g_ptr_array_add(widgets, gtk_bin_get_child(GTK_BIN(gwy_graph_get_area(graph))));

    cairo_surface_t *surface = NULL;
    if (gwy_strequal(name, "graph_export_image")) {
        surface = gdk_window_create_similar_image_surface(NULL, CAIRO_FORMAT_RGB24, width, height, 0);
    }
    else {
        args->buffer = g_byte_array_new();
#if CAIRO_HAS_SVG_SURFACE
        if (gwy_strequal(name, "graph_export_svg"))
            surface = cairo_svg_surface_create_for_stream(write_to_byte_array, args->buffer, width, height);
#endif
#if CAIRO_HAS_PDF_SURFACE
        if (gwy_strequal(name, "graph_export_pdf"))
            surface = cairo_pdf_surface_create_for_stream(write_to_byte_array, args->buffer, width, height);
#endif
#if CAIRO_HAS_PS_SURFACE
        if (gwy_strequal(name, "graph_export_ps")) {
            surface = cairo_pdf_surface_create_for_stream(write_to_byte_array, args->buffer, width, height);
            cairo_ps_surface_set_eps(surface, TRUE);
        }
#endif
    }
    g_assert(surface);

    cairo_t *cr = cairo_create(surface);
    if (scale != 1.0)
        cairo_scale(cr, (gdouble)width/allocation.width, (gdouble)height/allocation.height);

    for (guint i = 0; i < widgets->len; i++) {
        GtkWidget *widget = g_ptr_array_index(widgets, i);
        gtk_widget_get_allocation(widget, &allocation);
        gwy_debug("SIZE ALLOCATION of %s: %d×%d at (%d,%d)",
                  G_OBJECT_TYPE_NAME(widget),
                  allocation.width, allocation.height, allocation.x, allocation.y);
        /* FIXME: We may need a dedicated method for this. */
        cairo_save(cr);
        cairo_translate(cr, allocation.x, allocation.y);
        cairo_rectangle(cr, 0, 0, allocation.width, allocation.height);
        cairo_clip(cr);
        GTK_WIDGET_GET_CLASS(widget)->draw(widget, cr);
        cairo_restore(cr);
    }
    cairo_surface_flush(surface);
    if (!args->buffer)
        args->pixbuf = gdk_pixbuf_get_from_surface(surface, 0, 0, width, height);
    cairo_destroy(cr);
    cairo_surface_destroy(surface);

    g_ptr_array_free(widgets, TRUE);
    g_object_unref(graph);
    g_object_unref(gmodel);
}

static cairo_status_t
write_to_byte_array(void *user_data, const unsigned char *data, unsigned int length)
{
    GByteArray *buffer = (GByteArray*)user_data;
    g_byte_array_append(buffer, data, length);
    return CAIRO_STATUS_SUCCESS;
}

static const gchar*
raster_format_from_extension(const gchar *filename)
{
    const gchar *format = "png";
    const gchar *ext;
    if ((ext = strrchr(filename, '.'))) {
        ext++;
        if (gwy_stramong(ext, "jpeg", "jpg", "jpe", NULL))
            format = "jpeg";
        else if (gwy_stramong(ext, "tiff", "tif", NULL))
            format = "tiff";
        else if (gwy_stramong(ext, "bmp", NULL))
            format = "bmp";
    }
    return format;
}

static void
save_image(ModuleArgs *args)
{
    GtkWidget *dialog = gtk_file_chooser_dialog_new(_("Export Graph to Image"),
                                                    NULL, GTK_FILE_CHOOSER_ACTION_SAVE, NULL, NULL);
    gwy_stock_add_button_to_dialog(GTK_DIALOG(dialog), GWY_STOCK_CANCEL);
    gwy_stock_add_button_to_dialog(GTK_DIALOG(dialog), GWY_STOCK_SAVE);
    gtk_dialog_set_default_response(GTK_DIALOG(dialog), GWY_RESPONSE_SAVE);
    gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(dialog), gwy_app_get_current_directory());

    gboolean overwrite = gwy_params_get_boolean(args->params, PARAM_OVERWRITE);
    gint response = gtk_dialog_run(GTK_DIALOG(dialog));
    if (response == GTK_RESPONSE_NONE)
        return;
    if (response != GWY_RESPONSE_SAVE || (!overwrite && !gwy_app_file_confirm_overwrite(dialog))) {
        gtk_widget_destroy(dialog);
        return;
    }

    gchar *filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog));
    gtk_widget_destroy(dialog);

    GError *error = NULL;
    if (args->pixbuf) {
        const gchar *format = raster_format_from_extension(filename);
        gdk_pixbuf_save(args->pixbuf, filename, format, &error, NULL);
    }
    else {
        FILE *fh = g_fopen(filename, "wb");
        if (fh) {
            gboolean ok = fwrite(args->buffer->data, 1, args->buffer->len, fh) == args->buffer->len;
            if (!ok)
                err_WRITE(&error);
            if (fclose(fh) && ok) {
                ok = FALSE;
                err_WRITE(&error);
            }
            if (!ok)
                g_unlink(filename);
        }
        else
            err_OPEN_WRITE(&error);
    }

    if (!error) {
        g_free(filename);
        return;
    }

    dialog = gtk_message_dialog_new(NULL, 0, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, _("Saving of `%s' failed"), filename);
    gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(dialog), "%s", error->message);
    gtk_widget_show_all(dialog);
    if (gtk_dialog_run(GTK_DIALOG(dialog)) != GTK_RESPONSE_NONE)
        gtk_widget_destroy(dialog);
    g_clear_error(&error);
    g_free(filename);
}

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