/*
 *  $Id: gsffile.c 29611 2026-03-17 10:31:06Z yeti-dn $
 *  Copyright (C) 2009-2025 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-gsf-spm">
 *   <comment>Gwyddion Simple Field data</comment>
 *   <magic priority="80">
 *     <match type="string" offset="0" value="Gwyddion Simple Field 1.0\n"/>
 *   </magic>
 *   <glob pattern="*.gsf"/>
 *   <glob pattern="*.GSF"/>
 * </mime-type>
 **/

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

/**
 * [FILE-MAGIC-USERGUIDE]
 * Gwyddion Simple Field
 * .gsf
 * 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 Simple Field 1.0\n"
#define MAGIC_SIZE (sizeof(MAGIC)-1)
#define EXTENSION ".gsf"

static gboolean module_register (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 guint    read_pixel_size (GHashTable *hash,
                                 const gchar *key,
                                 GError **error);
static void     add_meta        (gpointer hkey,
                                 gpointer hvalue,
                                 gpointer user_data);
static gdouble  read_real_size  (GHashTable *hash,
                                 const gchar *key);
static gdouble  read_real_offset(GHashTable *hash,
                                 const gchar *key);
static void     append_num      (GString *str,
                                 const gchar *key,
                                 gdouble d);

static GwyModuleInfo module_info = {
    GWY_MODULE_ABI_VERSION,
    &module_register,
    N_("Reads and exports Gwyddion Simple Field files."),
    "Yeti <yeti@gwyddion.net>",
    "2.0",
    "David Nečas (Yeti)",
    "2010",
};

GWY_MODULE_QUERY2(module_info, gsffile)

static gboolean
module_register(void)
{
    gwy_file_func_register("gsffile",
                           N_("Gwyddion Simple Field (.gsf)"),
                           detect_file, load_file, NULL, export_file);

    return TRUE;
}

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

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

    return 100;
}

static GwyFile*
load_file(const gchar *filename,
          G_GNUC_UNUSED GwyRunModeFlags mode,
          GError **error)
{
    GwyFile *file = NULL;
    GwyDict *meta = NULL;
    GwyField *dfield = NULL;
    GwyNield *mask;
    GwyTextHeaderParser parser;
    const guchar *p, *datap, *buffer = NULL;
    gchar *value, *header = NULL;
    GHashTable *hash = NULL;
    gsize size, expected_size;
    GError *err = NULL;
    gdouble xreal, yreal, xoff, yoff;
    guint xres, yres;

    if (!gwy_map_file_contents(filename, &buffer, &size, &err)) {
        err_GET_FILE_CONTENTS(error, &err);
        return NULL;
    }

    if (size < MAGIC_SIZE || memcmp(buffer, MAGIC, MAGIC_SIZE) != 0) {
        err_FILE_TYPE(error, "Gwyddion Simple 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 += 4 - ((datap - buffer) % 4);

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

    xres = read_pixel_size(hash, "XRes", error);
    yres = read_pixel_size(hash, "YRes", error);
    if (!xres || !yres)
        goto fail;

    expected_size = (datap - buffer) + sizeof(gfloat)*xres*yres;
    if (err_SIZE_MISMATCH(error, expected_size, size, TRUE))
        goto fail;

    xreal = read_real_size(hash, "XReal");
    yreal = read_real_size(hash, "YReal");
    dfield = gwy_field_new(xres, yres, xreal, yreal, FALSE);

    xoff = read_real_offset(hash, "XOffset");
    yoff = read_real_offset(hash, "YOffset");
    gwy_field_set_xoffset(dfield, xoff);
    gwy_field_set_yoffset(dfield, yoff);

    gwy_unit_set_from_string(gwy_field_get_unit_xy(dfield), g_hash_table_lookup(hash, "XYUnits"));
    gwy_unit_set_from_string(gwy_field_get_unit_z(dfield), g_hash_table_lookup(hash, "ZUnits"));

    gwy_convert_raw_data(datap, xres*yres, 1, GWY_RAW_DATA_FLOAT, GWY_BYTE_ORDER_LITTLE_ENDIAN,
                         gwy_field_get_data(dfield), 1.0, 0.0);

    file = gwy_file_new_in_construction();
    gwy_file_pass_image(file, 0, dfield);
    if ((mask = gwy_mask_nans_in_field(dfield, TRUE)))
        gwy_file_pass_image_mask(file, 0, mask);

    if ((value = g_hash_table_lookup(hash, "Title")) && g_utf8_validate(value, -1, NULL))
        gwy_file_set_title(file, GWY_FILE_IMAGE, 0, value, FALSE);
    else
        gwy_image_title_fall_back(file, 0);

    meta = gwy_dict_new_in_construction();
    g_hash_table_foreach(hash, add_meta, meta);
    if (gwy_dict_get_n_items(meta))
        gwy_file_pass_meta(file, GWY_FILE_IMAGE, 0, meta);
    else
        g_object_unref(meta);

    gwy_log_add_import(file, GWY_FILE_IMAGE, 0, NULL, filename);

fail:
    gwy_unmap_file_contents(buffer);
    if (header)
        g_free(header);
    if (hash)
        g_hash_table_destroy(hash);

    return file;
}

static void
add_meta(gpointer hkey, gpointer hvalue, gpointer user_data)
{
    if (gwy_stramong((gchar*)hkey,
                     "XRes", "YRes", "XReal", "YReal", "XOffset", "YOffset", "XYUnits", "ZUnits", "Title",
                     NULL))
        return;

    gwy_dict_set_string_by_name(GWY_DICT(user_data), (gchar *)hkey, (gchar *)hvalue);
}

static guint
read_pixel_size(GHashTable *hash,
                const gchar *key,
                GError **error)
{
    gchar *value;
    guint size;

    if (!(value = g_hash_table_lookup(hash, key))) {
        err_MISSING_FIELD(error, key);
        return 0;
    }
    size = atoi(g_hash_table_lookup(hash, key));
    if (err_DIMENSION(error, size))
       return 0;

    return size;
}

static gdouble
read_real_size(GHashTable *hash,
               const gchar *key)
{
    gchar *value;
    gdouble dim = 1.0;

    if ((value = g_hash_table_lookup(hash, key))) {
        dim = g_ascii_strtod(value, NULL);
        sanitise_real_size(&dim, key);
    }
    return dim;
}

static gdouble
read_real_offset(GHashTable *hash,
                 const gchar *key)
{
    gchar *value;
    gdouble off = 0.0;

    if ((value = g_hash_table_lookup(hash, key))) {
        off = g_ascii_strtod(value, NULL);
        /* Catch NaNs */
        if (gwy_isnan(off) || gwy_isinf(off)) {
            g_warning("%s is NaN, fixing to 0.0", key);
            off = 0.0;
        }
    }
    return off;
}

