/*
 *  $Id: spmxfile.c 28789 2025-11-04 17:14:03Z yeti-dn $
 *  Copyright (C) 2014-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-USERGUIDE]
 * ATC SPMxFormat data
 * .spm
 * Read
 **/

/**
 * [FILE-MAGIC-MISSING]
 * Indistinguishable from ZIP.  Avoding clash with a standard file format.
 **/

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

#include "err.h"
#include "gwyzip.h"

#define MAGIC "PK\x03\x04"
#define MAGIC_SIZE (sizeof(MAGIC)-1)
#define MAGIC1 "main.xml"
#define MAGIC1_SIZE (sizeof(MAGIC1)-1)
#define MAGIC2 ".chnl.raw"
#define MAGIC2_SIZE (sizeof(MAGIC2)-1)
#define BLOODY_UTF8_BOM "\xef\xbb\xbf"
#define EXTENSION ".spm"

typedef struct {
    gchar *id;
    gchar *name;
    gchar *filename;
    guint blocksize;
    guint dim;
    GPtrArray *subimages;
    GwyUnit *unitxy[2];
    guint xyres[2];
    gdouble xyscale[2];
    GwyUnit *unitz;
    gdouble zoff, zscale;
} SPMXStream;

typedef struct {
    const gchar *filename;
    GHashTable *hash;
    GString *path;
    GString *varid;
    GString *str;
    GArray *streams;
} SPMXFile;

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 parse_main      (GwyZipFile zipfile,
                                 SPMXFile *spmxfile,
                                 GError **error);
static gboolean read_binary_data(const SPMXFile *spmxfile,
                                 GwyZipFile zipfile,
                                 GwyFile *file,
                                 GError **error);
static void     free_spmxfile   (SPMXFile *spmxfile);

static GwyModuleInfo module_info = {
    GWY_MODULE_ABI_VERSION,
    &module_register,
    N_("Reads ATC SPMxFormat files."),
    "Yeti <yeti@gwyddion.net>",
    "2.0",
    "David Nečas (Yeti)",
    "2014",
};

GWY_MODULE_QUERY(module_info)

static gboolean
module_register(void)
{
    gwy_file_func_register("spmxfile",
                           N_("ATC SPMxFormat data (.spm)"),
                           detect_file, load_file, NULL, NULL);

    return TRUE;
}

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

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

    /* Generic ZIP file. */
    if (fileinfo->file_size < MAGIC_SIZE
        || memcmp(fileinfo->head, MAGIC, MAGIC_SIZE) != 0)
        return 0;

    /* It contains main.xml and maybe directory bindata.  One of them should be somewehre near the begining of the
     * file. */
    if (!gwy_memmem(fileinfo->head, fileinfo->buffer_len, MAGIC1, MAGIC1_SIZE)
        && !gwy_memmem(fileinfo->tail, fileinfo->buffer_len, MAGIC1, MAGIC1_SIZE)
        && !gwy_memmem(fileinfo->head, fileinfo->buffer_len, MAGIC2, MAGIC2_SIZE)
        && !gwy_memmem(fileinfo->tail, fileinfo->buffer_len, MAGIC2, MAGIC2_SIZE))
        return 0;

    /* We have to realy look inside.  And since main.xml is a popular name for the main XML document within such
     * files, we also have to see if we find "SPMxFormat" somewehre near the begining of the file. */
    if ((zipfile = gwyzip_open(fileinfo->name, NULL))) {
        if (gwyzip_locate_file(zipfile, "main.xml", 1, NULL)
            && (content = gwyzip_get_file_content(zipfile, NULL, NULL))) {
            if (g_strstr_len(content, 4096, "SPMxFormat"))
                score = 100;
            g_free(content);
        }
        gwyzip_close(zipfile);
    }

    return score;
}

static GwyFile*
load_file(const gchar *filename,
          G_GNUC_UNUSED GwyRunModeFlags mode,
          GError **error)
{
    GwyFile *file = NULL;
    SPMXFile spmxfile;
    GwyZipFile zipfile;

    if (!(zipfile = gwyzip_open(filename, error)))
        return NULL;

    gwy_clear1(spmxfile);
    spmxfile.filename = filename;
    if (!parse_main(zipfile, &spmxfile, error))
        goto fail;

    if (!spmxfile.streams->len) {
        err_NO_DATA(error);
        goto fail;
    }

    file = gwy_file_new_in_construction();
    if (!read_binary_data(&spmxfile, zipfile, file, error))
        g_clear_object(&file);

fail:
    gwyzip_close(zipfile);
    free_spmxfile(&spmxfile);

    return file;
}

