/*
 *  $Id: nsidfile.c 29712 2026-04-02 09:13:11Z yeti-dn $
 *  Copyright (C) 2024 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-USERGUIDE]
 * N-Dimensional Spectroscopy and Imaging Data (NSID) HDF5
 * .h5
 * Read
 **/

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

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

#include "gwyhdf5.h"
#include "hdf5file.h"

enum {
    FOUND_NSID_VERSION   = (1u << 0),
    FOUND_PYNSID_VERSION = (1u << 1),

    FOUND_MACHINE_ID     = (1u << 2),
    FOUND_PLATFORM       = (1u << 3),
    FOUND_TIMESTAMP      = (1u << 4),
    FOUND_TITLE          = (1u << 5),
    FOUND_MODALITY       = (1u << 6),
    FOUND_DATA_TYPE      = (1u << 7),
};

typedef struct {
    guint flags;
    guint depth;
    guint maxdepth;
} DetectScanData;

typedef struct {
    GwyDict *meta;
    gchar *prefix;
    GString *str;
} CopyMetaData;

static gint     detect_file              (const GwyFileDetectInfo *fileinfo,
                                          gboolean only_name);
static GwyFile* load_file                (const gchar *filename,
                                          GwyRunModeFlags mode,
                                          GError **error);
static void     read_image               (hid_t file_id,
                                          const gchar *name,
                                          GwyHDF5File *ghfile,
                                          const gchar *filename,
                                          GwyFile *file,
                                          gint *id);
static gint     find_corresponding_scales(hid_t file_id,
                                          hid_t dataset,
                                          GwyHDF5File *ghfile,
                                          gchar **data_scales,
                                          guint ndims);

void
gwyhdf5_register_nsidfile(void)
{
    gwy_file_func_register("nsidfile",
                           N_("N-Dimensional Spectroscopy and Imaging Data (NSID) HDF5 files (.h5)"),
                           detect_file, load_file, NULL, NULL);
}

static void
look_for_canary_attributes(hid_t loc_id, guint *flags,
                           G_GNUC_UNUSED gint depth)
{
    static const GwyEnum attributes[] = {
        { "machine_id",     FOUND_MACHINE_ID,     },
        { "platform",       FOUND_PLATFORM,       },
        { "timestamp",      FOUND_TIMESTAMP,      },
        { "nsid_version",   FOUND_NSID_VERSION,   },
        { "pyNSID_version", FOUND_PYNSID_VERSION, },
        { "title",          FOUND_TITLE,          },
        { "modality",       FOUND_MODALITY,       },
        { "data_type",      FOUND_DATA_TYPE,      },
    };
    guint i, bit;

    for (i = 0; i < G_N_ELEMENTS(attributes); i++) {
        bit = attributes[i].value;
        if (!(*flags & bit) && H5Aexists(loc_id, attributes[i].name) > 0) {
            gwy_debug("found %s (at depth %d)", attributes[i].name, depth);
            *flags |= bit;
        }
    }
}

static herr_t
detect_group_scan(hid_t loc_id,
                  const char *name,
                  G_GNUC_UNUSED const H5L_info_t *info,
                  void *user_data)
{
    DetectScanData *dsd = (DetectScanData*)user_data;
    H5O_info_t infobuf;
    hid_t group, dataset;
    herr_t status;

    gwy_debug("%s", name);

    status = H5Oget_info_by_name(loc_id, name, &infobuf, H5O_INFO_BASIC, H5P_DEFAULT);
    if (status < 0)
        return status;

    if (infobuf.type == H5O_TYPE_GROUP && (group = H5Gopen(loc_id, name, H5P_DEFAULT)) >= 0) {
        look_for_canary_attributes(group, &dsd->flags, dsd->depth);
        if (dsd->depth+1 < dsd->maxdepth) {
            dsd->depth++;
            status = H5Literate(group, H5_INDEX_NAME, H5_ITER_NATIVE, NULL, detect_group_scan, dsd);
            dsd->depth--;
        }
        status = H5Gclose(group);
    }
    else if (infobuf.type == H5O_TYPE_DATASET && (dataset = H5Dopen(loc_id, name, H5P_DEFAULT)) >= 0) {
        look_for_canary_attributes(dataset, &dsd->flags, dsd->depth);
        status = H5Dclose(dataset);
    }

    return status;
}

