/*
 *  $Id: oldmda.c 28803 2025-11-05 11:58:19Z yeti-dn $
 *  Copyright (C) 2010-2025 David Necas (Yeti), Petr Klapetek,
 *  Daniil Bratashov (dn2010)
 *  E-mail: yeti@gwyddion.net, klapetek@gwyddion.net, dn2010@gmail.com
 *
 *  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-oldmda-spm">
 *   <comment>NTMDT old MDA Spectra data</comment>
 *   <magic priority="80">
 *     <match type="string" offset="0" value="&lt;?xml">
 *       <match type="string" offset="20:60" value="&lt;MDAList&gt;"/>
 *     </match>
 *   </magic>
 *   <glob pattern="*.sxml"/>
 *   <glob pattern="*.SXML"/>
 * </mime-type>
 **/

/**
 * [FILE-MAGIC-FILEMAGIC]
 * # NT-MDT old MDA spectra
 * 0 string \x3c?xml
 * >&20 search/60 \x3cMDAList\x3e NT-MDT old MDA spectra text data
 **/

/**
 * [FILE-MAGIC-USERGUIDE]
 * NT-MDT old MDA
 * .sxml + .dat
 * Volume
 **/

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

#include "err.h"

#define EXTENSION ".sxml"

#define MAGIC1 "<?xml"
#define MAGIC1_SIZE (sizeof(MAGIC1)-1)
#define MAGIC2 "<MDAList>"
#define MAGIC2_SIZE (sizeof(MAGIC2) - 1)

typedef enum {
    MDA_XML_NONE             = 0,
    MDA_XML_ARRAYSIZE        = 1,
    MDA_XML_DATACELL_MEMSIZE = 2,
    MDA_XML_NAME             = 3,
    MDA_XML_UNITNAME         = 4,
    MDA_XML_MININDEX         = 5,
    MDA_XML_MAXINDEX         = 6,
    MDA_XML_DATAARRAY        = -1,
    MDA_XML_DATANAME         = -2
} MDAXMLParamType;

typedef struct {
    gchar   *name;
    gchar   *unitname;
    gdouble  bias;
    gdouble  scale;
    gint     minindex;
    gint     maxindex;
} MDAAxis;

typedef struct {
    guint           arraysize;
    guint           datacellmemsize;
    guint           res;
    GArray          *xdata;
    gchar           *dataname;
    GArray          *axes;
    gint            numaxes;
    MDAXMLParamType flag;
} MDAXMLParams;

typedef struct {
    guint        xres;
    guint        yres;
    guint        zres;
    gdouble      xreal;
    gdouble      yreal;
    GArray      *xdata;
    GwyUnit     *siunitx;
    GwyUnit     *siunity;
    GwyUnit     *siunitz;
    gint         power10x;
    gint         power10y;
    gint         power10z;
    const gchar *filename;
    GwyFile     *file;
}OldMDAFile;

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 void     free_params    (MDAXMLParams *par);
static void     read_params    (MDAXMLParams *par,
                                OldMDAFile *mdafile);
static void     read_data      (OldMDAFile *mdafile,
                                const gchar *buffer);
static gchar*   find_data_name (const gchar *headername,
                                const gchar *dataname);
static void     start_element  (GMarkupParseContext *context,
                                const gchar *element_name,
                                const gchar **attribute_names,
                                const gchar **attribute_values,
                                gpointer user_data,
                                GError **error);
static void     end_element    (GMarkupParseContext *context,
                                const gchar *element_name,
                                gpointer user_data,
                                GError **error);
static void     parse_text     (GMarkupParseContext *context,
                                const gchar *text,
                                gsize text_len,
                                gpointer user_data,
                                GError **error);


static GwyModuleInfo module_info = {
    GWY_MODULE_ABI_VERSION,
    &module_register,
    N_("Imports old NTMDT MDA Spectra files."),
    "dn2010 <dn2010@gmail.com>",
    "0.5",
    "David Nečas (Yeti) & Daniil Bratashov (dn2010)",
    "2012",
};

GWY_MODULE_QUERY2(module_info, oldmda)

static gboolean
module_register(void)
{
    gwy_file_func_register("oldmda",
                           N_("NTMDT old MDA Spectra data (.sxml .dat)"),
                           detect_file, load_file, NULL, NULL);

    return TRUE;
}