static gboolean
read_binary_data(const SPMXFile *spmxfile,
                 GwyZipFile zipfile,
                 GwyFile *file,
                 GError **error)
{
    GArray *streams = spmxfile->streams;
    GwyField *dfield;
    guchar *content;
    gchar *title, *subimage;
    gsize contentsize, expected_size;
    guint i, j, n;
    gint id = 0;

    for (i = 0; i < streams->len; i++) {
        SPMXStream *stream = &g_array_index(streams, SPMXStream, i);

        if (!gwyzip_locate_file(zipfile, stream->filename, 1, error)
            || !(content = gwyzip_get_file_content(zipfile, &contentsize, error)))
            return FALSE;

        n = stream->xyres[0]*stream->xyres[1];
        expected_size = n*stream->blocksize*stream->subimages->len;
        if (err_SIZE_MISMATCH(error, expected_size, contentsize, TRUE)) {
            g_free(content);
            return FALSE;
        }

        for (j = 0; j < stream->subimages->len; j++) {
            dfield = gwy_field_new(stream->xyres[0], stream->xyres[1],
                                   stream->xyscale[0]*stream->xyres[0], stream->xyscale[1]*stream->xyres[1],
                                   FALSE);
            gwy_convert_raw_data(content + j*n*stream->blocksize, n, 1,
                                 GWY_RAW_DATA_SINT16, GWY_BYTE_ORDER_LITTLE_ENDIAN,
                                 gwy_field_get_data(dfield), stream->zscale, stream->zoff);

            gwy_unit_assign(gwy_field_get_unit_xy(dfield), stream->unitxy[0]);
            gwy_unit_assign(gwy_field_get_unit_z(dfield), stream->unitz);

            gwy_file_pass_image(file, id, dfield);
            gwy_log_add_import(file, GWY_FILE_IMAGE, id, NULL, spmxfile->filename);

            subimage = (gchar*)g_ptr_array_index(stream->subimages, j);
            title = g_strdup_printf("%s %s", stream->name, subimage);
            gwy_file_pass_title(file, GWY_FILE_IMAGE, id, title);

            id++;
        }

        g_free(content);
    }

    return TRUE;
}

static SPMXStream*
current_stream(SPMXFile *spmxfile)
{
    GArray *streams = spmxfile->streams;
    guint nstreams = streams->len;

    if (!nstreams)
        return NULL;

    return &g_array_index(streams, SPMXStream, nstreams-1);
}

static const gchar*
find_attribute(const gchar **attribute_names, const gchar **attribute_values,
               const gchar *attrname)
{
    guint i;

    if (!attribute_names)
        return NULL;

    for (i = 0; attribute_names[i]; i++) {
        if (gwy_strequal(attribute_names[i], attrname))
            return attribute_values[i];
    }

    return NULL;
}

static gboolean
require_attributes(const gchar *path,
                   const gchar **attribute_names,
                   const gchar **attribute_values,
                   GError **error,
                   ...)
{
    va_list ap;
    const gchar *attrname;
    gchar *errpath;

    va_start(ap, error);
    while ((attrname = va_arg(ap, const gchar *))) {
        if (!find_attribute(attribute_names, attribute_values, attrname)) {
            errpath = g_strconcat(path, "::", attrname, NULL);
            err_MISSING_FIELD(error, errpath);
            g_free(errpath);
            va_end(ap);
            return FALSE;
        }
    }
    va_end(ap);

    return TRUE;
}