static gboolean
export_file(GwyFile *file,
            const gchar *filename,
            G_GNUC_UNUSED GwyRunModeFlags mode,
            GError **error)
{
    static const gchar zeroes[4] = { 0, 0, 0, 0 };
    GString *header = NULL;
    gfloat *dfl = NULL;
    guint i, xres, yres, padding;
    gint id;
    GwyField *dfield;
    const gdouble *d;
    gdouble v;
    gchar *s;
    GwyUnit *unit;
    FILE *fh;

    gwy_data_browser_get_current(GWY_APP_FIELD, &dfield, GWY_APP_FIELD_ID, &id, 0);
    if (!dfield) {
        err_NO_CHANNEL_EXPORT(error);
        return FALSE;
    }

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

    xres = gwy_field_get_xres(dfield);
    yres = gwy_field_get_yres(dfield);

    header = g_string_new(MAGIC);
    g_string_append_printf(header, "XRes = %u\n", xres);
    g_string_append_printf(header, "YRes = %u\n", yres);
    append_num(header, "XReal", gwy_field_get_xreal(dfield));
    append_num(header, "YReal", gwy_field_get_yreal(dfield));
    if ((v = gwy_field_get_xoffset(dfield)))
        append_num(header, "XOffset", v);
    if ((v = gwy_field_get_yoffset(dfield)))
        append_num(header, "YOffset", v);

    unit = gwy_field_get_unit_xy(dfield);
    if (!gwy_unit_equal_string(unit, NULL)) {
        s = gwy_unit_get_string(unit, GWY_UNIT_FORMAT_PLAIN);
        g_string_append_printf(header, "XYUnits = %s\n", s);
        g_free(s);
    }
    unit = gwy_field_get_unit_z(dfield);
    if (!gwy_unit_equal_string(unit, NULL)) {
        s = gwy_unit_get_string(unit, GWY_UNIT_FORMAT_PLAIN);
        g_string_append_printf(header, "ZUnits = %s\n", s);
        g_free(s);
    }

    s = gwy_file_get_display_title(file, GWY_FILE_IMAGE, id);
    g_string_append_printf(header, "Title = %s\n", s);
    g_free(s);

    if (fwrite(header->str, 1, header->len, fh) != header->len) {
        err_WRITE(error);
        goto fail;
    }

    padding = 4 - (header->len % 4);
    if (fwrite(zeroes, 1, padding, fh) != padding) {
        err_WRITE(error);
        goto fail;
    }
    g_string_free(header, TRUE);
    header = NULL;

    dfl = g_new(gfloat, xres*yres);
    d = gwy_field_get_data_const(dfield);
    for (i = 0; i < xres*yres; i++) {
        union { guchar pp[4]; float f; } z;
        z.f = d[i];
#if (G_BYTE_ORDER == G_BIG_ENDIAN)
        GWY_SWAP(guchar, z.pp[0], z.pp[3]);
        GWY_SWAP(guchar, z.pp[1], z.pp[2]);
#endif
        dfl[i] = z.f;
    }

    if (fwrite(dfl, sizeof(gfloat), xres*yres, fh) != xres*yres) {
        err_WRITE(error);
        goto fail;
    }
    g_free(dfl);
    fclose(fh);

    return TRUE;

fail:
    if (fh)
        fclose(fh);
    g_unlink(filename);
    if (header)
        g_string_free(header, TRUE);
    g_free(dfl);

    return FALSE;
}

static void
append_num(GString *str,
           const gchar *key,
           gdouble d)
{
    gchar buf[32];

    g_string_append(str, key);
    g_string_append(str, " = ");
    g_ascii_formatd(buf, sizeof(buf), "%.14g", d);
    g_string_append(str, buf);
    g_string_append_c(str, '\n');
}

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