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

/*
 * TODO:
 * - skip Comments parts
 * - read split files (and accept any kind of EOL in magic)
 */

/**
 * [FILE-MAGIC-FREEDESKTOP]
 * <mime-type type="application/x-stp-spm">
 *   <comment>Molecular Imaging STP SPM data</comment>
 *   <magic priority="80">
 *     <match type="string" offset="0" value="UK SOFT\r\n"/>
 *   </magic>
 *   <glob pattern="*.stp"/>
 *   <glob pattern="*.STP"/>
 * </mime-type>
 **/

/**
 * [FILE-MAGIC-FILEMAGIC]
 * # Molecular Imaging old STP format
 * 0 string UK\ SOFT\x0d\x0a Molecular Imaging STP image SPM data
 **/

/**
 * [FILE-MAGIC-USERGUIDE]
 * Molecular Imaging STP
 * .stp
 * Read
 **/

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

#include "err.h"

#define MAGIC "UK SOFT\r\n"
#define MAGIC_SIZE (sizeof(MAGIC) - 1)

#define DATA_MAGIC "Data_section  \r\n"
#define DATA_MAGIC_SIZE (sizeof(DATA_MAGIC) - 1)

#define EXTENSION ".stp"

#define KEY_LEN 14

#define Angstrom (1e-10)

typedef struct {
    gint id;
    gint xres;
    gint yres;
    const guint16 *data;
    GHashTable *meta;
} STPData;

typedef struct {
    guint n;
    STPData *buffers;
    GHashTable *meta;
} STPFile;

typedef struct {
    const gchar *key;
    const gchar *meta;
    const gchar *format;
} MetaDataFormat;

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     free_stpfile    (STPFile *stpfile);
static void     read_field      (GwyField *dfield,
                                 STPData *stpdata);
static guint    file_read_header(STPFile *stpfile,
                                 gchar *buffer);
static void     process_metadata(STPFile *stpfile,
                                 guint id,
                                 GwyFile *file);

static const GwyEnum channels[] = {
    { N_("Topography"),                         1,  },
    { N_("Current or Deflection or Amplitude"), 2,  },
    { N_("Electrochemical Voltage (Vec)"),      8,  },
    { N_("Topography and SPS"),                 9,  },
    { N_("Topography and SPS (scripted)"),      10, },
    { N_("Electrochemical Current (Iec)"),      12, },
    { N_("Friction or Phase"),                  13, },
    { N_("AUX in BNC"),                         14, },
    { N_("AUX in 3 (Sum)"),                     16, },
    { N_("AUX in 4 (Raw Deflection)"),          17, },
    { N_("AUX in 5 (PicoPlus Aux BNC)"),        18, },
    { N_("AUX in 6 (Surface Potential)"),       19, },
    { N_("AUX in 7"),                           20, },
    { N_("AUX in 8"),                           21, },
    { N_("AUX in 9"),                           22, },
    { N_("AUX in 10"),                          23, },
    { N_("Topography and Flexgrid"),            24, },
    { N_("External"),                           99, },
};

static GwyModuleInfo module_info = {
    GWY_MODULE_ABI_VERSION,
    &module_register,
    N_("Imports Molecular Imaging STP data files."),
    "Yeti <yeti@gwyddion.net>",
    "0.6",
    "David Nečas (Yeti), Petr Klapetek, Chris Anderson",
    "2006",
};

GWY_MODULE_QUERY2(module_info, stpfile)