static void
start_element(G_GNUC_UNUSED GMarkupParseContext *context,
              const gchar *element_name,
              const gchar **attribute_names,
              const gchar **attribute_values,
              gpointer user_data,
              GError **error)
{
    SPMXFile *spmxfile = (SPMXFile*)user_data;
    SPMXStream *stream = current_stream(spmxfile);
    const gchar *value;
    gchar *path;
    gint power10;

    g_string_append_c(spmxfile->path, '/');
    g_string_append(spmxfile->path, element_name);
    path = spmxfile->path->str;
    gwy_debug("%s", path);

    if (gwy_strequal(path, "/spmx/stream")) {
        SPMXStream newstream;

        if (!require_attributes(path, attribute_names, attribute_values, error, "id", "name", "blocksize", NULL))
            return;

        gwy_clear1(newstream);
        value = find_attribute(attribute_names, attribute_values, "id");
        newstream.id = g_strdup(value);
        value = find_attribute(attribute_names, attribute_values, "name");
        newstream.name = g_strdup(value);
        value = find_attribute(attribute_names, attribute_values, "blocksize");
        newstream.blocksize = atoi(value);

        newstream.subimages = g_ptr_array_new();
        g_array_append_val(spmxfile->streams, newstream);
    }
    else if (gwy_strequal(path, "/spmx/var")) {
        if ((value = find_attribute(attribute_names, attribute_values, "id")))
            g_string_assign(spmxfile->varid, value);
        else
            g_string_truncate(spmxfile->varid, 0);
    }
    else if (gwy_strequal(path, "/spmx/stream/axis")) {
        g_return_if_fail(stream);
        /* There can be multiple records (e.g. Height also as voltage).  Take the first.  */
        if (stream->unitz)
            return;

        if (!require_attributes(path, attribute_names, attribute_values, error, "scale", "units", NULL))
            return;

        value = find_attribute(attribute_names, attribute_values, "scale");
        stream->zscale = g_ascii_strtod(value, NULL);
        value = find_attribute(attribute_names, attribute_values, "units");
        stream->unitz = gwy_unit_new_parse(value, &power10);
        stream->zscale *= gwy_exp10(power10);

        if ((value = find_attribute(attribute_names, attribute_values,
                                    "start")))
            stream->zoff = g_ascii_strtod(value, NULL) * gwy_exp10(power10);
    }
    else if (gwy_strequal(path, "/spmx/stream/dimension")) {
        g_return_if_fail(stream);
        if (!require_attributes(path, attribute_names, attribute_values, error, "length", NULL))
            return;

        /* XXX: More than two axes? */
        if (stream->dim >= G_N_ELEMENTS(stream->xyres)) {
            g_set_error(error, GWY_MODULE_FILE_ERROR, GWY_MODULE_FILE_ERROR_DATA,
                        _("Only two-dimensional images are supported."));
            return;
        }

        value = find_attribute(attribute_names, attribute_values, "length");
        stream->xyres[stream->dim] = atoi(value);
        gwy_debug("res[%u] %u", stream->dim, stream->xyres[stream->dim]);
    }
    else if (gwy_strequal(path, "/spmx/stream/dimension/axis")) {
        g_return_if_fail(stream);
        /* There can be multiple records (e.g. distance also as time).  Take
         * the first.  */
        if (stream->dim >= G_N_ELEMENTS(stream->xyres) || stream->unitxy[stream->dim])
            return;

        if (!require_attributes(path, attribute_names, attribute_values, error, "scale", "units", NULL))
            return;

        value = find_attribute(attribute_names, attribute_values, "scale");
        stream->xyscale[stream->dim] = g_ascii_strtod(value, NULL);
        value = find_attribute(attribute_names, attribute_values, "units");
        stream->unitxy[stream->dim] = gwy_unit_new_parse(value, &power10);
        stream->xyscale[stream->dim] *= gwy_exp10(power10);
    }
    else if (gwy_strequal(path, "/spmx/stream/data")) {
        g_return_if_fail(stream);
        if (!require_attributes(path, attribute_names, attribute_values, error, "content", "ref", NULL))
            return;

        value = find_attribute(attribute_names, attribute_values, "content");
        if (!gwy_strequal(value, "void")) {
            err_UNSUPPORTED(error, "/spmx/stream/data::content");
            return;
        }
        value = find_attribute(attribute_names, attribute_values, "ref");
        /* XXX: The file names are in fact bound to the ids via ENTITY.  We just assume the naming is consistent. */
        stream->filename = g_strconcat(value, ".raw", NULL);
    }
}

static void
end_element(G_GNUC_UNUSED GMarkupParseContext *context,
            const gchar *element_name,
            gpointer user_data,
            G_GNUC_UNUSED GError **error)
{
    SPMXFile *spmxfile = (SPMXFile*)user_data;
    guint n, len = spmxfile->path->len;
    gchar *path = spmxfile->path->str;

    n = strlen(element_name);
    g_return_if_fail(g_str_has_suffix(path, element_name));
    g_return_if_fail(len > n);
    g_return_if_fail(path[len-1 - n] == '/');
    gwy_debug("%s", path);

    if (gwy_strequal(path, "/spmx/stream/dimension")) {
        SPMXStream *stream = current_stream(spmxfile);
        stream->dim++;
    }

    g_string_set_size(spmxfile->path, len-1 - n);
}

