/*
 *  $Id: veloxemd.c 30114 2026-06-22 11:52:09Z yeti-dn $
 *  Copyright (C) 2026 David Necas (Yeti).
 *  E-mail: yeti@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]
 * ThermoFisher Velox EMD TEM HDF5
 * .emd
 * Read
 **/

/**
 * [FILE-MAGIC-FREEDESKTOP]
 * <mime-type type="application/x-thermofisher-velox-tem-data">
 *   <comment>ThermoFisher Velox EMD TEM data</comment>
 *   <glob pattern="*.emd"/>
 *   <glob pattern="*.emd"/>
 * </mime-type>
 **/

#if 0
File structure may look like this:
GROUP "/" {
   GROUP "Data" {
      GROUP "Image" {
         GROUP "3c9dbaf7f58e48068d464317374b1c06" {
            DATASET "Data" {
            DATASET "FrameLookupTable" {
            DATASET "Metadata" {
      GROUP "Spectrum" {
         GROUP "3c9dbaf7f58e48068d464317374b1c06" {
            DATASET "Data" {
            DATASET "FrameLookupTable" {
            DATASET "Metadata" {
      GROUP "SpectrumImage" {
         GROUP "3c9dbaf7f58e48068d464317374b1c06" {
            DATASET "Data" {
            DATASET "FrameLookupTable" {
            DATASET "Metadata" {
      GROUP "SpectrumStream" {
         GROUP "3c9dbaf7f58e48068d464317374b1c06" {
            DATASET "Data" {
            DATASET "FrameLookupTable" {
            DATASET "Metadata" {
      GROUP "Text" {
         DATASET "a4cce2b18f65436785e65f80d7022522" {
   DATASET "Experiment" {
   GROUP "Features" {
      GROUP "CameraFeature" {
         DATASET "8034d415a2194b9199568ed69a22d1c1" {
      DATASET "Features" {
   DATASET "Info" {
   GROUP "Operations" {
      GROUP "CameraInputOperation" {
         DATASET "27ca18b1eac34aee997412fc84c6370a" {
      DATASET "Operations" {
   GROUP "Presentation" {
      DATASET "DisplayIndex" {
      GROUP "Displays" {
         GROUP "HtmlDisplay" {
            DATASET "5b094c4684ba40958ee64afd9f248989" {
         GROUP "ImageDisplay" {
            DATASET "140de1a61eca444b892095e698529b78" {
   GROUP "Displays" {
      GROUP "DisplayGroup" {
         DATASET "bad72cefd1324df196494bf70c79c394" {
      GROUP "ImageDisplay" {
         DATASET "d25209ba9d2d449bb0d37885ddb1ce97" {
      GROUP "SpectrumDisplay" {
         DATASET "d01793d461844e4b8a50279acbd4e4c8" {
   GROUP "SharedProperties" {
      GROUP "Annotation" {
         DATASET "b046fd5cbbc546d1a1f3d9e523a46ea8" {
      GROUP "BackgroundCorrection" {
         DATASET "6c04c7c5d6b4429cbc0f25e69606e2df" {
         DATASET "f91bab9ccefe4b6b87f203b5d698548e" {
      GROUP "BackgroundWindows" {
         DATASET "e773faf4a87347278bbc1f89dcb47b12" {
         DATASET "fa59dc9d20bc463c921e89c08569bd66" {
      GROUP "DataReference" {
         DATASET "0b642447eeb84f6085d10987cd6b4dc8" {
      GROUP "DisplayGroupItem" {
         DATASET "61f50c8ea3fb47cca72415c5d5d16fad" {
      GROUP "EDSSpectrumQuantificationSettings" {
         DATASET "45d0195af26a46b39d5fbf5d18c97cd8" {
         DATASET "75c1c295a08f48c7953f729d10bc35ba" {
      GROUP "ImageDisplaySettings" {
         DATASET "6fe128f8debd418386649a219535dd60" {
         DATASET "7a45eea3b72a46729e27e89f767ab5ae" {
      GROUP "ImageSeriesDataReference" {
         DATASET "464783e7d65545ba80a4e890463c3fcc" {
      GROUP "ModeledBackgroundCorrectionModel" {
         DATASET "988233a670384531870cb4c271c60a26" {
         DATASET "fc41d43f60d343398db65350dd13b885" {
      GROUP "MultipolyBackgroundCorrectionModel" {
         DATASET "93efabe1865541f2a8e45afe4293d68a" {
         DATASET "e2907d557ba3463783a4e7a77cb4488e" {
      GROUP "Shape" {
         DATASET "4311bd01447d4b9eadbfca49d30d612e" {
      GROUP "SpectralFilterSettings" {
         DATASET "a8c9189f17a94b7b96456ef64748f500" {
      GROUP "UnaryImageFilterSettings" {
         DATASET "5afc61a601e34242a564882e0588ccea" {
   DATASET "Thumbnail.jpg" {
   DATASET "Version" {
#endif

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

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

/* ThermoFisher Velox (EMD) */
typedef struct {
    const gchar *filename;
    GwyFile *data;
    GHashTable *titles;
    hid_t file_id;
    // Various global text pieces (generally JSON and not very useful).
    JsonParser *parser;
    gchar *data_text;
    gchar *experiment;
    gchar *info;
    gchar *version;
    // Keep track of how many data we have.
    gint img_id;
    gint vol_id;
} VeloxFile;

static gint        detect_file             (const GwyFileDetectInfo *fileinfo,
                                            gboolean only_name);
static GwyFile*    import_file             (const gchar *filename,
                                            GwyRunModeFlags mode,
                                            GError **error);
static herr_t      handle_displays_group   (hid_t loc_id,
                                            const char *name,
                                            const H5L_info_t *info,
                                            void *user_data);
static herr_t      handle_shared_properties(hid_t loc_id,
                                            const char *name,
                                            const H5L_info_t *info,
                                            void *user_data);
static GwyDict*    get_meta                (GHashTable *hash);
static gboolean    handle_data_group       (VeloxFile *efile,
                                            hid_t data_group,
                                            GError **error);
static herr_t      handle_text_dataset     (hid_t loc_id,
                                            const char *name,
                                            const H5L_info_t *info,
                                            void *user_data);
static void        expand_global_json_meta (VeloxFile *efile,
                                            GHashTable *hash);
static void        expand_json_meta        (GHashTable *hash,
                                            JsonParser *parsed,
                                            const gchar *str,
                                            const gchar *prefix);

void
gwyhdf5_register_veloxemd(void)
{
    gwy_file_func_register("veloxemd", N_("ThermoFisher Velox EMD HDF5 files (.emd)"),
                           detect_file, import_file, NULL, NULL);
}

static gint
detect_file(const GwyFileDetectInfo *fileinfo,
            gboolean only_name)
{
    static const gchar *groups[] = {
        "Data", "Features", "Operations", "Presentation", "SharedProperties", "Displays",
    };
    static const gchar *datasets[] = {
        "Experiment", "Info", "Version", "Thumbnail.jpg",
    };
    hid_t file_id;
    gint antiscore = 100;

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

    // This is a pretty strong indication of the file format. The rest is kind of meh.
    if (H5Lexists(file_id, "Application/Velox", H5P_DEFAULT) > 0) {
        gwy_debug("found Application/Velox");
        antiscore = antiscore/4;
    }

    for (guint i = 0; i < G_N_ELEMENTS(datasets) && antiscore > 0; i++) {
        if (H5Lexists(file_id, datasets[i], H5P_DEFAULT) > 0) {
            gwy_debug("found dataset %s", datasets[i]);
            antiscore = 3*antiscore/4;
        }
    }
    for (guint i = 0; i < G_N_ELEMENTS(groups) && antiscore > 0; i++) {
        if (H5Lexists(file_id, groups[i], H5P_DEFAULT) > 0) {
            gwy_debug("found group %s", groups[i]);
            antiscore = 3*antiscore/4;
        }
    }

    H5Fclose(file_id);

    return 100 - antiscore;
}

static GwyFile*
import_file(const gchar *filename,
            G_GNUC_UNUSED GwyRunModeFlags mode,
            GError **error)
{
    hid_t file_id, group;
    H5O_info_t infobuf;

    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);
    herr_t status = H5Oget_info(file_id, &infobuf, H5O_INFO_BASIC);
    if (!gwyhdf5_check_status(status, file_id, NULL, "H5Oget_info", error))
        return NULL;

    VeloxFile efile;
    gwy_clear1(efile);
    efile.filename = filename;
    efile.data = gwy_file_new_in_construction();
    efile.titles = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
    efile.parser = json_parser_new_immutable();
    efile.file_id = file_id;

    // Read global information first to make it available in data handling functions.
    if ((group = H5Gopen2(file_id, "Data/Text", H5P_DEFAULT)) >= 0) {
        gwy_debug("found Data/Text");
        H5Literate(group, H5_INDEX_NAME, H5_ITER_NATIVE, NULL, handle_text_dataset, &efile.data_text);
        H5Gclose(group);
    }
    handle_text_dataset(file_id, "Experiment", NULL, &efile.experiment);
    handle_text_dataset(file_id, "Info", NULL, &efile.info);
    handle_text_dataset(file_id, "Version", NULL, &efile.version);

    // FIXME: Is there anything useful in SharedProperties we would like to scan it for?
    if ((group = H5Gopen2(file_id, "SharedProperties", H5P_DEFAULT)) >= 0) {
        gwy_debug("found SharedProperties");
        H5Literate(group, H5_INDEX_NAME, H5_ITER_NATIVE, NULL, handle_shared_properties, &efile);
        H5Gclose(group);
    }
    if ((group = H5Gopen2(file_id, "Displays", H5P_DEFAULT)) >= 0) {
        gwy_debug("found Displays");
        H5Literate(group, H5_INDEX_NAME, H5_ITER_NATIVE, NULL, handle_displays_group, &efile);
        H5Gclose(group);
    }

    // Finally, read data.
    if ((group = H5Gopen2(file_id, "Data", H5P_DEFAULT)) < 0) {
        err_HDF5(error, "H5Gopen2", group);
        goto end;
    }
    gwy_debug("found Data");
    handle_data_group(&efile, group, error);
    H5Gclose(group);

    if (!gwy_dict_get_n_items(GWY_DICT(efile.data))) {
        err_NO_DATA(error);
        g_clear_object(&efile.data);
    }

end:
    g_clear_object(&efile.parser);
    g_hash_table_destroy(efile.titles);
    g_free(efile.data_text);
    g_free(efile.experiment);
    g_free(efile.info);
    g_free(efile.version);
    status = gwyhdf5_fclose(file_id);

    return efile.data;
}

static herr_t
handle_displays_group(hid_t loc_id,
                      const char *name,
                      G_GNUC_UNUSED const H5L_info_t *info,
                      void *user_data)
{
    VeloxFile *efile = (VeloxFile*)user_data;
    H5O_info_t infobuf;
    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) {
        return H5Literate_by_name(loc_id, name, H5_INDEX_NAME, H5_ITER_NATIVE,
                                  NULL, handle_displays_group, user_data, H5P_DEFAULT);
    }

    // EDS (and possibly other) labels:
    // /Displays/ImageDisplay/<ID1> contains in JSON:
    //      data: /SharedProperties/ImageSeriesDataReference/<ID2>
    //      title: <element name>
    // /SharedProperties/ImageSeriesDataReference/<ID2> contains in JSON
    //      dataPath: /Data/Image/<ID3>
    // /Data/Image/<ID3> is dataset which should be displayed with title <element name> (or generally this title)
    if (infobuf.type == H5O_TYPE_DATASET) {
        gchar *s = NULL;
        handle_text_dataset(loc_id, name, NULL, &s);
        if (s) {
            GHashTable *hash;
            gchar *dataref, *title, *datapath;

            hash = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
            expand_json_meta(hash, efile->parser, s, NULL);
            title = g_hash_table_lookup(hash, (gpointer)"title");
            dataref = g_hash_table_lookup(hash, (gpointer)"data");
            if (title && dataref) {
                gwy_debug("display data ref: %s is called %s", dataref, title);
                title = g_strdup(title);
                dataref = g_strdup(dataref);
                GWY_FREE(s);

                handle_text_dataset(efile->file_id, dataref, NULL, &s);
                if (s) {
                    g_hash_table_remove_all(hash);
                    expand_json_meta(hash, efile->parser, s, NULL);
                    datapath = g_hash_table_lookup(hash, (gpointer)"dataPath");
                    if (datapath) {
                        gchar *slash = strrchr(datapath, '/');
                        gwy_debug("display data ref: %s is data %s", dataref, datapath);
                        if (slash) {
                            g_hash_table_replace(efile->titles, g_strdup(slash+1), title);
                            title = NULL;
                        }
                    }
                }
                GWY_FREE(s);

                g_free(title);
                g_free(dataref);
            }
            g_hash_table_destroy(hash);
        }
        GWY_FREE(s);
    }

    return 0;
}

static herr_t
handle_shared_properties(hid_t loc_id,
                         const char *name,
                         G_GNUC_UNUSED const H5L_info_t *info,
                         void *user_data)
{
    //VeloxFile *efile = (VeloxFile*)user_data;
    H5O_info_t infobuf;
    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) {
        return H5Literate_by_name(loc_id, name, H5_INDEX_NAME, H5_ITER_NATIVE,
                                  NULL, handle_shared_properties, user_data, H5P_DEFAULT);
    }

    if (infobuf.type == H5O_TYPE_DATASET) {
        gchar *s = NULL;
        handle_text_dataset(loc_id, name, NULL, &s);
        g_free(s);
    }

    return 0;
}

static herr_t
handle_text_dataset(hid_t loc_id,
                    const char *name,
                    G_GNUC_UNUSED const H5L_info_t *info,
                    void *user_data)
{
    gchar **p = (gchar**)user_data;
    gchar *s = gwyhdf5_read_string_dataset(loc_id, name, NULL);
    if (!s)
        return 0;

    // This is some bloody JSON and may not contain anything useful. At best, it seems to contain some HTML formatted
    // report, the entire thing overzealously HTML-escaped (not JSON-escaped).
    g_strstrip(s);
    gwy_debug("read as string dataset %s as <%s>", name, s);
    g_assert(p);
    g_free(*p);
    *p = s;

    return 0;
}

static void
get_pixel_size(GHashTable *meta, GwyUnit *xunit, GwyUnit *yunit, gdouble *dx, gdouble *dy)
{
    const gchar *s;
    gint power10x = 0, power10y = 0;

    if ((s = g_hash_table_lookup(meta, "BinaryResult::PixelUnitY")))
        power10x = gwy_unit_set_from_string(yunit, s);
    if ((s = g_hash_table_lookup(meta, "BinaryResult::PixelUnitX")))
        power10y = gwy_unit_set_from_string(xunit, s);

    if ((s = g_hash_table_lookup(meta, "BinaryResult::PixelSize::width"))) {
        *dx = g_ascii_strtod(s, NULL);
        sanitise_real_size(dx, "pixel width");
        *dx *= gwy_exp10(power10x);
        gwy_debug("pixel width %g", *dx);
    }
    if ((s = g_hash_table_lookup(meta, "BinaryResult::PixelSize::height"))) {
        *dy = g_ascii_strtod(s, NULL);
        sanitise_real_size(dy, "pixel height");
        *dy *= gwy_exp10(power10y);
        gwy_debug("pixel height %g", *dy);
    }
}

static gchar*
read_and_untranspose_meta(hid_t group, gsize nlayers, const gchar *name, gsize *pnmeta, gsize *pmetalen)
{
    gchar *transposedmeta, *meta;
    gint tdims[2] = { -1, -1 };
    gint status;
    hid_t metaset;
    gsize nmeta, metalen, i, k;

    // This seems to be the only useful JSON with all the parameters. The rest of strings is basically junk.
    // For image stacks, the Metadata is (can be?) stored for each image, so it has the second dimension nlayers, not
    // 1.
    if ((metaset = gwyhdf5_open_and_check_dataset(group, "Metadata", 2, tdims, NULL)) < 0) {
        g_warning("Missing Metadata for data %s.", name);
        return NULL;
    }
    metalen = tdims[0];
    nmeta = tdims[1];
    if (nmeta != 1 && nmeta != nlayers) {
        g_warning("Metadata count %lu does not match the number of images in %s.", (gulong)nmeta, name);
        H5Dclose(metaset);
        return NULL;
    }

    // Of course when there are multiple strings, they are is stored in the entirely logical manner like the first
    // characters of all strings, then the second characters of all strings, etc. We need to transpose them exactly as
    // we do with images.
    //
    // The type is of course an integer, H5T_STD_U8LE, not a character type. Storing string using a character type?
    // Instanity! However, HDF5 strings are so weird that I can see the appeal.
    transposedmeta = g_new(gchar, metalen*nmeta + 1);
    status = H5Dread(metaset, H5T_NATIVE_UCHAR, H5S_ALL, H5S_ALL, H5P_DEFAULT, transposedmeta);
    H5Dclose(metaset);
    if (status < 0) {
        g_warning("Cannot read Metadata for %s.", name);
        GWY_FREE(transposedmeta);
        return NULL;
    }
    gwy_debug("read Metadata of length %lux%lu (%.1024s)", (gulong)metalen, (gulong)nmeta, transposedmeta);
    *pmetalen = metalen;
    *pnmeta = nmeta;

    if (nmeta < 2)
        return transposedmeta;

    meta = g_new(gchar, (metalen + 1)*nmeta);
    for (i = 0; i < nmeta; i++) {
        for (k = 0; k < metalen; k++)
            meta[(metalen + 1)*i + k] = transposedmeta[nmeta*k + i];
        meta[(metalen + 1)*i + metalen] = '\0';
    }
    g_free(transposedmeta);

    return meta;
}

// FIXME: We need to pass a GError, probably in efile. For some HDF5 errors, like failed H5Dread(), we should abort
// instead skipping data.
static herr_t
handle_image_subgroup(hid_t loc_id,
                      const char *name,
                      G_GNUC_UNUSED const H5L_info_t *info,
                      void *user_data)
{
    VeloxFile *efile = (VeloxFile*)user_data;
    gint ddims[3] = { -1, -1, -1 };
    hid_t group, dataset = -1, dataset_type = -1;
    gsize xres, yres, nlayers, totalsize, metalen, nmeta = 0;
    H5T_class_t type_class;
    gchar *jsonmeta = NULL;
    GwyFile *data = efile->data;
    GHashTable *meta = NULL;
    gint status;

    if ((group = H5Gopen2(loc_id, name, H5P_DEFAULT)) < 0)
        return 0;

    gwy_debug("%s", name);

    if ((dataset = gwyhdf5_open_and_check_dataset(group, "Data", G_N_ELEMENTS(ddims), ddims, NULL)) < 0)
        goto end;
    // This should not really happen here. HDF5 should not give us dimension -5.
    if (err_DIMENSION(NULL, ddims[0]) || err_DIMENSION(NULL, ddims[1])) {
        g_warning("Invalid dimensions %dx%d.", ddims[0], ddims[1]);
        goto end;
    }
    dataset_type = H5Dget_type(dataset);
    type_class = H5Tget_class(dataset_type);
    gwy_debug("image dataset Data in %s with dims (%d, %d, %d) and class %d",
              name, ddims[0], ddims[1], ddims[2], type_class);
    // FIXME: What is the order for X and Y in ddims[]?
    yres = ddims[0];
    xres = ddims[1];
    nlayers = ddims[2];
    totalsize = xres*yres*nlayers;

    if (!(jsonmeta = read_and_untranspose_meta(group, nlayers, name, &nmeta, &metalen)))
        goto end;
    meta = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
    expand_global_json_meta(efile, meta);
    expand_json_meta(meta, efile->parser, jsonmeta, NULL);

    // TODO: If nlayers > 1 (or nlayers >> 1), create volume data.
    if (type_class == H5T_INTEGER || type_class == H5T_FLOAT) {
        if (nlayers <= 3) {
            gdouble *transposeddata = g_new(gdouble, totalsize);
            if ((status = H5Dread(dataset, H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, H5P_DEFAULT, transposeddata)) < 0) {
                g_warning("Cannot read Data for %s.", name);
                g_free(transposeddata);
                goto end;
            }

            for (gsize k = 0; k < nlayers; k++) {
                GwyField *field = gwy_field_new(xres, yres, xres, yres, FALSE);
                gdouble *d = gwy_field_get_data(field);
                const gchar *title;
                gdouble dx, dy;

                gwy_debug("creating field %zdx%zd id=%d", xres, yres, efile->img_id);
                for (gsize i = 0; i < yres; i++) {
                    for (gsize j = 0; j < xres; j++)
                        d[i*xres + j] = transposeddata[(i*xres + j)*nlayers + k];
                }
                if (k > 1 && nmeta > 1) {
                    g_hash_table_remove_all(meta);
                    expand_global_json_meta(efile, meta);
                    expand_json_meta(meta, efile->parser, jsonmeta + (metalen + 1)*k, NULL);
                }

                get_pixel_size(meta, gwy_field_get_unit_xy(field), gwy_field_get_unit_xy(field), &dx, &dy);
                gwy_field_set_xreal(field, xres*dx);
                gwy_field_set_yreal(field, yres*dy);

                gwy_file_pass_image(data, efile->img_id, field);
                if (!(title = g_hash_table_lookup(efile->titles, name)))
                    title = "TEM";
                gwy_file_set_title(data, GWY_FILE_IMAGE, efile->img_id, title, FALSE);
                gwy_file_pass_meta(data, GWY_FILE_IMAGE, efile->img_id, get_meta(meta));
                gwy_log_add_import(data, GWY_FILE_IMAGE, efile->img_id, NULL, efile->filename);
                efile->img_id++;
            }
            g_free(transposeddata);
        }
        else {
            GwyBrick *brick, *transposedbrick = gwy_brick_new(nlayers, xres, yres, 1.0, 1.0, 1.0, FALSE);
            gdouble *transposeddata = gwy_brick_get_data(transposedbrick);
            gdouble dx, dy;

            if ((status = H5Dread(dataset, H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, H5P_DEFAULT, transposeddata)) < 0) {
                g_warning("Cannot read Data for %s.", name);
                g_object_unref(transposedbrick);
                goto end;
            }

            gwy_debug("creating brick %zdx%zdx%zd id=%d", xres, yres, nlayers, efile->img_id);
            brick = gwy_brick_new(1, 1, 1, 1.0, 1.0, 1.0, FALSE);
            gwy_brick_transpose(transposedbrick, brick, GWY_BRICK_TRANSPOSE_ZYX, FALSE, FALSE, FALSE);
            g_object_unref(transposedbrick);

            get_pixel_size(meta, gwy_brick_get_unit_x(brick), gwy_brick_get_unit_y(brick), &dx, &dy);
            gwy_brick_set_xreal(brick, xres*dx);
            gwy_brick_set_yreal(brick, yres*dy);

            gwy_file_pass_volume(data, efile->vol_id, brick);
            gwy_file_set_title(data, GWY_FILE_VOLUME, efile->vol_id, "TEM image stack", FALSE);
            gwy_file_pass_meta(data, GWY_FILE_VOLUME, efile->vol_id, get_meta(meta));
            gwy_log_add_import(data, GWY_FILE_VOLUME, efile->vol_id, NULL, efile->filename);
            efile->vol_id++;
        }
    }
    // FIXME: FFT results are stored as an half-image of compound type
    // DATATYPE  H5T_COMPOUND {
    //        H5T_IEEE_F32LE "realFloatHalfEven";
    //        H5T_IEEE_F32LE "imagFloatHalfEven";
    //    }

end:
    g_free(jsonmeta);
    if (dataset > 0)
        H5Dclose(dataset);
    if (dataset_type > 0)
        H5Dclose(dataset_type);
    if (meta)
        g_hash_table_destroy(meta);
    H5Gclose(group);
    return 0;
}

static gboolean
handle_data_group(VeloxFile *efile, hid_t data_group, GError **error)
{
    hid_t image_group;

    if ((image_group = H5Gopen2(data_group, "Image", H5P_DEFAULT)) < 0) {
        err_HDF5(error, "H5Gopen2", image_group);
        return FALSE;
    }
    gwy_debug("found Image");
    H5Literate(image_group, H5_INDEX_NAME, H5_ITER_NATIVE, NULL, handle_image_subgroup, efile);
    H5Gclose(image_group);

    // TODO: There is also Spectrum. It contains a bunch of 1D data (or semi-1D; they seem to have dimensions (N, 1),
    // so maybe the second can be > 1), each with its own Metadata.

    // TODO: There is also SpectrumImage. It also contains semi-1D data, each with its own Metadata. The size is large
    // and not a multiple of anything nice. It may be a Lawn-like object? It also comes with SpectrumImageSettings,
    // which is a variable-length text dataset. Maybe it contains something useful?

    // TODO: There is also SpectrumStream. It is contains even larger 1D data of a size that does not factor nicely.
    // It comes with a FrameLocationTable array and AcquisitionSettings, which is a string.

    return TRUE;
}

static void
add_simple_meta(gpointer hkey, gpointer hdata, gpointer user_data)
{
    gchar *key = (gchar*)hkey, *value = (gchar*)hdata;
    GwyDict *meta = (GwyDict*)user_data;
    guint len = strlen(value);

    if (!len || len > 250)
        return;

    gwy_dict_set_string(meta, g_quark_from_string(key), value);
}

static GwyDict*
get_meta(GHashTable *hash)
{
    GwyDict *meta = gwy_dict_new();
    g_hash_table_foreach(hash, add_simple_meta, meta);
    return meta;
}

/* Expand JSON metadata into individual fields. Silently ignore any errors. */
static gboolean
format_atomic_json_meta(JsonNode *node, GString *str)
{
    GType type = json_node_get_value_type(node);

    if (type == G_TYPE_STRING)
        g_string_append(str, json_node_get_string(node));
    else if (type == G_TYPE_INT64)
        g_string_append_printf(str, "%" G_GINT64_FORMAT, json_node_get_int(node));
    else if (type == G_TYPE_DOUBLE)
        g_string_append_printf(str, "%.9g", json_node_get_double(node));
    else if (type == G_TYPE_BOOLEAN)
        g_string_assign(str, json_node_get_boolean(node) ? "True" : "False");
    else
        return FALSE;

    return TRUE;
}

static void
expand_json_object(JsonObject *object, GHashTable *hash, GString *path, GString *buf)
{
    g_string_append(path, "::");
    guint pfxlen = path->len;

    JsonObjectIter iter;
    json_object_iter_init(&iter, object);

    const gchar *name;
    JsonNode *node;
    while (json_object_iter_next(&iter, &name, &node)) {
        g_string_append(path, name);
        g_string_truncate(buf, 0);
        if (format_atomic_json_meta(node, buf)) {
            g_hash_table_replace(hash, g_strdup(path->str), g_strdup(buf->str));
        }
        else if (JSON_NODE_HOLDS_ARRAY(node)) {
            JsonArray *array = json_node_get_array(node);
            guint n = json_array_get_length(array);
            for (guint i = 0; i < n; i++) {
                JsonNode *item_node = json_array_get_element(array, i);
                if (format_atomic_json_meta(item_node, buf))
                    g_string_append(buf, "; ");
            }
            if (buf->len >= 2)
                g_string_truncate(buf, buf->len-2);
            g_hash_table_replace(hash, g_strdup(path->str), g_strdup(buf->str));
        }
        else if (JSON_NODE_HOLDS_OBJECT(node))
            expand_json_object(json_node_get_object(node), hash, path, buf);
        else {
            gwy_debug("unhandled meta %s of type %d", name, JSON_NODE_TYPE(member));
        }
        g_string_truncate(path, pfxlen);
    }
}

static void
expand_global_json_meta(VeloxFile *efile, GHashTable *hash)
{
    if (efile->info)
        expand_json_meta(hash, efile->parser, efile->info, "Info");
    if (efile->version)
        expand_json_meta(hash, efile->parser, efile->version, "Version");
}

static void
expand_json_meta(GHashTable *hash, JsonParser *parser, const gchar *str, const gchar *prefix)
{
    if (!json_parser_load_from_data(parser, str, -1, NULL))
        return;

    JsonNode *root_node = json_parser_get_root(parser);
    if (!JSON_NODE_HOLDS_OBJECT(root_node))
        return;

    GString *path = g_string_new(prefix), *buf = g_string_new(NULL);
    expand_json_object(json_node_get_object(root_node), hash, path, buf);
    g_string_free(buf, TRUE);
    g_string_free(path, 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 : */
