/*
 *  $Id: spip-asc.c 29571 2026-03-05 15:49:32Z 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.
 */

/* FIXME: Not sure where these come from, but the files tend to bear `created by SPIP'.  The field names resemble BCR,
 * but the format is not the same.  So let's call the format SPIP ASCII data... */
/**
 * [FILE-MAGIC-FREEDESKTOP]
 * <mime-type type="application/x-spip-asc">
 *   <comment>SPIP ASCII data</comment>
 *   <magic priority="80">
 *     <match type="string" offset="0" value="# File Format = ASCII\r\n"/>
 *   </magic>
 *   <glob pattern="*.asc"/>
 *   <glob pattern="*.ASC"/>
 * </mime-type>
 **/

/**
 * [FILE-MAGIC-FILEMAGIC]
 * # SPIP ASCII data
 * 0 string #\ File\ Format\ =\ ASCII\r\n SPIP ASCII export SPM text data
 **/

/**
 * [FILE-MAGIC-USERGUIDE]
 * SPIP ASCII
 * .asc
 * Read Export
 **/

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

#include "err.h"

#define MAGIC "# File Format = ASCII"
#define MAGIC_SIZE (sizeof(MAGIC)-1)
#define MAGIC2 "# Created by "
#define MAGIC2_SIZE (sizeof(MAGIC2)-1)
#define EXTENSION ".asc"

#define Nanometer (1e-9)

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 *data,
                                const gchar *filename,
                                GwyRunModeFlags mode,
                                GError **error);
static GwyFile* read_image_data(GHashTable *hash,
                                gchar *p,
                                const gchar *filename,
                                GError **error);
static GwyFile* read_graph_data(GHashTable *hash,
                                gchar *buffer,
                                gchar *p,
                                const gchar *filename,
                                GError **error);
static gchar*   format_header  (GwyFile *data,
                                GwyField *field,
                                gboolean *zunit_is_nm);

static GwyModuleInfo module_info = {
    GWY_MODULE_ABI_VERSION,
    &module_register,
    N_("Imports and exports SPIP ASC files."),
    "Yeti <yeti@gwyddion.net>",
    "0.10",
    "David Nečas (Yeti)",
    "2009",
};

GWY_MODULE_QUERY2(module_info, spip_asc)

static gboolean
module_register(void)
{
    gwy_file_func_register("spip-asc",
                           N_("SPIP ASCII files (.asc)"),
                           detect_file, load_file, NULL, export_file);

    return TRUE;
}

static gint
detect_file(const GwyFileDetectInfo *fileinfo, gboolean only_name)
{
    gint seplen;

    if (only_name)
        return g_str_has_suffix(fileinfo->name_lowercase, EXTENSION) ? 10 : 0;

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

    if (fileinfo->head[MAGIC_SIZE] == '\r')
        seplen = (fileinfo->head[MAGIC_SIZE+1] == '\n' ? 2 : 1);
    else if (fileinfo->head[MAGIC_SIZE] == '\n')
        seplen = 1;
    else
        return 0;

    /* Return nonzero score for files with matching first line, but a high score only for files with matching second
     * line. */
    return memcmp(fileinfo->head + MAGIC_SIZE + seplen, MAGIC2, MAGIC2_SIZE) ? 50 : 95;
}

static gboolean
header_error(G_GNUC_UNUSED const GwyTextHeaderContext *context,
             GError *error,
             G_GNUC_UNUSED gpointer user_data)
{
    return error->code == GWY_TEXT_HEADER_ERROR_TERMINATOR;
}

static void
header_end(G_GNUC_UNUSED const GwyTextHeaderContext *context,
           gsize length,
           gpointer user_data)
{
    gchar **pp = (gchar**)user_data;

    *pp += length;
}

