/*
 *  $Id: fusionscope.c 30114 2026-06-22 11:52:09Z yeti-dn $
 *  Copyright (C) 2025 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]
 * FusionScope HDF5 experiment files
 * .fsexp
 * Read
 **/

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

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

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

/* The file group structure goes like this
 *
 * PipelineData
 * +--1_Region_1
 *    +--SourceFrames
 *       +--1_Frame_3_Channel_0
 *          +--Processed
 *          +--Raw
 *       +--2_Frame_3_Channel_1
 *          +--Processed
 *          +--Raw
 * +--2_Region_2
 *    +--SourceFrames
 *       +--4_Frame_1_Channel_0
 *          +--Processed
 *          +--Raw
 *       +--5_Frame_1_Channel_1
 *          +--Processed
 *          +--Raw
 * +--3_Region_3
 *    +--SourceFrames
 *       +--6_Frame_1_Channel_0
 *          +--Processed
 *          +--Raw
 *       +--7_Frame_1_Channel_1
 *          +--Processed
 *          +--Raw
 */

typedef struct {
    GwyField *field;
    GwyNield *mask;
    GwyDict *meta;
    gchar *title;
    gchar *id_prefix;
    gchar *metadata_prefix;
    gint region_id;
    gint frame_id;
    gint data_id;
} FScopeImage;

typedef struct {
    gint region_id;
    gint frame_id;
    gint data_id;
    const gchar *filename;
    const gchar *region;
    const gchar *rawprocessed;
    const gchar *frames;
    gchar *region_clean;
    gchar *frame_clean;
    JsonParser *parser;
    GString *path;
    GString *buf;
    GHashTable *region_meta;
    GHashTable *frame_meta;
    GHashTable *experiment_meta;
    GArray *images;
} FScopeFile;

static gint     detect_file        (const GwyFileDetectInfo *fileinfo,
                                    gboolean only_name);
static GwyFile* load_file          (const gchar *filename,
                                    GwyRunModeFlags mode,
                                    GError **error);
static herr_t   region_scan        (hid_t loc_id,
                                    const char *name,
                                    const H5L_info_t *info,
                                    void *user_data);
static herr_t   frames_scan        (hid_t loc_id,
                                    const char *name,
                                    const H5L_info_t *info,
                                    void *user_data);
static herr_t   data_scan          (hid_t loc_id,
                                    const char *name,
                                    const H5L_info_t *info,
                                    void *user_data);
static herr_t   create_image       (FScopeFile *fsfile,
                                    hid_t dataset,
                                    const gint *dims);
static gint     compare_images     (gconstpointer pa,
                                    gconstpointer pb);
static void     set_real_dimensions(GwyField *field,
                                    FScopeFile *fsfile);
static void     create_image_meta  (FScopeFile *fsfile,
                                    FScopeImage *image);
static gboolean fill_hash_from_json(FScopeFile *fsfile,
                                    hid_t loc_id,
                                    const gchar *name,
                                    GHashTable *hash);
static void     fscope_file_init   (FScopeFile *fsfile,
                                    const gchar *filename);
static void     fscope_file_free   (FScopeFile *fsfile);

void
gwyhdf5_register_fusionscope(void)
{
    gwy_file_func_register("fusionscope",
                           N_("FusionScope HDF5 files (.fsexp)"),
                           &detect_file, &load_file, NULL, NULL);
}