static void
text(G_GNUC_UNUSED GMarkupParseContext *context,
     const gchar *value,
     G_GNUC_UNUSED gsize value_len,
     gpointer user_data,
     G_GNUC_UNUSED GError **error)
{
    SPMXFile *spmxfile = (SPMXFile*)user_data;
    SPMXStream *stream = current_stream(spmxfile);
    gchar *path = spmxfile->path->str;
    gchar *varid = spmxfile->varid->str;
    GString *str = spmxfile->str;

    if (!*value)
        return;

    /* I thought this would somehow refer to <var>s, but many mentioned in <depends> are blatantly missing...  So just
     * ignore them. */
    if (gwy_strequal(path, "/spmx/stream/depends"))
        return;

    g_string_assign(str, value);
    g_strstrip(str->str);
    if (!*(str->str))
        return;

    if (gwy_strequal(path, "/spmx/var") && strlen(varid)) {
        gwy_debug("var %s = <%s>", varid, str->str);
        g_hash_table_insert(spmxfile->hash, g_strdup(varid), g_strdup(str->str));
    }
    else if (gwy_strequal(path, "/spmx/stream/subimage")) {
        gwy_debug("subimage <%s>", str->str);
        g_return_if_fail(stream);
        g_ptr_array_add(stream->subimages, g_strdup(str->str));
    }
    else {
        gwy_debug("%s <%s>", path, str->str);
    }
}

static gboolean
parse_main(GwyZipFile zipfile,
           SPMXFile *spmxfile,
           GError **error)
{
    GMarkupParser parser = {
        &start_element, &end_element, &text, NULL, NULL,
    };
    GMarkupParseContext *context = NULL;
    guchar *content = NULL, *s;
    gboolean ok = FALSE;
    GError *err = NULL;

    if (!gwyzip_locate_file(zipfile, "main.xml", 1, error)
        || !(content = gwyzip_get_file_content(zipfile, NULL, error)))
        return FALSE;

    gwy_strkill(content, "\r");
    s = content;
    /* Not seen in the wild but the XML people tend to use BOM in UTF-8... */
    if (g_str_has_prefix(s, BLOODY_UTF8_BOM))
        s += strlen(BLOODY_UTF8_BOM);

    spmxfile->path = g_string_new(NULL);
    spmxfile->varid = g_string_new(NULL);
    spmxfile->str = g_string_new(NULL);
    spmxfile->hash = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
    spmxfile->streams = g_array_new(FALSE, FALSE, sizeof(SPMXStream));

    context = g_markup_parse_context_new(&parser, 0, spmxfile, NULL);
    if (!g_markup_parse_context_parse(context, s, -1, &err) || !g_markup_parse_context_end_parse(context, &err))
        err_XML(error, &err);
    else
        ok = TRUE;

    /* TODO: We still need to do some sanity checks, for instance whether the streams have set dimensions and units.
     */

    if (context)
        g_markup_parse_context_free(context);
    g_free(content);

    return ok;
}

static void
free_spmxfile(SPMXFile *spmxfile)
{
    if (spmxfile->hash)
        g_hash_table_destroy(spmxfile->hash);
    if (spmxfile->path)
        g_string_free(spmxfile->path, TRUE);
    if (spmxfile->varid)
        g_string_free(spmxfile->varid, TRUE);
    if (spmxfile->str)
        g_string_free(spmxfile->str, TRUE);
    if (spmxfile->streams) {
        guint i, j;

        for (i = 0; i < spmxfile->streams->len; i++) {
            SPMXStream *stream = &g_array_index(spmxfile->streams, SPMXStream, i);

            g_free(stream->id);
            g_free(stream->name);
            g_free(stream->filename);
            g_clear_object(stream->unitxy + 0);
            g_clear_object(stream->unitxy + 1);
            g_clear_object(&stream->unitz);

            for (j = 0; j < stream->subimages->len; j++)
                g_free(g_ptr_array_index(stream->subimages, j));
            g_ptr_array_free(stream->subimages, TRUE);
        }
        g_array_free(spmxfile->streams, 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 : */