static GwyFile*
load_file(const gchar *filename,
          G_GNUC_UNUSED GwyRunModeFlags mode,
          GError **error)
{
    GwyFile *file = NULL;
    GwyTextHeaderParser parser;
    gchar *p, *line, *buffer = NULL;
    GHashTable *hash = NULL;
    gsize size;
    GError *err = NULL;

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

    p = buffer;
    line = gwy_str_next_line(&p);
    if (!gwy_strequal(line, MAGIC)) {
        err_FILE_TYPE(error, "SPIP ASCII data");
        goto fail;
    }

    gwy_clear1(parser);
    parser.line_prefix = "#";
    parser.key_value_separator = "=";
    parser.terminator = "# Start of Data:";
    parser.error = &header_error;
    parser.end = &header_end;
    if (!(hash = gwy_text_header_parse(p, &parser, &p, &err))) {
        g_propagate_error(error, err);
        goto fail;
    }
    if (require_keys(hash, NULL, "x-pixels", "y-pixels", "x-length", "y-length", NULL))
        file = read_image_data(hash, p, filename, error);
    else if (require_keys(hash, NULL, "points", "length", NULL))
        file = read_graph_data(hash, buffer, p, filename, error);
    else
        err_NO_DATA(error);

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

    return file;
}

static GwyFile*
read_image_data(GHashTable *hash, gchar *p, const gchar *filename,
                GError **error)
{
    GwyFile *file = NULL;
    GwyField *field = NULL;
    GwyNield *mask = NULL;
    GError *err = NULL;
    gdouble xreal, yreal, q;
    gchar *value;
    gint xres, yres;

    xres = atoi(g_hash_table_lookup(hash, "x-pixels"));
    yres = atoi(g_hash_table_lookup(hash, "y-pixels"));
    if (err_DIMENSION(error, xres) || err_DIMENSION(error, yres))
        return NULL;

    xreal = Nanometer * g_ascii_strtod(g_hash_table_lookup(hash, "x-length"), NULL);
    yreal = Nanometer * g_ascii_strtod(g_hash_table_lookup(hash, "y-length"), NULL);
    sanitise_real_size(&xreal, "x size");
    sanitise_real_size(&yreal, "y size");

    field = gwy_field_new(xres, yres, xreal, yreal, FALSE);
    gwy_unit_set_from_string(gwy_field_get_unit_xy(field), "m");

    if ((value = g_hash_table_lookup(hash, "z-unit"))) {
        gint power10;
        power10 = gwy_unit_set_from_string(gwy_field_get_unit_z(field), value);
        q = gwy_exp10(power10);
    }
    else if ((value = g_hash_table_lookup(hash, "Bit2nm"))) {
        gwy_unit_set_from_string(gwy_field_get_unit_z(field), "m");
        q = Nanometer * g_ascii_strtod(value, NULL);
    }
    else
        q = 1.0;

    if (!gwy_parse_doubles(p, gwy_field_get_data(field), GWY_PARSE_DOUBLES_FREE_FORM, &yres, &xres, &p, &err)) {
        err_PARSE_DOUBLES(error, &err);
        g_object_unref(field);
        return NULL;
    }
    gwy_field_multiply(field, q);

    if ((value = g_hash_table_lookup(hash, "voidpixels")) && atoi(value)) {
        GwyField *mask_field = gwy_field_new_alike(field, FALSE);
        if (gwy_parse_doubles(p, gwy_field_get_data(mask_field), GWY_PARSE_DOUBLES_FREE_FORM,
                              &yres, &xres, &p, NULL)) {
            mask = gwy_field_new_nield_alike(mask_field);
            gwy_nield_mark_by_threshold(mask, mask_field, 0.5, TRUE);
            mask = gwy_remove_bad_image_data(field, mask);
        }
        else
            g_clear_object(&mask);
        g_object_unref(mask_field);
    }

    file = gwy_file_new_in_construction();
    gwy_file_pass_image(file, 0, field);
    if (mask)
        gwy_file_pass_image_mask(file, 0, mask);

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

    return file;
}