static gboolean
module_register(void)
{
    gwy_file_func_register("stpfile",
                           N_("STP files (.stp)"),
                           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, EXTENSION) ? 10 : 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)
{
    STPFile *stpfile;
    GwyFile *file = NULL;
    const guchar *buffer = NULL;
    gsize size = 0;
    GError *err = NULL;
    GwyField *dfield = NULL;
    guint header_size;
    gchar *p;
    gboolean ok;
    guint i = 0, pos, expected;

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

    stpfile = g_new0(STPFile, 1);
    p = g_strndup(buffer, header_size);
    ok = file_read_header(stpfile, p);
    g_free(p);

    if (ok) {
        expected = header_size;
        for (i = 0; i < stpfile->n; i++) {
            if (err_DIMENSION(error, stpfile->buffers[i].xres) || err_DIMENSION(error, stpfile->buffers[i].yres)) {
                ok = FALSE;
                break;
            }
            expected += 2*stpfile->buffers[i].xres * stpfile->buffers[i].yres;
        }
    }
    if (!ok || err_SIZE_MISMATCH(error, expected, size, FALSE)) {
        gwy_unmap_file_contents(buffer);
        g_free(stpfile);
        return NULL;
    }

    if (ok) {
        pos = header_size;
        for (i = 0; i < stpfile->n; i++) {
            stpfile->buffers[i].data = (const guint16*)(buffer + pos);
            pos += 2*stpfile->buffers[i].xres * stpfile->buffers[i].yres;

            dfield = gwy_field_new(stpfile->buffers[i].xres, stpfile->buffers[i].yres, 1.0, 1.0, FALSE);
            read_field(dfield, stpfile->buffers + i);

            if (!file)
                file = gwy_file_new_in_construction();

            gwy_file_pass_image(file, i, dfield);
            process_metadata(stpfile, i, file);
            gwy_log_add_import(file, GWY_FILE_IMAGE, i, NULL, filename);
        }
    }

    gwy_unmap_file_contents(buffer);
    free_stpfile(stpfile);

    if (!file) {
        err_NO_DATA(error);
        return NULL;
    }

    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
free_stpfile(STPFile *stpfile)
{
    guint i;

    for (i = 0; i < stpfile->n; i++)
        g_hash_table_destroy(stpfile->buffers[i].meta);

    g_free(stpfile->buffers);
    g_hash_table_destroy(stpfile->meta);
    g_free(stpfile);
}

static guint
file_read_header(STPFile *stpfile,
                 gchar *buffer)
{
    STPData *data = NULL;
    GHashTable *meta;
    gchar *line, *key, *value = NULL;

    stpfile->meta = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
    meta = stpfile->meta;
    while ((line = gwy_str_next_line(&buffer))) {
        if (!strncmp(line, "buffer_id     ", KEY_LEN)) {
            gwy_debug("buffer id = %s", line + KEY_LEN);
            stpfile->n++;
            stpfile->buffers = g_renew(STPData, stpfile->buffers, stpfile->n);
            data = stpfile->buffers + (stpfile->n - 1);
            data->meta = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
            data->data = NULL;
            data->xres = data->yres = 0;
            data->id = atol(line + KEY_LEN);
            meta = data->meta;
        }
        if (!line[0] || line[0] == ' ')
            continue;

        key = g_strstrip(g_strndup(line, KEY_LEN));
        value = g_strstrip(g_strdup(line + KEY_LEN));
        g_hash_table_replace(meta, key, value);

        if (data) {
            if (gwy_strequal(key, "samples_x"))
                data->xres = atol(value);
            if (gwy_strequal(key, "samples_y"))
                data->yres = atol(value);
        }
    }

    return stpfile->n;
}

static gboolean
stpfile_get_double(GHashTable *meta,
                   const gchar *key,
                   gdouble *value)
{
    gchar *p, *end;
    gdouble r;

    p = g_hash_table_lookup(meta, key);
    if (!p)
        return FALSE;

    r = g_ascii_strtod(p, &end);
    if (end == p)
        return FALSE;

    *value = r;
    return TRUE;
}

static void
process_metadata(STPFile *stpfile,
                 guint id,
                 GwyFile *file)
{
    static const MetaDataFormat global_metadata[] = {
        { "software",      "Software",                  "%s",                 },
        { "op_mode",       "Operation mode",            "%s",                 },
        { "x_v_to_angs_0", "X calibration (linear)",    "%s Å/V",             },
        { "x_v_to_angs_1", "X calibration (quadratic)", "%s Å/V<sup>2</sup>", },
        { "y_v_to_angs_0", "Y calibration (linear)",    "%s Å/V",             },
        { "y_v_to_angs_1", "Y calibration (quadratic)", "%s Å/V<sup>2</sup>", },
        { "z_v_to_angs",   "Z calibration",             "%s Å/V",             },
        { "servo_sense",   "Servo sign",                "%s",                 },
    };
    static const MetaDataFormat local_metadata[] = {
        { "i_servo_gain", "Integral servo gain",        "%s",    },
        { "p_servo_gain", "Proportional servo gain",    "%s",    },
        { "servo_range",  "Servo range",                "%s V",  },
        { "tip_bias",     "Force setpoint (tip bias)",  "%s V",  },
        { "line_freq",    "Line frequency (requested)", "%s Hz", },
    };
    STPData *data = stpfile->buffers + id;
    GwyField *dfield = gwy_file_get_image(file, id);
    GwyUnit *siunit;
    gdouble q, r;
    gchar *p, *s;
    const gchar *title;
    gint power10;
    guint mode, i;

    if ((p = g_hash_table_lookup(data->meta, "source_mode"))) {
        mode = atol(p);
        title = gwy_enum_to_string(mode, channels, G_N_ELEMENTS(channels));
        if (title)
            gwy_file_set_title(file, GWY_FILE_IMAGE, id, title, FALSE);
    }
    else
        mode = 1;

    gwy_unit_set_from_string(gwy_field_get_unit_xy(dfield), "m");

    /* Fix value scale */
    switch (mode) {
        /* Topography */
        case 1:
        case 9:
        case 10:
        case 24:
        gwy_unit_set_from_string(gwy_field_get_unit_z(dfield), "m");
        if (stpfile_get_double(stpfile->meta, "z_v_to_angs", &q) && stpfile_get_double(data->meta, "servo_range", &r)) {
            gwy_field_multiply(dfield, q*r/32768*Angstrom);
            gwy_debug("z_v_to_angs = %g, servo_range = %g", q, r);
        }
        else
            gwy_field_multiply(dfield, Angstrom/32768);
        break;

        /* External */
        case 99:
        if (stpfile_get_double(data->meta, "ExSourceZFact", &q)) {
            s = g_hash_table_lookup(data->meta, "ExSourceZBip");
            if (s && gwy_strequal(s, "TRUE"))
                gwy_field_add(dfield, -32768);
            gwy_field_multiply(dfield, q);

            if ((s = g_hash_table_lookup(data->meta, "ExSourceZUnit"))) {
                siunit = GWY_UNIT(gwy_unit_new_parse(s, &power10));
                gwy_unit_assign(gwy_field_get_unit_z(dfield), siunit);
                g_object_unref(siunit);
                gwy_field_multiply(dfield, gwy_exp10(power10));
            }
        }
        break;

        /* Current, Deflection, Vec, Iec, Friction, and Auxes */
        default:
        gwy_field_add(dfield, -32768.0);
        gwy_field_multiply(dfield, 10.0/32768.0);
        gwy_unit_set_from_string(gwy_field_get_unit_z(dfield), "V");
        break;
    }

    /* Fix lateral scale */
    if (!stpfile_get_double(data->meta, "length_x", &r)) {
        g_warning("Missing or invalid x length");
        r = 1e4;
    }
    gwy_field_set_xreal(dfield, r*Angstrom);

    if (!stpfile_get_double(data->meta, "length_y", &r)) {
        g_warning("Missing or invalid y length");
        r = 1e4;
    }
    gwy_field_set_yreal(dfield, r*Angstrom);

    /* Metadata */
    GwyDict *meta = gwy_dict_new_in_construction();
    GString *str = g_string_new(NULL);

    /* Global */
    for (i = 0; i < G_N_ELEMENTS(global_metadata); i++) {
        if (!(p = g_hash_table_lookup(stpfile->meta, global_metadata[i].key)))
            continue;

        g_string_printf(str, global_metadata[i].format, p);
        gwy_dict_set_string_by_name(meta, global_metadata[i].meta, str->str);
    }

    /* Local */
    for (i = 0; i < G_N_ELEMENTS(local_metadata); i++) {
        if (!(p = g_hash_table_lookup(data->meta, local_metadata[i].key)))
            continue;

        g_string_printf(str, local_metadata[i].format, p);
        gwy_dict_set_string_by_name(meta, local_metadata[i].meta, str->str);
    }

    /* Special */
    if ((p = g_hash_table_lookup(data->meta, "Date")) && (s = g_hash_table_lookup(data->meta, "time")))
        gwy_dict_pass_string_by_name(meta, "Date", g_strconcat(p, " ", s, NULL));
    if ((p = g_hash_table_lookup(data->meta, "scan_dir"))) {
        if (gwy_strequal(p, "0"))
            gwy_dict_set_string_by_name(meta, "Scanning direction", "Top to bottom");
        else if (gwy_strequal(p, "1"))
            gwy_dict_set_string_by_name(meta, "Scanning direction", "Bottom to top");
    }
    if ((p = g_hash_table_lookup(data->meta, "collect_mode"))) {
        if (gwy_strequal(p, "1"))
            gwy_dict_set_string_by_name(meta, "Line direction", "Left to right");
        else if (gwy_strequal(p, "2"))
            gwy_dict_set_string_by_name(meta, "Line direction", "Right to left");
    }

    if (gwy_dict_get_n_items(meta))
        gwy_file_pass_meta(file, GWY_FILE_IMAGE, id, meta);
    else
        g_object_unref(meta);

    g_string_free(str, TRUE);
}

static void
read_field(GwyField *dfield,
           STPData *stpdata)
{
    gint xres, yres;

    xres = stpdata->xres;
    yres = stpdata->yres;
    gwy_field_resize(dfield, xres, yres);
    gwy_convert_raw_data(stpdata->data, xres*yres, 1,
                         GWY_RAW_DATA_UINT16, GWY_BYTE_ORDER_LITTLE_ENDIAN,
                         gwy_field_get_data(dfield), 1.0, 0.0);
    gwy_field_flip(dfield, FALSE, 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 : */