static gint
detect_file(const GwyFileDetectInfo *fileinfo, gboolean only_name)
{
    static const gchar *attributes[] = {
        "APPLICATION_VERSION", "EXPERIMENT_FILE_VERSION", "EXPERIMENT_INFO",
    };

    hid_t file_id, group_id, attr_id;
    gint score = 0;
    gint rect_dims = 4;
    guint i;

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

    if (H5Lexists(file_id, "PipelineData", H5P_DEFAULT) <= 0) {
        H5Fclose(file_id);
        return 0;
    }

    /* The files do not have any of the typical top-level attributes we then use to recognise the data formats.
     * Look for some groups. They should be always present, even if they are empty. */
    if ((group_id = H5Gopen2(file_id, "PipelineData", H5P_DEFAULT)) > 0) {
        score += 60;
        H5Gclose(group_id);

        for (i = 0; i < G_N_ELEMENTS(attributes); i++) {
            if ((attr_id = gwyhdf5_open_and_check_attr(file_id, "/", attributes[i], H5T_STRING, 1, NULL, NULL))) {
                score += 10;
                H5Aclose(attr_id);
            }
        }
        if ((attr_id = gwyhdf5_open_and_check_attr(file_id, "/", "OVERVIEW_RECT", H5T_FLOAT, 1, &rect_dims, NULL))) {
            score += 10;
            H5Aclose(attr_id);
        }
    }

    H5Fclose(file_id);

    return score;
}

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

    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;

    fscope_file_init(&fsfile, filename);
    if ((pipeline_data_group = H5Gopen2(file_id, "PipelineData", H5P_DEFAULT)) < 0) {
        err_HDF5(error, "H5Gopen2", pipeline_data_group);
        goto end;
    }

    fsfile.parser = json_parser_new_immutable();
    g_string_assign(fsfile.path, "experiement_info");
    fill_hash_from_json(&fsfile, file_id, "EXPERIMENT_INFO", fsfile.experiment_meta);

    status = H5Literate(pipeline_data_group, H5_INDEX_NAME, H5_ITER_NATIVE, NULL, region_scan, &fsfile);
    if (!gwyhdf5_check_status(status, file_id, NULL, "H5Literate", error))
        goto end;

    if (!fsfile.images->len) {
        err_NO_DATA(error);
        goto end;
    }

    file = gwy_file_new_in_construction();
    g_array_sort(fsfile.images, compare_images);
    for (i = 0; i < fsfile.images->len; i++) {
        FScopeImage *image = &g_array_index(fsfile.images, FScopeImage, i);

        gwy_file_set_image(file, i, image->field);
        if (image->mask)
            gwy_file_set_image_mask(file, i, image->mask);
        if (image->meta) {
            gwy_file_pass_meta(file, GWY_FILE_IMAGE, i, image->meta);
            image->meta = NULL;
        }
        gwy_file_pass_title(file, GWY_FILE_IMAGE, i, image->title);
        gwy_log_add_import(file, GWY_FILE_IMAGE, i, NULL, filename);
        image->title = NULL;
    }

end:
    fscope_file_free(&fsfile);

    return file;
}

static herr_t
region_scan(hid_t loc_id,
            const char *name,
            G_GNUC_UNUSED const H5L_info_t *info,
            void *user_data)
{
    static const gchar *frame_types[] = { "SourceFrames", "ResultFrames" };
    FScopeFile *fsfile = (FScopeFile*)user_data;
    H5O_info_t infobuf;
    hid_t group, frames;
    herr_t status;
    guint i;

    gwy_debug("  %s", name);

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

    fsfile->region = name;
    if (sscanf(fsfile->region, "%u_Region_%u", &i, &fsfile->region_id) == 2)
        fsfile->region_clean = g_strdup_printf("Region %u", fsfile->region_id);
    else if (gwy_strequal(fsfile->region, "OverviewRegion")) {
        fsfile->region_clean = g_strdup("Overview Region");
        /* Put it first. */
        fsfile->region_id = -1;
    }
    else {
        /* Put it last. */
        fsfile->region_clean = g_strdup(fsfile->region);
        fsfile->region_id = G_MAXINT;
    }

    if (infobuf.type == H5O_TYPE_GROUP && (group = H5Gopen(loc_id, name, H5P_DEFAULT)) >= 0) {
        g_string_assign(fsfile->path, "region_info");
        fill_hash_from_json(fsfile, group, "REGION_INFO", fsfile->region_meta);

        for (i = 0; i < G_N_ELEMENTS(frame_types); i++) {
            frames = H5Gopen(group, frame_types[i], H5P_DEFAULT);
            if (frames >= 0) {
                gwy_debug("  found %s", frame_types[i]);
                status = H5Literate(frames, H5_INDEX_NAME, H5_ITER_NATIVE, NULL, frames_scan, fsfile);
                H5Gclose(frames);
            }
        }

        status = H5Gclose(group);
    }

    GWY_FREE(fsfile->region_clean);
    fsfile->region = NULL;
    g_hash_table_remove_all(fsfile->frame_meta);

    return status;
}

