/*
 *  $Id: gxyzffile.c 30047 2026-06-11 08:30:45Z yeti-dn $
 *  Copyright (C) 2013-2026 David Necas (Yeti).
 *
 *  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.
 */

/**
 * [FILE-MAGIC-FREEDESKTOP]
 * <mime-type type="application/x-gxyzf-spm">
 *   <comment>Gwyddion XYZ data</comment>
 *   <magic priority="80">
 *     <match type="string" offset="0" value="Gwyddion XYZ Field 1.0\n"/>
 *   </magic>
 *   <glob pattern="*.gxyzf"/>
 *   <glob pattern="*.GXYZF"/>
 * </mime-type>
 **/

/**
 * [FILE-MAGIC-FILEMAGIC]
 * # Gwyddion simple XYZ files (GXYZF), see http://gwyddion.net/
 * 0 string Gwyddion\ XYZ\ Field\ 1.0\x0d\x0a Gwyddion XYZ field SPM data version 1.0
 **/

/**
 * [FILE-MAGIC-USERGUIDE]
 * Gwyddion XYZ data
 * .gxyzf
 * Read Export
 **/

#include "config.h"
#include <glib/gi18n-lib.h>
#include <string.h>
#include <stdlib.h>
#include <glib/gstdio.h>
#include <gwy.h>

#include "err.h"

#define MAGIC "Gwyddion XYZ Field 1.0\n"
#define MAGIC_SIZE (sizeof(MAGIC)-1)
#define EXTENSION ".gxyzf"

enum {
    PARAM_ALL_CHANNELS,

    INFO_CHANNEL,
};

typedef struct {
    GwyParams *params;
    GwyDataKind data_kind;
} ModuleArgs;

static gboolean         module_register         (void);
static GwyParamDef*     define_module_params    (void);
static gint             detect_file             (const GwyFileDetectInfo *fileinfo,
                                                 gboolean only_name);
static GwyFile*         load_file               (const gchar *filename,
                                                 GwyRunModeFlags mode,
                                                 GError **error);
static gboolean         export_file             (GwyFile *file,
                                                 const gchar *filename,
                                                 GwyRunModeFlags mode,
                                                 GError **error);
static GwyDialogOutcome run_gui_images          (ModuleArgs *args,
                                                 const gchar *title);
static gboolean         export_fields           (const gchar *filename,
                                                 GwyFile *file,
                                                 gint id,
                                                 const ModuleArgs *args,
                                                 GError **error);
static gint*            gather_compatible_fields(GwyFile *data,
                                                 GwyField *dfield,
                                                 guint *nchannels);
static gboolean         export_xyz              (const gchar *filename,
                                                 GwyFile *file,
                                                 gint id,
                                                 GError **error);
static gboolean         write_header            (FILE *fh,
                                                 guint nchannels,
                                                 guint npoints,
                                                 const gchar **titles,
                                                 GwyUnit *xyunit,
                                                 GwyUnit **zunits,
                                                 gint xres,
                                                 gint yres,
                                                 GError **error);

static GwyModuleInfo module_info = {
    GWY_MODULE_ABI_VERSION,
    &module_register,
    N_("Imports Gwyddion XYZ field files."),
    "Yeti <yeti@gwyddion.net>",
    "3.0",
    "David Nečas (Yeti)",
    "2013",
};

GWY_MODULE_QUERY2(module_info, gxyzffile)

static gboolean
module_register(void)
{
    gwy_file_func_register("gxyzfile",
                           N_("GwyXYZ data files"),
                           detect_file, load_file, 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_boolean(paramdef, PARAM_ALL_CHANNELS, "all-channels",
                              _("Multi-channel file with all compatible data"), FALSE);
    return paramdef;
}

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

    if (fileinfo->file_size < MAGIC_SIZE || memcmp(fileinfo->head, MAGIC, MAGIC_SIZE) != 0)
        return 0;

    return 100;
}