static gint
detect_file(const GwyFileDetectInfo *fileinfo, gboolean only_name)
{
    static const guint other_flags[] = {
        FOUND_MACHINE_ID, FOUND_PLATFORM, FOUND_TIMESTAMP, FOUND_TITLE, FOUND_MODALITY, FOUND_DATA_TYPE,
    };
    DetectScanData dsd = { 0, 0, 3 };
    hid_t file_id;
    herr_t status;
    gint score = 0;
    guint i;

    if ((file_id = gwyhdf5_quick_check(fileinfo, only_name)) < 0)
        return 0;

    look_for_canary_attributes(file_id, &dsd.flags, 0);
    status = H5Literate(file_id, H5_INDEX_NAME, H5_ITER_NATIVE, NULL, detect_group_scan, &dsd);
    gwy_debug("status %d", status);
    if (status < 0)
        return 0;

    if (!dsd.flags) {
        H5Fclose(file_id);
        return 0;
    }
    dsd.maxdepth++;
    status = H5Literate(file_id, H5_INDEX_NAME, H5_ITER_NATIVE, NULL, detect_group_scan, &dsd);
    H5Fclose(file_id);

    if (status < 0)
        return 0;

    /* This will return 30 for files with only auxiliary flags (all of them), 50 for NSID but no auxiliary flags
     * and 80 for both. */
    if (dsd.flags & (FOUND_NSID_VERSION | FOUND_PYNSID_VERSION))
        score = 50;
    for (i = 0; i < G_N_ELEMENTS(other_flags); i++) {
        if (dsd.flags & other_flags[i])
            score += 5;
    }

    return score;
}

static GwyFile*
load_file(const gchar *filename,
          G_GNUC_UNUSED GwyRunModeFlags mode,
          GError **error)
{
    GwyFile *file = NULL;
    GwyHDF5File ghfile;
    hid_t file_id;
    herr_t status;
    H5O_info_t infobuf;
    guint i;
    gint id;

    if ((file_id = H5Fopen(filename, H5F_ACC_RDONLY, H5P_DEFAULT)) < 0) {
        err_HDF5(error, "H5Fopen", file_id);
        return NULL;
    }
    gwy_debug("file_id %d", (gint)file_id);
    status = H5Oget_info(file_id, &infobuf, H5O_INFO_BASIC);
    if (!gwyhdf5_check_status(status, file_id, NULL, "H5Oget_info", error))
        return NULL;

    gwyhdf5_init(&ghfile, file_id, infobuf.token);
    status = H5Literate(file_id, H5_INDEX_NAME, H5_ITER_NATIVE, NULL, gwyhdf5_scan_file, &ghfile);
    if (!gwyhdf5_check_status(status, file_id, &ghfile, "H5Literate", error))
        return NULL;

    file = gwy_file_new_in_construction();

    /* Read generic simple 2D data, i.e. images.
     * TODO: May want to read also other data types. */
    id = 0;
    for (i = 0; i < ghfile.datasets->len; i++)
        read_image(file_id, g_array_index(ghfile.datasets, gchar*, i), &ghfile, filename, file, &id);

    status = gwyhdf5_fclose(file_id);
    gwy_debug("status %d", status);

    gwyhdf5_free(&ghfile);

    if (!gwy_dict_get_n_items(GWY_DICT(file))) {
        g_clear_object(&file);
        err_NO_DATA(error);
    }

    return file;
}

static void
copy_meta(GQuark quark, GValue *gvalue, gpointer user_data)
{
    const gchar *key = g_quark_to_string(quark);
    const gchar *value = g_value_get_string(gvalue);
    CopyMetaData *cmd = (CopyMetaData*)user_data;
    GString *str = cmd->str;
    gchar *s, *newkey = NULL;
    gchar c;

    /* If it is longer than a tweet it is probably some LUT table or a similar monster. Ignore those. */
    if (strlen(value) > 140)
        return;

    g_string_truncate(str, 1);
    g_string_append(str, key);
    if (g_str_has_prefix(str->str, cmd->prefix)) {
        /* Metadata under the data. */
        s = str->str + strlen(cmd->prefix);
        if (g_str_has_prefix(s, "/metadata"))
            s += strlen("/metadata");
        else if (g_str_has_prefix(s, "/original_metadata"))
            s += strlen("/original_metadata");

        if (s[0] == '/')
            s++;
        newkey = gwy_strreplace(s, "/", "::", (gsize)-1);
        gwy_dict_set_string_by_name(cmd->meta, newkey, value);
        g_free(newkey);
    }
    else if ((s = strrchr(str->str, '/'))) {
        /* Metadata in direct parent levels. */
        c = s[1];
        s[1] = '\0';
        if (c && g_str_has_prefix(cmd->prefix, str->str)) {
            s[1] = c;
            newkey = gwy_strreplace(s+1, "/", "::", (gsize)-1);
            if (!gwy_dict_contains_by_name(cmd->meta, newkey))
                gwy_dict_set_string_by_name(cmd->meta, newkey, value);
            g_free(newkey);
        }
    }
}

