/*
 *  $Id: xyzexport.c 30113 2026-06-22 11:50:26Z yeti-dn $
 *  Copyright (C) 2015-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.
 */

/* NB: Magic is in rawxyz. */

/**
 * [FILE-MAGIC-MISSING]
 * Export only.
 **/

#include "config.h"
#include <glib/gi18n-lib.h>
#include <string.h>
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
#include <glib/gstdio.h>
#include <gtk/gtk.h>
#include <gwy.h>

#include "err.h"

#define EXTENSION ".xyz"

enum {
    PARAM_DATA,
    PARAM_ADD_COMMENT,
    PARAM_DECIMAL_DOT,
    PARAM_PRECISION,
    PARAM_MASKING,

    INFO_CHANNEL,
};

typedef struct {
    GwyParams *params;
    GwyDataKind data_kind;
    gboolean have_mask;
    GwyScatter *scatter;
    gint idata;
} ModuleArgs;

static gboolean         module_register(void);
static gint             detect_file    (const GwyFileDetectInfo *fileinfo,
                                        gboolean only_name);
static gboolean         export_file    (GwyFile *file,
                                        const gchar *filename,
                                        GwyRunModeFlags mode,
                                        GError **error);
static GwyDialogOutcome run_gui        (ModuleArgs *args,
                                        const gchar *title);
static gboolean         export_field   (GwyField *field,
                                        GwyNield *mask,
                                        FILE *fh,
                                        ModuleArgs *args);
static gboolean         export_scatter (GwyScatter *scatter,
                                        FILE *fh,
                                        ModuleArgs *args);

static GwyModuleInfo module_info = {
    GWY_MODULE_ABI_VERSION,
    &module_register,
    N_("Exports data as simple XYZ text file."),
    "Yeti <yeti@gwyddion.net>",
    "5.0",
    "David Nečas (Yeti)",
    "2015",
};

GWY_MODULE_QUERY2(module_info, xyzexport)

static gboolean
module_register(void)
{
    gwy_file_func_register("xyzexport",
                           N_("XYZ text data (.xyz)"),
                           detect_file, NULL, NULL, export_file);

    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, gwy_file_func_current());
    gwy_param_def_add_scatter_data(paramdef, PARAM_DATA, "data", _("Value to export"));
    gwy_param_def_add_boolean(paramdef, PARAM_ADD_COMMENT, "add-comment", _("Add _informational comment header"),
                              FALSE);
    gwy_param_def_add_boolean(paramdef, PARAM_DECIMAL_DOT, "decimal-dot", _("Use _dot as decimal separator"), TRUE);
    gwy_param_def_add_int(paramdef, PARAM_PRECISION, "precision", _("_Precision"), 0, 16, 5);
    gwy_param_def_add_enum(paramdef, PARAM_MASKING, "Masking", NULL, GWY_TYPE_MASKING_TYPE, GWY_MASK_IGNORE);

    return paramdef;
}

static gint
detect_file(const GwyFileDetectInfo *fileinfo,
            G_GNUC_UNUSED gboolean only_name)
{
    return g_str_has_suffix(fileinfo->name_lowercase, EXTENSION) ? 20 : 0;
}