static herr_t
frames_scan(hid_t loc_id,
            const char *name,
            G_GNUC_UNUSED const H5L_info_t *info,
            void *user_data)
{
    static const gchar *data_types[] = { "Raw", "Processed" };
    FScopeFile *fsfile = (FScopeFile*)user_data;
    H5O_info_t infobuf;
    hid_t group, data;
    herr_t status;
    guint i;

    gwy_debug("    %s", name);

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

    fsfile->frames = name;
    if (sscanf(fsfile->frames, "%u_Frame_%u_Channel_%u", &i, &fsfile->frame_id, &i) == 3)
        fsfile->frame_clean = g_strdup_printf("Frame %u", fsfile->frame_id);
    else if (sscanf(fsfile->frames, "%u_OverviewFrame", &fsfile->frame_id) == 1) {
        fsfile->frame_clean = g_strdup("Overview Frame");
        /* Put it first. */
        fsfile->frame_id = -1;
    }
    else {
        /* Put it last. */
        fsfile->frame_clean = g_strdup(fsfile->frames);
        fsfile->frame_id = G_MAXINT;
    }

    if (infobuf.type == H5O_TYPE_GROUP && (group = H5Gopen(loc_id, name, H5P_DEFAULT)) >= 0) {
        gboolean found = TRUE;
        g_string_assign(fsfile->path, "frame_info");
        fill_hash_from_json(fsfile, group, "FRAME_INFO", fsfile->frame_meta);
        /* Is not present in overview frames, and maybe elsewhere. */

        for (i = 0; found; i++) {
           gchar *key = g_strdup_printf("IMAGE_LAYERS_JSON_%u", i);
           g_string_printf(fsfile->path, "%u", i);
           found = fill_hash_from_json(fsfile, group, key, fsfile->frame_meta);
           g_free(key);
        }

        for (i = 0; i < G_N_ELEMENTS(data_types); i++) {
            data = H5Gopen(group, data_types[i], H5P_DEFAULT);
            if (data >= 0) {
                gwy_debug("  found %s", data_types[i]);
                fsfile->rawprocessed = data_types[i];
                status = H5Literate(data, H5_INDEX_NAME, H5_ITER_NATIVE, NULL, data_scan, fsfile);
                fsfile->rawprocessed = NULL;
                H5Gclose(data);
            }
        }

        status = H5Gclose(group);
    }

    GWY_FREE(fsfile->frame_clean);
    fsfile->frames = NULL;
    g_hash_table_remove_all(fsfile->frame_meta);

    return status;
}

static herr_t
data_scan(hid_t loc_id,
          const char *name,
          G_GNUC_UNUSED const H5L_info_t *info,
          void *user_data)
{
    FScopeFile *fsfile = (FScopeFile*)user_data;
    H5O_info_t infobuf;
    hid_t 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 (sscanf(name, "DataIndex_%u", &fsfile->data_id) != 1)
        fsfile->data_id = G_MAXINT;

    if (infobuf.type == H5O_TYPE_DATASET) {
        gint dims[2] = { -1, -1 };

        /* Just ignore bad datasets. */
        if ((dataset = gwyhdf5_open_and_check_dataset(loc_id, name, 2, dims, NULL)) < 0) {
            return status;
        }

        gwy_debug("      dims %u x %u", dims[0], dims[1]);
        if (err_DIMENSION(NULL, dims[0]) || err_DIMENSION(NULL, dims[1])) {
            H5Dclose(dataset);
            return status;
        }

        status = create_image(fsfile, dataset, dims);

        H5Dclose(dataset);
    }

    return status;
}

static herr_t
create_image(FScopeFile *fsfile, hid_t dataset, const gint *dims)
{
    FScopeImage image;
    gchar *channel_name = NULL;
    const gchar *chname = "Unknown";
    GwyField *field;
    GwyUnit *unit;
    herr_t status;
    const gchar *sval;
    gchar *lc_datatype;
    gint power10 = 0;

    gwy_clear1(image);
    field = gwy_field_new(dims[0], dims[1], 1.0, 1.0, FALSE);
    status = H5Dread(dataset, H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, H5P_DEFAULT, gwy_field_get_data(field));
    if (status < 0) {
        g_object_unref(field);
        return status;
    }

    unit = gwy_field_get_unit_z(field);
    if (fsfile->data_id >= 0) {
        /* NB: Always use raw data units, even for processed. They do generally not store units for processed data.
         * Sometimes they are there but… */
        g_string_printf(fsfile->buf, "frame_info::meta_data_raw_%u::datachannel_units", fsfile->data_id);
        if ((sval = g_hash_table_lookup(fsfile->frame_meta, fsfile->buf->str))) {
            power10 = gwy_unit_set_from_string(unit, sval);
            if (power10)
                gwy_field_multiply(field, gwy_exp10(power10));
        }
    }

    /* There is regionUnits in REGION_INFO but it is always empty. */
    gwy_unit_set_from_string(gwy_field_get_unit_xy(field), "m");
    set_real_dimensions(field, fsfile);

    image.field = field;
    image.mask = gwy_mask_nans_in_field(field, TRUE);
    if (gwyhdf5_get_str_attr(dataset, ".", "SUBCHANNEL_NAME", &channel_name, NULL))
        chname = channel_name;
    image.title = g_strconcat(fsfile->region_clean, ", ",
                              fsfile->frame_clean, ", ",
                              chname, " ",
                              fsfile->rawprocessed, NULL);
    if (channel_name)
        H5free_memory(channel_name);

    image.region_id = fsfile->region_id;
    image.frame_id = fsfile->frame_id;
    image.data_id = fsfile->data_id;
    image.id_prefix = g_strdup_printf("%d::", image.data_id);
    lc_datatype = g_ascii_strdown(fsfile->rawprocessed, -1);
    image.metadata_prefix = g_strdup_printf("frame_info::meta_data_%s_%d::", lc_datatype, image.data_id);
    g_free(lc_datatype);
    create_image_meta(fsfile, &image);
    g_array_append_val(fsfile->images, image);

    return status;
}

