/*
 *  $Id: ezdfile.c 29611 2026-03-17 10:31:06Z yeti-dn $
 *  Copyright (C) 2005-2025 David Necas (Yeti), Petr Klapetek.
 *  E-mail: yeti@gwyddion.net, klapetek@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.
 */

/**
 * [FILE-MAGIC-FREEDESKTOP]
 * <mime-type type="application/x-nanosurf-spm">
 *   <comment>Nanosurf SPM data</comment>
 *   <magic priority="80">
 *     <match type="string" offset="0" value="[DataSet]\r\n"/>
 *   </magic>
 *   <glob pattern="*.ezd"/>
 *   <glob pattern="*.EZD"/>
 *   <glob pattern="*.nid"/>
 *   <glob pattern="*.NID"/>
 * </mime-type>
 **/

/**
 * [FILE-MAGIC-FILEMAGIC]
 * # Nanosurf EZD.
 * 0 string [DataSet]\x0d\x0a
 * >&0 string Version=
 * >>&0 regex [0-9]+ Nanosurf EZD SPM data version %s
 **/

/**
 * [FILE-MAGIC-USERGUIDE]
 * Nanosurf EZD, NID
 * .ezd, .nid
 * Read
 **/

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

#include "err.h"
#include "get.h"

#define MAGIC "[DataSet]\r\n"
#define MAGIC_SIZE (sizeof(MAGIC) - 1)

#define DATA_MAGIC "#!"
#define DATA_MAGIC_SIZE (sizeof(DATA_MAGIC) - 1)

#define EXTENSION1 ".ezd"
#define EXTENSION2 ".nid"

typedef enum {
    SCAN_UNKNOWN = 0,
    SCAN_FORWARD = 1,
    SCAN_BACKWARD = -1
} ScanDirection;

typedef struct {
    gchar *name;
    gchar *unit;
    gdouble min;
    gdouble range;
} EZDRange;

typedef struct {
    gchar *name;
    GHashTable *meta;
    /* following fields are meaningful only for data */
    ScanDirection direction;
    gint group;
    gint channel;
    gint xres;
    gint yres;
    EZDRange xrange;
    EZDRange yrange;
    EZDRange zrange;
    guint bitdepth;
    guint byteorder;
    gboolean sign;
    const gchar *data;
    GError *firsterror;
} EZDSection;

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 guint     find_data_start     (const guchar *buffer,
                                      gsize size);
static void      ezdfile_free        (GPtrArray *ezdfile);
static void      ezdsection_free     (EZDSection *section);
static GwyField* read_field          (EZDSection *section,
                                      GError **error);
static gboolean  file_read_header    (GPtrArray *ezdfile,
                                      gchar *buffer,
                                      GError **error);
static guint     find_data_offsets   (const gchar *buffer,
                                      gsize size,
                                      GPtrArray *ezdfile,
                                      GError **error);
static GwyDict*  get_metadata        (GPtrArray *ezdfile,
                                      gint idx);
static void      fix_scales          (EZDSection *section,
                                      gint idx,
                                      GwyFile *file);
static void      check_section_ranges(gpointer data,
                                      gpointer user_data);

static GwyModuleInfo module_info = {
    GWY_MODULE_ABI_VERSION,
    &module_register,
    N_("Imports Nanosurf EZD and NID data files."),
    "Yeti <yeti@gwyddion.net>",
    "0.10",
    "David Nečas (Yeti) & Petr Klapetek",
    "2005",
};

GWY_MODULE_QUERY2(module_info, ezdfile)

static gboolean
module_register(void)
{
    gwy_file_func_register("ezdfile",
                           N_("Nanosurf files (.ezd, .nid)"),
                           detect_file, load_file, NULL, NULL);

    return TRUE;
}

static gint
detect_file(const GwyFileDetectInfo *fileinfo, gboolean only_name)
{
    gint score = 0;

    if (only_name)
        return (g_str_has_suffix(fileinfo->name_lowercase, EXTENSION1)
                || g_str_has_suffix(fileinfo->name_lowercase, EXTENSION2))
                ? 20 : 0;

    if (fileinfo->buffer_len > MAGIC_SIZE && !memcmp(fileinfo->head, MAGIC, MAGIC_SIZE))
        score = 100;

    return score;
}