static GwyFile*
read_graph_data(GHashTable *hash, gchar *buffer, gchar *p,
                const gchar *filename,
                GError **error)
{
    GwyFile *file = NULL;
    GwyGraphModel *gmodel;
    GwyGraphCurveModel *gcmodel;
    GRegex *regex;
    GMatchInfo *minfo = NULL;
    GError *err = NULL;
    const guchar *buf2;
    gchar *line, *header, *s;
    GwyUnit *xunit, *yunit;
    gdouble qx, qy;
    gsize size2;
    guint npoints, i;
    GwyXY *xydata;
    gboolean ok;

    npoints = atoi(g_hash_table_lookup(hash, "points"));
    if (err_DIMENSION(error, npoints))
        return NULL;

    /* Unfortunately, the axes are given in the header in some random format, different from other header lines.  So
     * the text header parser discards them and we have to extract them separately. */
    if (!gwy_map_file_contents(filename, &buf2, &size2, &err)) {
        err_GET_FILE_CONTENTS(error, &err);
        return NULL;
    }
    if (size2 < p - buffer) {
        err_TRUNCATED_HEADER(error);
        gwy_unmap_file_contents(buf2);
        return NULL;
    }

    header = g_memdup(buf2, p-buffer + 1);
    header[p-buffer] = '\0';
    gwy_unmap_file_contents(buf2);

    regex = g_regex_new("^#\\s*X-Axis:\\s*(?P<xunit>[^;]*);\\s*Y-Axis:\\s*(?P<yunit>.*?)\\s*$",
                        G_REGEX_MULTILINE | G_REGEX_NO_AUTO_CAPTURE, 0, NULL);
    g_assert(regex);

    ok = g_regex_match(regex, header, 0, &minfo);
    if (ok) {
        gchar *unitstr;
        gint power10;

        unitstr = g_match_info_fetch_named(minfo, "xunit");
        xunit = gwy_unit_new_parse(unitstr, &power10);
        g_free(unitstr);
        qx = gwy_exp10(power10);

        unitstr = g_match_info_fetch_named(minfo, "yunit");
        yunit = gwy_unit_new_parse(unitstr, &power10);
        g_free(unitstr);
        qy = gwy_exp10(power10);
    }
    g_match_info_free(minfo);
    g_regex_unref(regex);
    g_free(header);
    if (!ok) {
        err_MISSING_FIELD(error, "X-Axis");
        return NULL;
    }

    xydata = g_new(GwyXY, npoints);
    i = 0;
    while ((line = gwy_str_next_line(&p)) && i < npoints) {
        gdouble x, y;
        gchar *end;

        g_strstrip(line);
        if (!line[0] || line[0] == '#')
            continue;

        x = g_ascii_strtod(line, &end);
        line = end;
        y = g_ascii_strtod(line, &end);
        if (end == line) {
            g_set_error(error, GWY_MODULE_FILE_ERROR, GWY_MODULE_FILE_ERROR_DATA,
                        _("Malformed data encountered when reading sample #%u"), i);
            g_free(xydata);
            g_object_unref(xunit);
            g_object_unref(yunit);
            return NULL;
        }
        xydata[i].x = qx*x;
        xydata[i].y = qy*y;
        i++;
    }
    if (i < npoints) {
        g_set_error(error, GWY_MODULE_FILE_ERROR, GWY_MODULE_FILE_ERROR_DATA,
                    _("End of file reached when reading sample #%u of %u"), i, npoints);
        g_free(xydata);
        g_object_unref(xunit);
        g_object_unref(yunit);
        return NULL;
    }

    gmodel = gwy_graph_model_new();
    g_object_set(gmodel, "unit-x", xunit, "unit-y", yunit, NULL);
    g_object_unref(xunit);
    g_object_unref(yunit);

    gcmodel = gwy_graph_curve_model_new();
    g_object_set(gcmodel, "mode", GWY_GRAPH_CURVE_LINE, NULL);
    if ((s = g_hash_table_lookup(hash, "description"))) {
        g_object_set(gmodel, "title", s, NULL);
        g_object_set(gcmodel, "description", s, NULL);
    }
    gwy_graph_curve_model_set_data_interleaved(gcmodel, (gdouble*)xydata, npoints);
    g_free(xydata);
    gwy_graph_model_add_curve(gmodel, gcmodel);
    g_object_unref(gcmodel);

    file = gwy_file_new_in_construction();
    gwy_file_pass_graph(file, 0, gmodel);
    gwy_log_add_import(file, GWY_FILE_GRAPH, 0, NULL, filename);

    return file;
}