static inline void
append_double(gdouble *target, const gdouble v)
{
    union { guchar pp[8]; double d; } u;
    u.d = v;
#if (G_BYTE_ORDER == G_BIG_ENDIAN)
    GWY_SWAP(guchar, u.pp[0], u.pp[7]);
    GWY_SWAP(guchar, u.pp[1], u.pp[6]);
    GWY_SWAP(guchar, u.pp[2], u.pp[5]);
    GWY_SWAP(guchar, u.pp[3], u.pp[4]);
#endif
    *target = u.d;
}

static GwyFile*
load_file(const gchar *filename,
          G_GNUC_UNUSED GwyRunModeFlags mode,
          GError **error)
{
    GwyFile *file = NULL;
    GwyTextHeaderParser parser;
    GHashTable *hash = NULL;
    guchar *p, *value, *buffer = NULL, *header = NULL, *datap;
    gsize size;
    GError *err = NULL;
    GwyUnit **zunits = NULL;
    GwyUnit *xyunit = NULL, *zunit = NULL;
    guint nchan = 0, pointlen, pointsize, npoints, i, id;

    if (!g_file_get_contents(filename, (gchar**)&buffer, &size, &err)) {
        err_GET_FILE_CONTENTS(error, &err);
        goto fail;
    }

    if (size < MAGIC_SIZE || memcmp(buffer, MAGIC, MAGIC_SIZE) != 0) {
        err_FILE_TYPE(error, "Gwyddion XYZ Field");
        goto fail;
    }

    p = buffer + MAGIC_SIZE;
    datap = memchr(p, '\0', size - (p - buffer));
    if (!datap) {
        err_TRUNCATED_HEADER(error);
        goto fail;
    }
    header = g_strdup(p);
    datap += 8 - ((datap - buffer) % 8);

    gwy_clear1(parser);
    parser.key_value_separator = "=";
    if (!(hash = gwy_text_header_parse(header, &parser, NULL, NULL))) {
        g_propagate_error(error, err);
        goto fail;
    }

    if (!(value = g_hash_table_lookup(hash, "NChannels"))) {
        err_MISSING_FIELD(error, "NChannels");
        goto fail;
    }
    nchan = atoi(value);
    if (nchan < 1 || nchan > 1024) {
        err_INVALID(error, "NChannels");
        goto fail;
    }

    pointlen = nchan + 2;
    pointsize = pointlen*sizeof(gdouble);
    if ((size - (datap - buffer)) % pointsize) {
        g_set_error(error, GWY_MODULE_FILE_ERROR, GWY_MODULE_FILE_ERROR_DATA,
                    _("Data size %lu is not a multiple of point size %u."),
                    (gulong)(size - (datap - buffer)), pointsize);
        goto fail;
    }
    npoints = (size - (datap - buffer))/pointsize;

    value = g_hash_table_lookup(hash, "XYUnits");
    xyunit = gwy_unit_new(value);

    /* If there is ZUnits it applies to all channels. */
    if ((value = g_hash_table_lookup(hash, "ZUnits")))
        zunit = gwy_unit_new(value);
    else {
        zunits = g_new0(GwyUnit*, nchan);
        for (id = 0; id < nchan; id++) {
            gchar *buf = g_strdup_printf("ZUnits%u", id+1);
            value = g_hash_table_lookup(hash, buf);
            g_free(buf);
            zunits[id] = gwy_unit_new(value);
        }
    }

    const gdouble *packed = (const gdouble*)datap;
    gdouble *buf = NULL;
#if (G_BYTE_ORDER == G_BIG_ENDIAN)
    packed = buf = g_new(gdouble, npoints*pointlen);
    gwy_memcpy_byte_swap(datap, (guint8*)buf, sizeof(gdouble), npoints*pointlen, 7);
#endif

    file = gwy_file_new_in_construction();
    GwyScatter *scatter = gwy_scatter_new(nchan);
    gwy_unit_assign(gwy_scatter_get_unit_xy(scatter), xyunit);
    gwy_scatter_set_from_packed(scatter, packed, nchan, npoints);
    g_free(buf);

    for (id = 0; id < nchan; id++) {
        GwyUnit *unit = gwy_scatter_get_unit_data(scatter, id);
        if (zunit)
            gwy_unit_assign(unit, zunit);
        else
            gwy_unit_assign(unit, zunits[id]);

        gchar *key = g_strdup_printf("Title%u", id+1);
        if ((value = g_hash_table_lookup(hash, key)))
            gwy_scatter_set_data_label(scatter, id, value);
        g_free(key);
    }
    gwy_file_pass_xyz(file, 0, scatter);
    gwy_file_set_title(file, GWY_FILE_XYZ, 0, "XYZ data", FALSE);
    gwy_log_add_import(file, GWY_FILE_XYZ, 0, NULL, filename);

fail:
    g_free(header);
    g_free(buffer);
    if (hash)
        g_hash_table_destroy(hash);
    g_clear_object(&xyunit);
    g_clear_object(&zunit);
    if (zunits) {
        for (i = 0; i < nchan; i++)
            g_clear_object(zunits + i);
        g_free(zunits);
    }

    return file;
}