static gboolean
check_magic(const gchar *header)
{
    return (memcmp(header, MAGIC1, MAGIC1_SIZE) == 0) && (strstr(header, MAGIC2) != NULL);
}

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

    return check_magic(fileinfo->head) ? 100 : 0;
}

static GwyFile*
load_file(const gchar *filename,
          G_GNUC_UNUSED GwyRunModeFlags mode,
          GError **error)
{
    GwyFile *file = NULL;
    OldMDAFile mdafile;
    gchar *head, *buffer = NULL;
    gchar *buffer2 = NULL;
    gchar *dataname = NULL, *dname;
    GMarkupParser parser = { start_element, end_element, parse_text, NULL, NULL };
    GMarkupParseContext *context;
    gsize size, size2;
    MDAXMLParams params;
    GError *err = NULL;

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

    gwy_clear1(mdafile);
    head = g_strndup(buffer, 1024);
    if (!check_magic(head)) {
        err_FILE_TYPE(error, "NTMDT old MDA");
        g_free(head);
        goto fail;
    }
    g_free(head);

    params.xdata = g_array_new(FALSE, TRUE, sizeof(gdouble));
    params.numaxes = 0;
    params.axes = g_array_new(FALSE, FALSE, sizeof(MDAAxis));
    params.flag = MDA_XML_NONE;
    context = g_markup_parse_context_new(&parser, G_MARKUP_TREAT_CDATA_AS_TEXT, &params, NULL);
    if (!g_markup_parse_context_parse(context, buffer, size, &err)
        || !g_markup_parse_context_end_parse(context, &err)) {
        err_XML(error, &err);
        g_markup_parse_context_free(context);
        goto fail;
    }
    g_markup_parse_context_free(context);

    if (params.axes->len != 4) {
        g_set_error(error, GWY_MODULE_FILE_ERROR, GWY_MODULE_FILE_ERROR_DATA,
                    _("Incorrect number of axes in parameter file."));
        goto fail;
    }

    read_params(&params, &mdafile);

    dname = g_strdelimit(params.dataname, "\\/", G_DIR_SEPARATOR);
    if (!(dataname = find_data_name(filename, dname))) {
        g_set_error(error, GWY_MODULE_FILE_ERROR, GWY_MODULE_FILE_ERROR_DATA,
                    _("No corresponding data file was found for header file."));
        goto fail;
    }

    if (!g_file_get_contents(dataname, &buffer2, &size2, &err)) {
        g_set_error(error, GWY_MODULE_FILE_ERROR, GWY_MODULE_FILE_ERROR_IO,
                    _("Cannot load data file: %s"), err->message);
        g_clear_error(&err);
        goto fail;
    }

    if ((size2 != params.arraysize * params.datacellmemsize)
        || (params.arraysize != mdafile.xres * mdafile.yres * mdafile.zres)) {
        g_set_error(error, GWY_MODULE_FILE_ERROR, GWY_MODULE_FILE_ERROR_IO,
                    _("Data file is too short."));
        g_clear_error(&err);
        goto fail;
    }

    file = gwy_file_new_in_construction();
    mdafile.file = file;
    mdafile.filename = dataname;
    read_data(&mdafile, buffer2);

fail:
    g_free(buffer2);
    g_free(buffer);
    free_params(&params);

    return file;
}

static void
free_params(MDAXMLParams *par)
{
    guint i;

    if (par->axes) {
        for (i = 0; i < par->axes->len; i++) {
            MDAAxis *axis = &g_array_index(par->axes, MDAAxis, i);
            g_free(axis->name);
            g_free(axis->unitname);
        }
        g_array_free(par->axes, TRUE);
    }

    g_array_free(par->xdata, TRUE);
    g_free(par->dataname);
}