static gboolean
export_file(GwyFile *file,
            const gchar *filename,
            GwyRunModeFlags mode,
            GError **error)
{
    ModuleArgs args;
    GwyField *field;
    GwyNield *mask;
    GwyScatter *scatter;
    gint fid, sid;
    const gchar *title = NULL;
    GwyDialogOutcome outcome;
    FILE *fh = NULL;
    gboolean ok = FALSE;

    gwy_clear1(args);
    gwy_data_browser_get_current(GWY_APP_FIELD, &field,
                                 GWY_APP_FIELD_ID, &fid,
                                 GWY_APP_MASK_FIELD, &mask,
                                 GWY_APP_SCATTER, &scatter,
                                 GWY_APP_SCATTER_ID, &sid,
                                 GWY_APP_DATA_KIND, &args.data_kind,
                                 0);

    /* Ensure at most one is set.  We produce an error if no exportable data type is available or both types are
     * available but neither is active. When only one is available or one is active we assume that is what the user
     * wants to export. */
    if (field && scatter) {
        if (args.data_kind != GWY_FILE_IMAGE)
            field = NULL;
        if (args.data_kind != GWY_FILE_XYZ)
            scatter = NULL;
    }
    if (!field && (!scatter || (scatter && !gwy_scatter_get_n_values(scatter)))) {
        err_NO_CHANNEL_EXPORT(error);
        return FALSE;
    }

    if (field) {
        args.data_kind = GWY_FILE_IMAGE;
    }
    if (scatter) {
        args.data_kind = GWY_FILE_XYZ;
        args.scatter = scatter;
        mask = NULL;
    }

    args.params = gwy_params_new_from_settings(define_module_params());

    title = gwy_file_get_title(file, args.data_kind, field ? fid : sid);
    if (!title)
        title = _("Untitled");

    if (mode == GWY_RUN_INTERACTIVE) {
        outcome = run_gui(&args, title);
        gwy_params_save_to_settings(args.params);
        if (outcome == GWY_DIALOG_CANCEL) {
            err_CANCELLED(error);
            goto fail;
        }
    }

    if (!(fh = gwy_fopen(filename, "w"))) {
        err_OPEN_WRITE(error);
        goto fail;
    }

    if (scatter) {
        // Sanitise value for non-interactive use.
        args.idata = gwy_scatter_find_data_by_label(scatter, gwy_params_get_string(args.params, PARAM_DATA));
        args.idata = MAX(args.idata, 0);
    }

    if (gwy_params_get_boolean(args.params, PARAM_ADD_COMMENT)) {
        GwyUnit *xyunit = NULL, *zunit = NULL;
        gchar *s;

        if (args.data_kind == GWY_FILE_IMAGE) {
            gwy_fprintf(fh, "# %s %s\n", _("Channel:"), title);
            xyunit = gwy_field_get_unit_xy(field);
            zunit = gwy_field_get_unit_z(field);
        }
        else if (args.data_kind == GWY_FILE_XYZ) {
            const gchar *label = gwy_scatter_get_data_label(scatter, args.idata);
            if (label)
                gwy_fprintf(fh, "# %s %s (%s)\n", _("XYZ data:"), title, label);
            else
                gwy_fprintf(fh, "# %s %s\n", _("XYZ data:"), title);
            xyunit = gwy_scatter_get_unit_xy(scatter);
            zunit = gwy_scatter_get_unit_data(scatter, args.idata);
        }

        s = gwy_unit_get_string(xyunit, GWY_UNIT_FORMAT_VFMARKUP);
        gwy_fprintf(fh, "# %s %s\n", _("Lateral units:"), s);
        g_free(s);

        s = gwy_unit_get_string(zunit, GWY_UNIT_FORMAT_VFMARKUP);
        gwy_fprintf(fh, "# %s %s\n", _("Value units:"), s);
        g_free(s);
    }

    if (args.data_kind == GWY_FILE_IMAGE) {
        if (!export_field(field, mask, fh, &args)) {
            err_WRITE(error);
            goto fail;
        }
    }
    else if (args.data_kind == GWY_FILE_XYZ) {
        if (!export_scatter(scatter, fh, &args)) {
            err_WRITE(error);
            goto fail;
        }
    }

    ok = TRUE;

fail:
    g_object_unref(args.params);
    if (fh)
        fclose(fh);
    if (!ok)
        g_unlink(filename);

    return ok;
}