static void
set_real_dimensions(GwyField *field, FScopeFile *fsfile)
{
    const gchar *sval;
    gdouble xstart = 0.0, xend = 1.0, ystart = 0.0, yend = 1.0;
    gdouble xreal = 1.0, yreal = 1.0;
    gboolean have_start_end = TRUE;

    if ((sval = g_hash_table_lookup(fsfile->frame_meta, "frame_info::xStart")))
        xstart = g_ascii_strtod(sval, NULL);
    else
        have_start_end = FALSE;

    if ((sval = g_hash_table_lookup(fsfile->frame_meta, "frame_info::xEnd")))
        xend = g_ascii_strtod(sval, NULL);
    else
        have_start_end = FALSE;

    if ((sval = g_hash_table_lookup(fsfile->frame_meta, "frame_info::yStart")))
        ystart = g_ascii_strtod(sval, NULL);
    else
        have_start_end = FALSE;

    if ((sval = g_hash_table_lookup(fsfile->frame_meta, "frame_info::yEnd")))
        yend = g_ascii_strtod(sval, NULL);
    else
        have_start_end = FALSE;

    if ((sval = g_hash_table_lookup(fsfile->region_meta, "region_info::regionWidth"))) {
        xreal = g_ascii_strtod(sval, NULL);
        sanitise_real_size(&xreal, "width");
    }
    if ((sval = g_hash_table_lookup(fsfile->region_meta, "region_info::regionHeight"))) {
        yreal = g_ascii_strtod(sval, NULL);
        sanitise_real_size(&yreal, "height");
    }

    gwy_debug("have_start_end: %d", have_start_end);
    gwy_debug("x: %g (%g..%g = %g)", xreal, xstart, xend, xend-xstart);
    gwy_debug("y: %g (%g..%g = %g)", yreal, ystart, yend, yend-ystart);
    if (!have_start_end) {
        xstart = ystart = 0.0;
        xend = xreal;
        yend = yreal;
    }
    gwy_field_set_xreal(field, (xend - xstart)*1e-6);
    gwy_field_set_yreal(field, (yend - ystart)*1e-6);
    gwy_field_set_xoffset(field, xstart*1e-6);
    gwy_field_set_yoffset(field, ystart*1e-6);
}

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

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

    gwy_dict_set_string(image->meta, g_quark_from_string(key), value);
}

static void
add_meta_with_data_id(gpointer hkey, gpointer hdata, gpointer user_data)
{
    gchar *key = (gchar*)hkey, *value = (gchar*)hdata;
    FScopeImage *image = (FScopeImage*)user_data;
    guint len = strlen(value);
    gint data_id;

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

    /* If the key looks like 123::blah::blah, ignore it when the number does not match and include it without the
     * prefix when it does. */
    if (g_ascii_isdigit(key[0]) && sscanf(key, "%d::", &data_id) == 1) {
        if (!g_str_has_prefix(key, image->id_prefix))
            return;
        key += strlen(image->id_prefix);
    }

    /* If the key looks like frame_info::meta_data_{raw,processed}_123::blah::blah, ignore it when the number and
     * type do not match and include it without the prefix when they do. */
    if (g_str_has_prefix(key, "frame_info::meta_data_")) {
        if (!g_str_has_prefix(key, image->metadata_prefix))
            return;
        key += strlen(image->metadata_prefix);
    }

    gwy_dict_set_string(image->meta, g_quark_from_string(key), value);
}