static GwyFile*
load_file(const gchar *filename,
          G_GNUC_UNUSED GwyRunModeFlags mode,
          GError **error)
{
    GwyFile *file = NULL;
    GwyDict *meta;
    const guchar *buffer = NULL;
    gsize size = 0;
    GError *err = NULL;
    EZDSection *section = NULL;
    GwyField *dfield = NULL;
    GPtrArray *ezdfile;
    guint header_size, n;
    gint i;
    gchar *p;

    if (!gwy_map_file_contents(filename, &buffer, &size, &err)) {
        err_GET_FILE_CONTENTS(error, &err);
        return NULL;
    }
    if (strncmp(buffer, MAGIC, MAGIC_SIZE) || !(header_size = find_data_start(buffer, size))) {
        err_FILE_TYPE(error, "EZD/NID");
        gwy_unmap_file_contents(buffer);
        return NULL;
    }

    ezdfile = g_ptr_array_new();
    p = g_strndup(buffer, header_size - DATA_MAGIC_SIZE);
    if (!file_read_header(ezdfile, p, error)) {
        gwy_unmap_file_contents(buffer);
        g_free(p);
        return NULL;
    }
    g_free(p);

    n = find_data_offsets(buffer + header_size, size - header_size, ezdfile, error);
    if (!n) {
        gwy_unmap_file_contents(buffer);
        return NULL;
    }

    /* fix for ranges equal to zero */
    g_ptr_array_foreach(ezdfile, &check_section_ranges, NULL);

    file = gwy_file_new_in_construction();
    i = 0;
    for (n = 0; n < ezdfile->len; n++) {
        section = (EZDSection*)g_ptr_array_index(ezdfile, n);
        if (!section->data)
            continue;
        if (err_DIMENSION(NULL, section->xres) || err_DIMENSION(NULL, section->yres))
            continue;
        if (!(dfield = read_field(section, NULL)))
            continue;
        gwy_file_pass_image(file, i, dfield);
        fix_scales(section, i, file);

        meta = get_metadata(ezdfile, n);
        if (meta)
            gwy_file_pass_meta(file, GWY_FILE_IMAGE, i, meta);

        gwy_log_add_import(file, GWY_FILE_IMAGE, i, NULL, filename);
        i++;
    }
    gwy_unmap_file_contents(buffer);
    ezdfile_free(ezdfile);

    return file;
}

static guint
find_data_start(const guchar *buffer,
                gsize size)
{
    const guchar *p;

    p = gwy_memmem(buffer, size, DATA_MAGIC, DATA_MAGIC_SIZE);
    return p ? (p - buffer) + DATA_MAGIC_SIZE : 0;
}

static void
ezdfile_free(GPtrArray *ezdfile)
{
    EZDSection *section;
    guint i;

    for (i = 0; i < ezdfile->len; i++) {
        section = (EZDSection*)g_ptr_array_index(ezdfile, i);
        ezdsection_free(section);
    }
    g_ptr_array_free(ezdfile, TRUE);
}

static void
ezdsection_free(EZDSection *section)
{
    g_hash_table_destroy(section->meta);
    g_clear_error(&section->firsterror);
    g_free(section->name);
    g_free(section->xrange.unit);
    g_free(section->yrange.unit);
    g_free(section->zrange.unit);
    g_free(section->xrange.name);
    g_free(section->yrange.name);
    g_free(section->zrange.name);
    g_free(section);
}