static GwyDialogOutcome
run_gui(ModuleArgs *args, const gchar *title)
{
    gboolean needs_decimal_dot_option;
    GwyDialog *dialog;
    GwyParamTable *table;
    gchar *desc = NULL;

    needs_decimal_dot_option = !gwy_strequal(gwy_get_decimal_separator(), ".");

    dialog = GWY_DIALOG(gwy_dialog_new(_("Export XYZ")));
    gwy_dialog_add_stock_buttons(dialog, GWY_STOCK_CANCEL, GWY_STOCK_OK, 0);

    if (args->data_kind == GWY_FILE_IMAGE)
        desc = _("Image");
    else if (args->data_kind == GWY_FILE_XYZ)
        desc = _("XYZ data");

    table = gwy_param_table_new(args->params);
    gwy_param_table_append_info(table, INFO_CHANNEL, desc);
    gwy_param_table_info_set_valuestr(table, INFO_CHANNEL, title);
    if (args->data_kind == GWY_FILE_XYZ)
        gwy_param_table_append_scatter_data(table, PARAM_DATA, args->scatter);
    gwy_param_table_append_header(table, -1, _("Options"));
    if (needs_decimal_dot_option)
        gwy_param_table_append_checkbox(table, PARAM_DECIMAL_DOT);
    gwy_param_table_append_checkbox(table, PARAM_ADD_COMMENT);
    gwy_param_table_append_slider(table, PARAM_PRECISION);
    gwy_param_table_slider_set_mapping(table, PARAM_PRECISION, GWY_SCALE_MAPPING_LINEAR);
    if (args->have_mask)
        gwy_param_table_append_radio(table, PARAM_MASKING);
    gwy_dialog_add_content(dialog, gwy_param_table_widget(table), FALSE, FALSE, 0);

    return gwy_dialog_run(dialog);
}

/* See libgwyddion/internal.h */
static inline gint
nielded_included(const gint *m, GwyMaskingType masking)
{
    if (masking == GWY_MASK_INCLUDE)
        return *m > 0;
    if (masking == GWY_MASK_EXCLUDE)
        return *m <= 0;
    return 1;
}

static gboolean
export_field(GwyField *field, GwyNield *mask,
             FILE *fh, ModuleArgs *args)
{
    gboolean decimal_dot = gwy_params_get_boolean(args->params, PARAM_DECIMAL_DOT);
    gint precision = gwy_params_get_int(args->params, PARAM_PRECISION);
    GwyMaskingType masking = gwy_params_get_masking(args->params, PARAM_MASKING, &mask);
    gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
    gdouble dx = gwy_field_get_dx(field), dy = gwy_field_get_dy(field);
    gdouble xoff = gwy_field_get_xoffset(field), yoff = gwy_field_get_yoffset(field);
    const gdouble *d = gwy_field_get_data_const(field);
    const gint *m = mask ? gwy_nield_get_data_const(mask) : NULL;
    GString *str = g_string_new(NULL);

    for (gint i = 0; i < yres; i++) {
        gdouble xyz[3];
        xyz[1] = dy*(i + 0.5) + yoff;
        for (gint j = 0; j < xres; j++) {
            if (!nielded_included(m + i*xres + j, masking))
                continue;

            xyz[0] = dx*(j + 0.5) + xoff;
            xyz[2] = d[i*xres + j];

            g_string_truncate(str, 0);
            gwy_append_doubles_to_gstring(str, xyz, 3, precision, "\t", decimal_dot);
            g_string_append_c(str, '\n');
            if (fputs(str->str, fh) == EOF) {
                g_string_free(str, TRUE);
                return FALSE;
            }
        }
    }
    g_string_free(str, TRUE);

    return TRUE;
}

static gboolean
export_scatter(GwyScatter *scatter,
               FILE *fh, ModuleArgs *args)
{
    gboolean decimal_dot = gwy_params_get_boolean(args->params, PARAM_DECIMAL_DOT);
    gint precision = gwy_params_get_int(args->params, PARAM_PRECISION);
    const GwyXY *xy = gwy_scatter_get_xy(scatter);
    const gdouble *data = gwy_scatter_get_data(scatter, args->idata);
    guint i, n = gwy_scatter_get_n_points(scatter);
    GString *str;

    str = g_string_new(NULL);
    for (i = 0; i < n; i++) {
        gdouble point[3];
        g_string_truncate(str, 0);
        point[0] = xy[i].x;
        point[1] = xy[i].y;
        point[2] = data[i];
        gwy_append_doubles_to_gstring(str, point, 3, precision, "\t", decimal_dot);
        g_string_append_c(str, '\n');
        if (fputs(str->str, fh) == EOF) {
            g_string_free(str, TRUE);
            return FALSE;
        }
    }
    g_string_free(str, TRUE);

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