static void
read_image(hid_t file_id, const gchar *path, GwyHDF5File *ghfile, const gchar *filename,
           GwyFile *file, gint *id)
{
    enum { FIELD_NDIMS = 2 };
    gint dims[FIELD_NDIMS], scaledim[1];
    gchar *dim_scales[FIELD_NDIMS];
    GwyField *field;
    CopyMetaData cmd;
    hid_t dataset;
    herr_t status;
    gdouble *scale_data;
    guint j;
    gint power10;
    gchar *s;
    gdouble dx;

    gwy_clear(dim_scales, FIELD_NDIMS);
    for (j = 0; j < FIELD_NDIMS; j++)
        dims[j] = -1;

    if ((dataset = gwyhdf5_open_and_check_dataset(file_id, path, 2, dims, NULL)) < 0)
        return;

    if (err_DIMENSION(NULL, dims[0]) || err_DIMENSION(NULL, dims[1])) {
        H5Dclose(dataset);
        return;
    }

    find_corresponding_scales(file_id, dataset, ghfile, dim_scales, FIELD_NDIMS);

    field = gwy_field_new(dims[1], dims[0], dims[1], dims[0], FALSE);
    status = H5Dread(dataset, H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, H5P_DEFAULT, gwy_field_get_data(field));
    gwy_debug("status %d", status);
    H5Dclose(dataset);
    if (status < 0) {
        g_object_unref(field);
        goto end;
    }

    if (gwyhdf5_get_str_attr_g(file_id, path, "units", &s, NULL)) {
        power10 = gwy_unit_set_from_string(gwy_field_get_unit_z(field), s);
        if (power10)
            gwy_field_multiply(field, gwy_exp10(power10));
        g_free(s);
    }

    for (j = 0; j < FIELD_NDIMS; j++) {
        if (!dim_scales[j])
            continue;

        power10 = 0;
        if (gwyhdf5_get_str_attr_g(file_id, dim_scales[j], "units", &s, NULL)) {
            /* This ultimately uses Y for units because it comes second. */
            power10 = gwy_unit_set_from_string(gwy_field_get_unit_xy(field), s);
            g_free(s);
        }

        scaledim[0] = dims[j];
        if ((dataset = gwyhdf5_open_and_check_dataset(file_id, dim_scales[j], 1, scaledim, NULL)) >= 0) {
            scale_data = g_new(gdouble, dims[j]);
            if (H5Dread(dataset, H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, H5P_DEFAULT, scale_data) >= 0) {
                dx = fabs(scale_data[dims[j] - 1] - scale_data[j]);
                if (dims[j] > 1)
                    dx *= dims[j]/(dims[j] - 1.0);

                if (j == 1)
                    gwy_field_set_yreal(field, gwy_exp10(power10)*dx);
                else
                    gwy_field_set_xreal(field, gwy_exp10(power10)*dx);
            }
            g_free(scale_data);
            H5Dclose(dataset);
        }
    }

    gwy_file_pass_image(file, *id, field);
    gwy_log_add_import(file, GWY_FILE_IMAGE, *id, NULL, filename);

    if (gwyhdf5_get_str_attr_g(file_id, path, "title", &s, NULL)
        || gwyhdf5_get_str_attr_g(file_id, path, "quantity", &s, NULL)
        || gwyhdf5_get_str_attr_g(file_id, path, "main_data_name", &s, NULL))
        gwy_file_pass_title(file, GWY_FILE_IMAGE, *id, s);

    cmd.meta = gwy_dict_new_in_construction();
    cmd.prefix = g_strdup(path);
    if ((s = strrchr(cmd.prefix, '/')))
        *s = '\0';
    cmd.str = g_string_new("/");
    gwy_dict_foreach(ghfile->meta, NULL, copy_meta, &cmd);
    if (gwy_dict_get_n_items(cmd.meta))
        gwy_file_pass_meta(file, GWY_FILE_IMAGE, *id, cmd.meta);
    else
        g_object_unref(cmd.meta);
    g_string_free(cmd.str, TRUE);
    g_free(cmd.prefix);

    (*id)++;

end:
    for (j = 0; j < FIELD_NDIMS; j++)
        g_free(dim_scales[j]);
}

static gint
find_corresponding_scales(hid_t file_id, hid_t dataset, GwyHDF5File *ghfile,
                          gchar **data_scales, guint ndims)
{
    hid_t dscale;
    const gchar *dscale_name;
    guint i, j, nattached = 0;
    gboolean is_attached;

    for (j = 0; j < ndims; j++) {
        for (i = 0; i < ghfile->datascales->len; i++) {
            dscale_name = g_array_index(ghfile->datascales, const gchar*, i);
            dscale = H5Dopen(file_id, dscale_name, H5P_DEFAULT);
            if (dscale >= 0) {
                is_attached = H5DSis_attached(dataset, dscale, j);
                H5Dclose(dscale);
                if (is_attached) {
                    gwy_debug("dim%d has as datascale %s", j, dscale_name);
                    data_scales[j] = g_strdup(dscale_name);
                    nattached++;
                    break;
                }
            }
        }
    }

    return nattached;
}

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