static void
read_params(MDAXMLParams *par, OldMDAFile *mdafile)
{
    MDAAxis axis;
    mdafile->zres = par->res;
    mdafile->xdata = par->xdata;
    axis = g_array_index(par->axes, MDAAxis, 0);
    if (axis.unitname)
        mdafile->siunitz = gwy_unit_new(axis.unitname);
    else
        mdafile->siunitz = gwy_unit_new(NULL);
    axis = g_array_index(par->axes, MDAAxis, 1);
    if (axis.unitname)
        mdafile->siunitx = gwy_unit_new_parse(axis.unitname, &mdafile->power10x);
    else
        mdafile->siunitx = gwy_unit_new(NULL);
    mdafile->xres = axis.maxindex - axis.minindex + 1;
    if (mdafile->xres < 1)
        mdafile->xres = 1;
    if (axis.scale <= 0.0)
        axis.scale = 1.0;
    mdafile->xreal = axis.scale * mdafile->xres * gwy_exp10(mdafile->power10x);
    axis = g_array_index(par->axes, MDAAxis, 2);
    if (axis.unitname)
        mdafile->siunity = gwy_unit_new_parse(axis.unitname, &mdafile->power10y);
    else
        mdafile->siunity = gwy_unit_new(NULL);
    mdafile->yres = axis.maxindex - axis.minindex + 1;
    if (mdafile->yres < 1)
        mdafile->yres = 1;
    if (axis.scale <= 0.0)
        axis.scale = 1.0;
    mdafile->yreal = axis.scale * mdafile->yres * gwy_exp10(mdafile->power10y);
}

static void
read_data(OldMDAFile *mdafile, const gchar *buffer)
{
    GwyBrick *brick;
    GwyLine *cal;
    gdouble *data;
    gint i, j, k;
    const guchar *p;
    gint xres = mdafile->xres, yres = mdafile->yres, zres = mdafile->zres;

    p = buffer;
    brick = gwy_brick_new(xres, yres, zres,
                          mdafile->xreal, mdafile->yreal, zres,
                          TRUE);
    data = gwy_brick_get_data(brick);

    for (k = 0; k < zres; k++) {
        p = buffer + k * 4;
        for (i = 0; i < yres; i++) {
            for (j = 0; j < xres; j++) {
                data[k*xres*yres + j + (yres - i - 1)*xres] = gwy_get_gint32_le(&p);
                p += (zres - 1) * 4;
            }
        }
    }

    gwy_unit_assign(gwy_brick_get_unit_x(brick), mdafile->siunitx);
    gwy_unit_assign(gwy_brick_get_unit_y(brick), mdafile->siunity);
    gwy_unit_assign(gwy_brick_get_unit_z(brick), mdafile->siunitz);

    cal = gwy_line_new(zres, zres, FALSE);
    data = gwy_line_get_data(cal);
    for (k = 0; k < zres; k++) {
        *(data++) = g_array_index(mdafile->xdata, gdouble, k);
    }
    gwy_unit_assign(gwy_line_get_unit_y(cal), mdafile->siunitz);
    gwy_brick_set_zcalibration(brick, cal);
    g_object_unref(cal);

    g_object_unref(mdafile->siunitx);
    g_object_unref(mdafile->siunity);
    g_object_unref(mdafile->siunitz);

    gwy_file_pass_volume(mdafile->file, 0, brick);
    gwy_file_set_title(mdafile->file, GWY_FILE_VOLUME, 0, "MDA data", FALSE);
    gwy_log_add_import(mdafile->file, GWY_FILE_VOLUME, 0, NULL, mdafile->filename);
}

static gchar*
find_data_name(const gchar *headername, const gchar *dataname)
{
    gchar *dirname = g_path_get_dirname(headername);
    gchar *data_name = g_path_get_basename(dataname);
    gchar *dname, *filename;

    filename = g_build_filename(dirname, data_name, NULL);
    gwy_debug("trying <%s>", filename);
    if (g_file_test(filename, G_FILE_TEST_IS_REGULAR)) {
        g_free(dirname);
        return filename;
    }
    g_free(filename);

    dname = g_ascii_strup(data_name, -1);
    filename = g_build_filename(dirname, dname, NULL);
    gwy_debug("trying <%s>", filename);
    if (g_file_test(filename, G_FILE_TEST_IS_REGULAR)) {
        g_free(dname);
        g_free(dirname);
        return filename;
    }
    g_free(dname);
    g_free(filename);

    dname = g_ascii_strdown(data_name, -1);
    filename = g_build_filename(dirname, dname, NULL);
    gwy_debug("trying <%s>", filename);
    if (g_file_test(filename, G_FILE_TEST_IS_REGULAR)) {
        g_free(dname);
        g_free(dirname);
        return filename;
    }
    g_free(dname);
    g_free(filename);
    g_free(dirname);

    gwy_debug("failed");

    return NULL;
}