static gboolean
file_read_header(GPtrArray *ezdfile,
                 gchar *buffer,
                 GError **error)
{
    EZDSection *section = NULL;
    gchar *p, *line;
    guint len;

    while ((line = gwy_str_next_line(&buffer))) {
        line = g_strstrip(line);
        if (!(len = strlen(line)))
            continue;
        if (line[0] == '[' && line[len-1] == ']') {
            section = g_new0(EZDSection, 1);
            g_ptr_array_add(ezdfile, section);
            line[len-1] = '\0';
            section->name = g_strdup(line + 1);
            section->meta = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
            gwy_debug("Section <%s>", section->name);
            continue;
        }
        if (!section) {
            g_set_error(error, GWY_MODULE_FILE_ERROR, GWY_MODULE_FILE_ERROR_DATA,
                        _("Garbage before first header section."));
            return FALSE;
        }
        /* Skip comments */
        if (g_str_has_prefix(line, "--"))
            continue;

        p = strchr(line, '=');
        if (!p) {
            g_set_error(error, GWY_MODULE_FILE_ERROR, GWY_MODULE_FILE_ERROR_DATA,
                        _("Malformed header line (missing =)."));
            return FALSE;
        }
        *p = '\0';
        p++;

        if (gwy_strequal(line, "SaveMode")) {
            if (!gwy_strequal(p, "Binary")) {
                if (!section->firsterror)
                    err_UNSUPPORTED(&section->firsterror, "SaveMode");
            }
        }
        else if (gwy_strequal(line, "SaveBits")) {
            section->bitdepth = atol(p);
            if (section->bitdepth != 8
                && section->bitdepth != 16
                && section->bitdepth != 32) {
                if (!section->firsterror)
                    err_BPP(&section->firsterror, section->bitdepth);
            }
        }
        else if (gwy_strequal(line, "SaveSign")) {
            section->sign = gwy_strequal(p, "Signed");
            if (!section->sign) {
                if (!section->firsterror)
                    err_UNSUPPORTED(&section->firsterror, "SaveSign");
            }
        }
        else if (gwy_strequal(line, "SaveOrder")) {
            if (gwy_strequal(p, "Intel"))
                section->byteorder = G_LITTLE_ENDIAN;
            else {
                if (!section->firsterror)
                    err_UNSUPPORTED(&section->firsterror, "SaveOrder");
            }
        }
        else if (gwy_strequal(line, "Frame")) {
            if (gwy_strequal(p, "Scan forward"))
                section->direction = SCAN_FORWARD;
            else if (gwy_strequal(p, "Scan backward"))
                section->direction = SCAN_BACKWARD;
        }
        else if (gwy_strequal(line, "Points"))
            section->xres = atol(p);
        else if (gwy_strequal(line, "Lines"))
            section->yres = atol(p);
        /* FIXME: this is ugly, and incorrect for non-2D data */
        else if (gwy_strequal(line, "Dim0Name"))
            section->xrange.name = g_strdup(p);
        else if (gwy_strequal(line, "Dim1Name"))
            section->yrange.name = g_strdup(p);
        else if (gwy_strequal(line, "Dim2Name"))
            section->zrange.name = g_strdup(p);
        else if (gwy_strequal(line, "Dim0Unit"))
            section->xrange.unit = g_strdup(p);
        else if (gwy_strequal(line, "Dim1Unit"))
            section->yrange.unit = g_strdup(p);
        else if (gwy_strequal(line, "Dim2Unit"))
            section->zrange.unit = g_strdup(p);
        else if (gwy_strequal(line, "Dim0Min"))
            section->xrange.min = g_ascii_strtod(p, NULL);
        else if (gwy_strequal(line, "Dim1Min"))
            section->yrange.min = g_ascii_strtod(p, NULL);
        else if (gwy_strequal(line, "Dim2Min"))
            section->zrange.min = g_ascii_strtod(p, NULL);
        else if (gwy_strequal(line, "Dim0Range"))
            section->xrange.range = g_ascii_strtod(p, NULL);
        else if (gwy_strequal(line, "Dim1Range"))
            section->yrange.range = g_ascii_strtod(p, NULL);
        else if (gwy_strequal(line, "Dim2Range"))
            section->zrange.range = g_ascii_strtod(p, NULL);
        else
            g_hash_table_replace(section->meta, g_strdup(line), g_strdup(p));
    }

    return TRUE;
}

static guint
find_data_offsets(const gchar *buffer,
                  gsize size,
                  GPtrArray *ezdfile,
                  GError **error)
{
    EZDSection *dataset, *section, *firstchannel = NULL;
    GString *grkey;
    guint required_size = 0;
    gint ngroups, nchannels, i, j, k;
    guint ndata = 0;
    gchar *p;

    /* Sanity check */
    if (!ezdfile->len) {
        err_NO_DATA(error);
        return 0;
    }
    dataset = (EZDSection*)g_ptr_array_index(ezdfile, 0);
    if (strcmp(dataset->name, "DataSet")) {
        g_set_error(error, GWY_MODULE_FILE_ERROR, GWY_MODULE_FILE_ERROR_DATA,
                    _("First section isn't DataSet"));
        return 0;
    }

    if (!(p = g_hash_table_lookup(dataset->meta, "GroupCount")) || (ngroups = atol(p)) <= 0) {
        err_INVALID(error, _("GroupCount in [DataSet]"));
        return 0;
    }

    /* Scan groups */
    grkey = g_string_new(NULL);
    for (i = 0; i < ngroups; i++) {
        g_string_printf(grkey, "Gr%d-Count", i);
        if (!(p = g_hash_table_lookup(dataset->meta, grkey->str))) {
            g_warning("No count for group %u", i);
            continue;
        }

        if ((nchannels = atol(p)) <= 0)
            continue;

        /* Scan channels inside a group, note it's OK there's less channels than specified */
        for (j = 0; j < nchannels; j++) {
            g_string_printf(grkey, "Gr%d-Ch%d", i, j);
            if (!(p = g_hash_table_lookup(dataset->meta, grkey->str)))
                continue;

            section = NULL;
            for (k = 1; k < ezdfile->len; k++) {
                section = (EZDSection*)g_ptr_array_index(ezdfile, k);
                if (gwy_strequal(section->name, p))
                    break;
            }
            if (!section) {
                g_warning("Cannot find section for %s", p);
                continue;
            }
            if (!firstchannel)
                firstchannel = section;

            /* Compute data position */
            gwy_debug("Data %s at offset %u from data start", grkey->str, required_size);
            gwy_debug("xres = %d, yres = %d, bpp = %d, z-name = %s",
                      section->xres, section->yres, section->bitdepth, section->zrange.name);
            if (section->yres < 2) {
                gwy_debug("Skipping 1D data Gr%d-Ch%d. FIXME.", i, j);
                continue;
            }
            if (section->firsterror) {
                gwy_debug("Skipping data Gr%d-Ch%d due to previous errors.", i, j);
                continue;
            }
            ndata++;
            section->data = buffer + required_size;
            required_size += section->xres * section->yres * (section->bitdepth/8);
            if (required_size > size) {
                g_warning("Truncated file, %s doesn't fit", grkey->str);
                g_string_free(grkey, TRUE);
                section->data = NULL;

                return 0;
            }
            section->group = i;
            section->channel = j;
        }
    }
    g_string_free(grkey, TRUE);

    if (!ndata) {
        if (firstchannel)
            g_propagate_error(error, firstchannel->firsterror);
        else
            err_NO_DATA(error);
    }

    return ndata;
}