static gboolean
export_file(GwyFile *file,
            const gchar *filename,
            G_GNUC_UNUSED GwyRunModeFlags mode,
            GError **error)
{
    GwyField *field;
    guint xres, i, n;
    gchar *header;
    const gdouble *d;
    gboolean zunit_is_nm;
    FILE *fh;

    gwy_data_browser_get_current(GWY_APP_FIELD, &field, 0);

    if (!field) {
        err_NO_CHANNEL_EXPORT(error);
        return FALSE;
    }

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

    header = format_header(file, field, &zunit_is_nm);
    if (fputs(header, fh) == EOF)
        goto fail;

    d = gwy_field_get_data_const(field);
    xres = gwy_field_get_xres(field);
    n = xres*gwy_field_get_yres(field);
    for (i = 0; i < n; i++) {
        gchar buf[G_ASCII_DTOSTR_BUF_SIZE];
        gchar c;

        if (zunit_is_nm)
            g_ascii_dtostr(buf, G_ASCII_DTOSTR_BUF_SIZE, d[i]/Nanometer);
        else
            g_ascii_dtostr(buf, G_ASCII_DTOSTR_BUF_SIZE, d[i]);

        if (fputs(buf, fh) == EOF)
            goto fail;

        c = (i % xres == xres-1) ? '\n' : '\t';
        if (fputc(c, fh) == EOF)
            goto fail;
    }

    fclose(fh);
    g_free(header);

    return TRUE;

fail:
    err_WRITE(error);
    fclose(fh);
    g_free(header);
    g_unlink(filename);

    return FALSE;
}

static gchar*
format_header(GwyFile *file, GwyField *field, gboolean *zunit_is_nm)
{
    static const gchar header_template[] =
        "# File Format = ASCII\n"
        "# Created by Gwyddion %s\n"
        "# Original file: %s\n"
        "# x-pixels = %u\n"
        "# y-pixels = %u\n"
        "# x-length = %s\n"
        "# y-length = %s\n"
        "# x-offset = %s\n"
        "# y-offset = %s\n"
        "# Bit2nm = 1.0\n"
        "%s"
        "# Start of Data:\n";

    GwyUnit *zunit;
    gchar *header, *zunit_str, *zunit_line;
    gchar xreal_str[G_ASCII_DTOSTR_BUF_SIZE], yreal_str[G_ASCII_DTOSTR_BUF_SIZE],
          xoff_str[G_ASCII_DTOSTR_BUF_SIZE], yoff_str[G_ASCII_DTOSTR_BUF_SIZE];
    const gchar *filename = "NONE";
    gdouble xreal, yreal, xoff, yoff;

    /* XXX: Gwyddion can have lateral dimensions as whatever we want.  But who knows about the SPIP ASC format... */
    xreal = gwy_field_get_xreal(field)/Nanometer;
    yreal = gwy_field_get_yreal(field)/Nanometer;
    xoff = gwy_field_get_xoffset(field)/Nanometer;
    yoff = gwy_field_get_yoffset(field)/Nanometer;
    zunit = gwy_field_get_unit_z(field);

    g_ascii_dtostr(xreal_str, G_ASCII_DTOSTR_BUF_SIZE, xreal);
    g_ascii_dtostr(yreal_str, G_ASCII_DTOSTR_BUF_SIZE, yreal);
    g_ascii_dtostr(xoff_str, G_ASCII_DTOSTR_BUF_SIZE, xoff);
    g_ascii_dtostr(yoff_str, G_ASCII_DTOSTR_BUF_SIZE, yoff);
    zunit_str = gwy_unit_get_string(zunit, GWY_UNIT_FORMAT_PLAIN);
    if ((*zunit_is_nm = gwy_strequal(zunit_str, "m")))
        zunit_line = g_strdup("");
    else
        zunit_line = g_strdup_printf("# z-unit = %s\n", zunit_str);

    gwy_dict_gis_string(GWY_DICT(file), gwy_file_key_filename(), &filename);

    header = g_strdup_printf(header_template,
                             gwy_version_string(), filename,
                             gwy_field_get_xres(field), gwy_field_get_yres(field),
                             xreal_str, yreal_str, xoff_str, yoff_str, zunit_line);

    g_free(zunit_str);
    g_free(zunit_line);

    return header;
}

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