static void
start_element(G_GNUC_UNUSED GMarkupParseContext *context,
              const gchar *element_name,
              G_GNUC_UNUSED const gchar **attribute_names,
              G_GNUC_UNUSED const gchar **attribute_values,
              gpointer user_data,
              G_GNUC_UNUSED GError **error)
{
    const gchar **name_cursor = attribute_names;
    const gchar **value_cursor = attribute_values;
    MDAAxis axis;
    gchar *value;

    MDAXMLParams *params = (MDAXMLParams *)user_data;
    if (params->flag != MDA_XML_NONE) {
        /* error */
    }
    else {
        if (gwy_strequal(element_name, "ArraySize")) {
            params->flag = MDA_XML_ARRAYSIZE;
        }
        else if (gwy_strequal(element_name, "DataCellMemSize")) {
            params->flag = MDA_XML_DATACELL_MEMSIZE;
        }
        else if (gwy_strequal(element_name, "MinIndex")) {
            params->flag = MDA_XML_MININDEX;
        }
        else if (gwy_strequal(element_name, "MaxIndex")) {
            params->flag = MDA_XML_MAXINDEX;
        }
        else if (gwy_strequal(element_name, "Name")) {
            params->flag = MDA_XML_NAME;
        }
        else if (gwy_strequal(element_name, "Data")) {
            params->flag = MDA_XML_DATANAME;
        }
        else if (gwy_strequal(element_name, "Calibration")) {
            params->numaxes++;
            gwy_clear1(axis);

            while (*name_cursor) {
                if (gwy_strequal(*name_cursor, "UnitName")) {
                    axis.unitname = g_strdup(*value_cursor);
                }
                else if (gwy_strequal(*name_cursor, "Bias")) {
                    value = g_strdup(*value_cursor);
                    axis.bias = g_ascii_strtod(g_strdelimit(value, ",.", '.'), NULL);
                    g_free(value);
                }
                else if (gwy_strequal(*name_cursor, "Scale")) {
                    value = g_strdup(*value_cursor);
                    axis.scale = g_ascii_strtod(g_strdelimit(value, ",.", '.'), NULL);
                    g_free(value);
                }
                name_cursor++;
                value_cursor++;
            }
            g_array_append_val(params->axes, axis);
        }
        else if (gwy_strequal(element_name, "Array")) {
            params->flag = MDA_XML_DATAARRAY;
            while (*name_cursor) {
                if (gwy_strequal(*name_cursor, "Count"))
                    params->res = atoi(*value_cursor);

                name_cursor++;
                value_cursor++;
            }
        }
    }
}

static void
end_element(G_GNUC_UNUSED GMarkupParseContext *context,
            G_GNUC_UNUSED const gchar *element_name,
            gpointer user_data,
            G_GNUC_UNUSED GError **error)
{
    MDAXMLParams *params = (MDAXMLParams*)user_data;

    params->flag = MDA_XML_NONE;
}

static void
parse_text(G_GNUC_UNUSED GMarkupParseContext *context,
           const gchar *value,
           gsize value_len,
           gpointer user_data,
           G_GNUC_UNUSED GError **error)
{
    MDAXMLParams *params = (MDAXMLParams*)user_data;
    MDAAxis *axis;
    gchar *line;
    gdouble val;
    gint i;
    gchar *v = g_strndup(value, value_len);

    axis = &g_array_index(params->axes, MDAAxis, params->numaxes - 1);

    if (params->flag == MDA_XML_NONE) {
        /* error */
    }
    else if (params->flag == MDA_XML_ARRAYSIZE) {
        params->arraysize = atoi(v);
    }
    else if (params->flag == MDA_XML_DATACELL_MEMSIZE) {
        params->datacellmemsize = atoi(v);
    }
    else if (params->flag == MDA_XML_DATANAME) {
        params->dataname = v;
        v = NULL;
    }
    else if (params->flag == MDA_XML_MININDEX) {
        axis->minindex = atoi(v);
    }
    else if (params->flag == MDA_XML_MAXINDEX) {
        axis->maxindex = atoi(v);
    }
    else if (params->flag == MDA_XML_NAME) {
        axis->name = v;
        v = NULL;
    }
    else if (params->flag == MDA_XML_DATAARRAY) {
        line = v;
        if (!params->res) {
            /* Error */
        }
        else {
            for (i = 0; i < params->res; i++) {
                val = g_ascii_strtod(g_strdelimit(line, ",.", '.'), &line);
                line += 2; /* skip ". " between values */
                g_array_append_val(params->xdata, val);
            }
        }
    }
    g_free(v);
}

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