static void
create_image_meta(FScopeFile *fsfile, FScopeImage *image)
{
    image->meta = gwy_dict_new_in_construction();

    /* TODO: There are also a couple top level attributes and attributes scattered in the HDF5 groups. Most do not
     * seem very useful, but some may be. */
    g_hash_table_foreach(fsfile->experiment_meta, add_simple_meta, image);
    g_hash_table_foreach(fsfile->region_meta, add_simple_meta, image);
    g_hash_table_foreach(fsfile->frame_meta, add_meta_with_data_id, image);
}

static gint
compare_images(gconstpointer pa, gconstpointer pb)
{
    const FScopeImage *a = (const FScopeImage*)pa, *b = (const FScopeImage*)pb;

    if (a->region_id < b->region_id)
        return -1;
    if (a->region_id > b->region_id)
        return 1;

    if (a->frame_id < b->frame_id)
        return -1;
    if (a->frame_id > b->frame_id)
        return 1;

    if (a->data_id < b->data_id)
        return -1;
    if (a->data_id > b->data_id)
        return 1;

    return 0;
}

static void
fscope_file_init(FScopeFile *fsfile, const gchar *filename)
{
    gwy_clear(fsfile, 1);
    fsfile->filename = filename;
    fsfile->path = g_string_new(NULL);
    fsfile->buf = g_string_new(NULL);
    fsfile->experiment_meta = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
    fsfile->region_meta = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
    fsfile->frame_meta = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
    fsfile->images = g_array_new(FALSE, FALSE, sizeof(FScopeImage));
}

static void
fscope_file_free(FScopeFile *fsfile)
{
    guint i;

    g_clear_object(&fsfile->parser);
    g_string_free(fsfile->buf, TRUE);
    g_string_free(fsfile->path, TRUE);
    g_hash_table_destroy(fsfile->frame_meta);
    g_hash_table_destroy(fsfile->region_meta);
    g_hash_table_destroy(fsfile->experiment_meta);
    for (i = 0; i < fsfile->images->len; i++) {
        FScopeImage *image = &g_array_index(fsfile->images, FScopeImage, i);
        g_clear_object(&image->field);
        g_clear_object(&image->mask);
        g_clear_object(&image->meta);
        g_free(image->title);
        g_free(image->id_prefix);
        g_free(image->metadata_prefix);
    }
    g_array_free(fsfile->images, TRUE);
}

/* 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_json_meta(GHashTable *hash, JsonParser *parser, const gchar *str, GString *path, GString *buf)
{
    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;

    expand_json_object(json_node_get_object(root_node), hash, path, buf);
}

/* There are quoted JSON strings inside the JSON! Yay! */
static void
gather_embedded_jsons(gpointer hkey, gpointer hdata, gpointer user_data)
{
    gchar *key = (gchar*)hkey, *value = (gchar*)hdata;
    GPtrArray *array = (GPtrArray*)user_data;

    /* Do not gather pipeline_string. It contains some JSON, but it does not seem useful and it seems to be
     * overquoted, requiring one more level of unquoting to parse (or something like that). */
    if (g_str_has_suffix(hkey, "JSON") && strchr(value, '{'))
        g_ptr_array_add(array, key);
}

static gboolean
fill_hash_from_json(FScopeFile *fsfile, hid_t loc_id, const gchar *name, GHashTable *hash)
{
    gchar *strattr = NULL;
    GPtrArray *embedded_jsons;
    guint i;

    if (!gwyhdf5_get_str_attr(loc_id, ".", name, &strattr, NULL))
        return FALSE;

    expand_json_meta(hash, fsfile->parser, strattr, fsfile->path, fsfile->buf);
    H5free_memory(strattr);

    embedded_jsons = g_ptr_array_new();
    g_hash_table_foreach(hash, gather_embedded_jsons, embedded_jsons);
    for (i = 0; i < embedded_jsons->len; i++) {
        gchar *key = g_ptr_array_index(embedded_jsons, i);
        gchar *sep, *jsonstr = g_hash_table_lookup(hash, key);

        gwy_debug("expanding embedded json %s", key);
        g_string_assign(fsfile->path, key);
        sep = g_strrstr_len(fsfile->path->str, fsfile->path->len, "::");
        if (sep)
            g_string_truncate(fsfile->path, sep - fsfile->path->str);
        expand_json_meta(hash, fsfile->parser, jsonstr, fsfile->path, fsfile->buf);
        g_hash_table_remove(hash, key);
    }
    g_ptr_array_free(embedded_jsons, TRUE);

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