static void
fix_scales(EZDSection *section,
           gint idx,
           GwyFile *file)
{
    GwyField *dfield = gwy_file_get_image(file, idx);
    gint power10;

    /* Fix value scale */
    power10 = gwy_unit_set_from_string(gwy_field_get_unit_z(dfield), section->zrange.unit);
    gdouble r = gwy_exp10(power10);
    gwy_field_multiply(dfield, r*section->zrange.range);
    gwy_field_add(dfield, r*section->zrange.min);

    /* Fix lateral scale */
    power10 = gwy_unit_set_from_string(gwy_field_get_unit_xy(dfield), section->xrange.unit);
    gwy_field_set_xreal(dfield, gwy_exp10(power10)*section->xrange.range);

    GwyUnit *unit = gwy_unit_new_parse(section->yrange.unit, &power10);
    gwy_field_set_yreal(dfield, gwy_exp10(power10)*section->yrange.range);
    g_object_unref(unit);

    /* Some metadata */
    if (section->zrange.name) {
        const gchar *s;

        switch (section->direction) {
            case SCAN_FORWARD:
            s = " forward";
            break;

            case SCAN_BACKWARD:
            s = " backward";
            break;

            default:
            s = "";
            break;
        }
        gwy_file_pass_title(file, GWY_FILE_IMAGE, idx, g_strdup_printf("%s%s", section->zrange.name, s));
    }
}

static void
store_meta(gpointer key, gpointer value, gpointer user_data)
{
    GwyDict *meta = (GwyDict*)user_data;
    gchar *cval;

    if (!(cval = gwy_convert_to_utf8(value, -1, "ISO-8859-1")))
        return;
    gwy_dict_pass_string_by_name(meta, key, cval);
}

static GwyDict*
get_metadata(GPtrArray *ezdfile,
             G_GNUC_UNUSED gint idx)
{
    EZDSection *section;
    GwyDict *meta;
    guint i;

    meta = gwy_dict_new_in_construction();

    for (i = 0; i < ezdfile->len; i++) {
        section = (EZDSection*)g_ptr_array_index(ezdfile, i);
        if (gwy_strequal(section->name, "DataSet-Info"))
            g_hash_table_foreach(section->meta, store_meta, meta);
    }

    if (!gwy_dict_get_n_items(meta)) {
        g_object_unref(meta);
        meta = NULL;
    }

    return meta;
}

static GwyField*
read_field(EZDSection *section,
           GError **error)
{
    GwyRawDataType rawtype;
    GwyField *field;
    gdouble q, z0;

    if (section->bitdepth == 8)
        rawtype = GWY_RAW_DATA_SINT8;
    else if (section->bitdepth == 16)
        rawtype = GWY_RAW_DATA_SINT16;
    else if (section->bitdepth == 32)
        rawtype = GWY_RAW_DATA_SINT32;
    else {
        err_BPP(error, section->bitdepth);
        return NULL;
    }

    g_assert(section->data);
    field = gwy_field_new(section->xres, section->yres, 1.0, 1.0, FALSE);
    q = gwy_powi(2.0, section->bitdepth);
    z0 = q/2.0;
    gwy_convert_raw_data(section->data, section->xres*section->yres, 1, rawtype, GWY_BYTE_ORDER_LITTLE_ENDIAN,
                         gwy_field_get_data(field), 1.0/q, z0/q);
    gwy_field_flip(field, FALSE, TRUE);

    return field;
}

static void
check_section_ranges(gpointer data, G_GNUC_UNUSED gpointer user_data)
{
    EZDSection *section = (EZDSection*) data;

    if (section->data) {
        gwy_debug("Check ranges");
        sanitise_real_size(&section->xrange.range, "x size");
        sanitise_real_size(&section->yrange.range, "y size");
        sanitise_real_size(&section->zrange.range, "z size");
    }
}

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