static gboolean
export_file(G_GNUC_UNUSED GwyFile *data,
            const gchar *filename,
            GwyRunModeFlags mode,
            GError **error)
{
    ModuleArgs args;
    GwyField *dfield;
    GwyScatter *scatter;
    gint fid, sid;
    const gchar *title = NULL;
    GwyDialogOutcome outcome;
    gboolean ok = FALSE;

    gwy_data_browser_get_current(GWY_APP_FIELD, &dfield,
                                 GWY_APP_FIELD_ID, &fid,
                                 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 (dfield && scatter) {
        if (args.data_kind != GWY_FILE_IMAGE)
            dfield = NULL;
        if (args.data_kind != GWY_FILE_XYZ)
            scatter = NULL;
    }
    if (!dfield && !scatter) {
        err_NO_CHANNEL_EXPORT(error);
        return FALSE;
    }

    if (dfield)
        args.data_kind = GWY_FILE_IMAGE;
    if (scatter) {
        if (!gwy_scatter_get_n_values(scatter)) {
            err_NO_CHANNEL_EXPORT(error);
            return FALSE;
        }
        args.data_kind = GWY_FILE_XYZ;
    }

    args.params = gwy_params_new_from_settings(define_module_params());
    title = gwy_file_get_title(data, args.data_kind, dfield ? fid : sid);
    if (!title)
        title = _("Untitled");

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

    if (dfield)
        ok = export_fields(filename, data, fid, &args, error);
    else if (scatter)
        ok = export_xyz(filename, data, sid, error);
    else {
        g_assert_not_reached();
    }

fail:
    g_object_unref(args.params);
    return ok;
}

static GwyDialogOutcome
run_gui_images(ModuleArgs *args, const gchar *title)
{
    GwyDialog *dialog;
    GwyParamTable *table;

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


    table = gwy_param_table_new(args->params);
    // FIXME: We used to also have "XYZ data". However, we no longer need to gather all the channels because they are
    // in a single data object. We may add an option to *extract* a single channel. However, that would probably
    // look different (a combo) and have separate GUI.
    gwy_param_table_append_info(table, INFO_CHANNEL, _("Image"));
    gwy_param_table_info_set_valuestr(table, INFO_CHANNEL, title);
    gwy_param_table_append_header(table, -1, _("Options"));
    gwy_param_table_append_checkbox(table, PARAM_ALL_CHANNELS);
    gwy_dialog_add_content(dialog, gwy_param_table_widget(table), FALSE, FALSE, 0);

    return gwy_dialog_run(dialog);
}

static gboolean
export_fields(const gchar *filename,
              GwyFile *file,
              gint id,
              const ModuleArgs *args,
              GError **error)
{
    gdouble *ddbl = NULL;
    gint *ids = NULL;
    guint nchannels, ci, i, j, k, xres, yres;
    size_t npts;
    GwyField *dfield, *other;
    const gdouble **d;
    gdouble xreal, yreal, xoff, yoff;
    gchar **titles = NULL;
    GwyUnit *xyunit, **zunits = NULL;
    FILE *fh;

    if (!(fh = gwy_fopen(filename, "wb"))) {
        err_OPEN_WRITE(error);
        return FALSE;
    }

    dfield = gwy_file_get_image(file, id);
    g_return_val_if_fail(dfield, FALSE);

    xres = gwy_field_get_xres(dfield);
    yres = gwy_field_get_yres(dfield);
    xreal = gwy_field_get_xreal(dfield);
    yreal = gwy_field_get_yreal(dfield);
    xoff = gwy_field_get_xoffset(dfield);
    yoff = gwy_field_get_yoffset(dfield);
    xyunit = gwy_field_get_unit_xy(dfield);

    if (gwy_params_get_boolean(args->params, PARAM_ALL_CHANNELS))
        ids = gather_compatible_fields(file, dfield, &nchannels);
    else {
        nchannels = 1;
        ids = g_new(gint, 2);
        ids[0] = id;
        ids[1] = -1;
    }
    g_return_val_if_fail(nchannels, FALSE);

    zunits = g_new0(GwyUnit*, nchannels + 1);
    titles = g_new0(gchar*, nchannels + 1);
    d = g_new0(const gdouble*, nchannels + 1);
    for (ci = 0; ci < nchannels; ci++) {
        other = gwy_file_get_image(file, ids[ci]);
        zunits[ci] = gwy_field_get_unit_z(other);
        d[ci] = gwy_field_get_data_const(other);
        titles[ci] = gwy_file_get_display_title(file, GWY_FILE_IMAGE, ids[ci]);
    }

    if (!write_header(fh, nchannels, xres*yres, (const gchar**)titles, xyunit, zunits, xres, yres, error))
        goto fail;

    npts = ((size_t)nchannels + 2)*xres*yres;
    ddbl = g_new(gdouble, npts);
    k = 0;
    for (i = 0; i < yres; i++) {
        for (j = 0; j < xres; j++) {
            append_double(ddbl + k++, (j + 0.5)*xreal/xres + xoff);
            append_double(ddbl + k++, (i + 0.5)*yreal/yres + yoff);
            for (ci = 0; ci < nchannels; ci++)
                append_double(ddbl + k++, *(d[ci]++));
        }
    }

    if (fwrite(ddbl, sizeof(gdouble), npts, fh) != npts) {
        err_WRITE(error);
        goto fail;
    }
    g_free(ddbl);
    fclose(fh);

    return TRUE;

fail:
    if (fh)
        fclose(fh);
    g_unlink(filename);

    if (titles)
        g_strfreev(titles);
    g_free(zunits);
    g_free(d);
    g_free(ddbl);
    g_free(ids);

    return FALSE;
}

static gint*
gather_compatible_fields(GwyFile *file, GwyField *dfield,
                         guint *nchannels)
{
    gint *ids = gwy_file_get_ids(file, GWY_FILE_IMAGE);
    guint n = 0;

    for (guint ci = 0; ids[ci] > -1; ci++) {
        GwyField *other = gwy_file_get_image(file, ids[ci]);
        if (gwy_field_is_incompatible(dfield, other,
                                      GWY_DATA_MISMATCH_RES | GWY_DATA_MISMATCH_REAL | GWY_DATA_MISMATCH_LATERAL))
            continue;
        ids[n] = ids[ci];
        n++;
    }
    ids[n] = -1;

    *nchannels = n;
    return ids;
}

static gboolean
export_xyz(const gchar *filename,
           GwyFile *file,
           gint id,
           GError **error)
{
    FILE *fh = NULL;
    if (!(fh = gwy_fopen(filename, "wb"))) {
        err_OPEN_WRITE(error);
        return FALSE;
    }

    GwyScatter *scatter = gwy_file_get_xyz(file, id);
    g_return_val_if_fail(scatter, FALSE);

    GwyUnit *xyunit = gwy_scatter_get_unit_xy(scatter);
    gsize n = gwy_scatter_get_n_points(scatter);
    gint nchannels = gwy_scatter_get_n_values(scatter);
    g_return_val_if_fail(nchannels, FALSE);

    GwyUnit **zunits = g_new(GwyUnit*, nchannels);
    const gchar **titles = g_new(const gchar*, nchannels);
    for (gint ci = 0; ci < nchannels; ci++) {
        zunits[ci] = gwy_scatter_get_unit_data(scatter, ci);
        titles[ci] = gwy_scatter_get_data_label(scatter, ci);
    }

    if (!write_header(fh, nchannels, n, titles, xyunit, zunits, 0, 0, error))
        goto fail;

    gsize npts = n*(nchannels + 2);
    gdouble *packed = gwy_scatter_pack(scatter, -1);
#if (G_BYTE_ORDER == G_BIG_ENDIAN)
    gdouble *buf = g_new(gdouble, npts);
    gwy_memcpy_byte_swap((const guint8*)packed, (guint8*)buf, sizeof(gdouble), npts, 7);
    GWY_SWAP(gdouble*, packed, buf);
    GWY_FREE(buf);
#endif

    if (fwrite(packed, sizeof(gdouble), npts, fh) != npts) {
        g_free(packed);
        err_WRITE(error);
        goto fail;
    }
    g_free(packed);
    fclose(fh);

    return TRUE;

fail:
    if (fh)
        fclose(fh);
    g_unlink(filename);

    g_free(titles);
    g_free(zunits);

    return FALSE;
}

static gboolean
write_header(FILE *fh, guint nchannels, guint npoints,
             const gchar **titles, GwyUnit *xyunit, GwyUnit **zunits,
             gint xres, gint yres,
             GError **error)
{
    static const gchar zeros[8] = { 0, 0, 0, 0, 0, 0, 0, 0 };
    GString *header;
    gchar *s;
    guint i, padding;

    header = g_string_new(MAGIC);
    g_string_append_printf(header, "NChannels = %u\n", nchannels);
    g_string_append_printf(header, "NPoints = %u\n", npoints);

    if (!gwy_unit_equal_string(xyunit, NULL)) {
        s = gwy_unit_get_string(xyunit, GWY_UNIT_FORMAT_PLAIN);
        g_string_append_printf(header, "XYUnits = %s\n", s);
        g_free(s);
    }

    for (i = 0; i < nchannels; i++) {
        if (!gwy_unit_equal_string(zunits[i], NULL)) {
            s = gwy_unit_get_string(zunits[i], GWY_UNIT_FORMAT_PLAIN);
            g_string_append_printf(header, "ZUnits%u = %s\n", i+1, s);
            g_free(s);
        }
    }

    for (i = 0; i < nchannels; i++)
        g_string_append_printf(header, "Title%u = %s\n", i, titles[i]);

    if (xres && yres) {
        g_string_append_printf(header, "XRes = %u\n", xres);
        g_string_append_printf(header, "YRes = %u\n", yres);
    }

    if (fwrite(header->str, 1, header->len, fh) != header->len) {
        err_WRITE(error);
        g_string_free(header, TRUE);
        return FALSE;
    }

    padding = 8 - (header->len % 8);
    g_string_free(header, TRUE);
    if (fwrite(zeros, 1, padding, fh) != padding) {
        err_WRITE(error);
        return FALSE;
    }

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