/*
 *  $Id: nhffile.c 29720 2026-04-02 14:16:19Z yeti-dn $
 *  Copyright (C) 2024-2025 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]
 * Nanosurf NHF
 * .nhf
 * Read
 **/

/**
 * [FILE-MAGIC-FREEDESKTOP]
 * <mime-type type="application/x-nanosurf-nhf-hdf5-spm">
 *   <comment>Nanosurf NHF HDF5 SPM data</comment>
 *   <glob pattern="*.nhf"/>
 *   <glob pattern="*.NHF"/>
 * </mime-type>
 **/

/* TODO: Implement full calibrations. It is done by
 * 1. Taking "signal_id" and "signal_selected" for the dataset. These are not random strings, they are basically
 *    enums.
 * 2. Find a path through the calibration graph which connects them (hoping it is unambiguous), as each calibration
 *    has "signal_sink_id" and "signal_source_id". The chain may contain a whole bunch of them even for simple data.
 *    In principle calibrations are directional, so we must traverse the graph correctly.
 * 3. Go through the chain and apply the calibrations. Probably use linear composition for linear calibrations as
 *    there are often many of them and many of them are even identities.
 *
 * The simple calibration (we are currently using) should correspond to the selected signal.
 */

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

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

/* FAIL and NEUTRAL are also returnable as herr_t; SUCCESS – maybe, because non-zero values usually actually mean
 * something. We could use htri_t, but its semantics is different (yes, no, failure). */
typedef enum {
    NHF_FAIL    = -1,
    NHF_NEUTRAL = 0,
    NHF_SUCCESS = 1,
} NHFStatus;

/* The group type is a string an can be anything. But if it is anything we do not know what to do with the stuff
 * inside. So there is a finite list of group types we care about. */
typedef enum {
    NHF_GROUP_UNKNOWN          = -1,
    NHF_GROUP_CALIBRATION      = 0,
    NHF_GROUP_IMAGE_LINE_BASED = 1,
    NHF_GROUP_SPECTROSCOPY     = 2,
    NHF_GROUP_WAVEMODE_NMA     = 3,
} NHFGroupType;

typedef enum {
    NHF_MAPPING_UNSUPPORTED = -1,
    NHF_MAPPING_NONE        = 0,
    NHF_MAPPING_LINEAR      = 1,
    NHF_MAPPING_EXPONENTIAL = 2,
    NHF_MAPPING_LOGARITHMIC = 3,
} NHFMappingType;

/* Things which have an UUID and can be referenced from other places in the file. */
typedef enum {
    NHF_TARGET_GROUP       = 0,
    NHF_TARGET_CALIBRATION = 1,
    NHF_TARGET_BLOCK_SIZE  = 2,
} NHFTargetType;

typedef enum {
    NHF_EXTRACTING_NOTHING = 0,
    NHF_EXTRACTING_IMAGES  = 1,
    NHF_EXTRACTING_VOLUMES = 2,
    NHF_EXTRACTING_SPECTRA = 3,
} NHFExtractingWhat;

typedef struct {
    NHFTargetType type;
    gint group;
    gint subgroup;
    gint dataset;
} NHFTarget;

typedef struct {
    gdouble *data;
    GwyUnit *unit;
    gchar *name;
} NHFCurveData;

typedef struct {
    const gchar *filename;
    GwyHDF5File *ghfile;
    GHashTable *targets;
    GString *path;
    GwyFile *file;
    hid_t file_id;
    /* These are not global; they reflect the current state as we traverse the file. */
    NHFGroupType group_type;
    guint group;
    guint subgroup;
    guint dataset;
    gint image_id;
    gint volume_id;
    gint graph_id;
    gint cmap_id;
    gint xres;
    gint yres;
    gdouble xreal;
    gdouble yreal;
    gchar *measurement_mode;
    gchar *segment_name;
    gchar *freewave_name;
    gchar *signal_data_pattern;
    GwyLine *freewave;
    GwyXY *xy;
    guint *coord_index;
    gboolean have_x;
    gboolean have_y;
    GArray *curve_data;
    guint *curve_lengths;
    guint maxlen;
    NHFExtractingWhat extracting;
    GError *error;
} NHFFile;

static gint      detect_file              (const GwyFileDetectInfo *fileinfo,
                                           gboolean only_name);
static GwyFile*  load_file                (const gchar *filename,
                                           GwyRunModeFlags mode,
                                           GError **error);
static herr_t    handle_group             (hid_t loc_id,
                                           const char *name,
                                           const H5L_info_t *info,
                                           void *op_data);
static void      nhf_attr_handler         (GwyHDF5File *ghfile,
                                           hid_t loc_id,
                                           const char *attr_name);
static NHFStatus enter_grouping           (hid_t loc_id,
                                           const gchar *name,
                                           H5O_type_t expected_type,
                                           const gchar *format,
                                           guint namelen,
                                           guint *idx,
                                           GString *path,
                                           guint *pfxlen,
                                           GError **error);
static void      leave_grouping           (hid_t obj_id,
                                           GString *path,
                                           guint pfxlen);
static gboolean  create_image_data        (NHFFile *nhfile,
                                           hid_t dataset_id);
static gboolean  create_volume_data       (NHFFile *nhfile,
                                           hid_t dataset_id);
static void      create_free_wave_graph   (NHFFile *nhfile);
static void      create_curve_map         (NHFFile *nhfile);
static void      create_cmap_1x1_graph    (NHFFile *nhfile);
static gboolean  get_image_pixel_res      (hid_t loc_id,
                                           NHFFile *nhfile);
static gboolean  get_image_real_range     (hid_t loc_id,
                                           NHFFile *nhfile);
static gboolean  prepare_calibration      (hid_t dataset_id,
                                           GwyUnit *unit,
                                           gdouble *q,
                                           gdouble *v0);
static gchar*    extract_unit_symbol      (gchar *signal_unit);
static void      add_segment_name_to_title(NHFFile *nhfile,
                                           GQuark quark);
static gboolean  dataset_is_placeholder   (hid_t parent_id,
                                           const gchar *dataset_name,
                                           gint *size);
static void      set_gerror_hdf_internal  (NHFFile *nhfile,
                                           const gchar *funcname,
                                           herr_t status);
static void      clean_up_json_meta       (GwyDict *meta);

static const GwyEnum group_names[] = {
    { "calibration",      NHF_GROUP_CALIBRATION,      },
    { "image_line_based", NHF_GROUP_IMAGE_LINE_BASED, },
    { "spectroscopy",     NHF_GROUP_SPECTROSCOPY,     },
    { "wavemode_nma",     NHF_GROUP_WAVEMODE_NMA,     },
};

void
gwyhdf5_register_nhffile(void)
{
    gwy_file_func_register("nhffile",
                           N_("Nanosurf NHF HDF5 files (.nhf)"),
                           detect_file,
                           load_file,
                           NULL,
                           NULL);
}

static gint
detect_file(const GwyFileDetectInfo *fileinfo,
            gboolean only_name)
{
    hid_t file_id;
    gchar *created = NULL, *modified = NULL;
    gint ver_major, ver_minor;
    gint score = 0;

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

    if (H5Aexists(file_id, "nsf_file_version_major") > 0
        && H5Aexists(file_id, "nsf_file_version_minor") > 0
        && gwyhdf5_get_int_attr(file_id, ".", "nsf_file_version_major", &ver_major, NULL)
        && gwyhdf5_get_int_attr(file_id, ".", "nsf_file_version_minor", &ver_minor, NULL)) {
        score = 70;
        if (gwyhdf5_get_str_attr(file_id, ".", "created", &created, NULL)) {
            H5free_memory(created);
            score += 15;
        }
        if (gwyhdf5_get_str_attr(file_id, ".", "modified", &modified, NULL)) {
            H5free_memory(modified);
            score += 15;
        }
    }

    H5Fclose(file_id);

    return score;
}

static GwyFile*
load_file(const gchar *filename,
          G_GNUC_UNUSED GwyRunModeFlags mode,
          GError **error)
{
    GwyHDF5File ghfile;
    NHFFile nhfile;
    G_GNUC_UNUSED herr_t status;
    H5O_info_t infobuf;
    hid_t file_id;

    gwy_clear1(nhfile);
    nhfile.filename = filename;
    if ((file_id = H5Fopen(filename, H5F_ACC_RDONLY, H5P_DEFAULT)) < 0) {
        err_HDF5(error, "H5Fopen", file_id);
        return NULL;
    }
    nhfile.file_id = file_id;
    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;

    nhfile.path = g_string_new(NULL);
    nhfile.targets = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);

    gwyhdf5_init(&ghfile, file_id, infobuf.token);
    ghfile.impl = &nhfile;
    ghfile.attr_handler = nhf_attr_handler;
    nhfile.ghfile = &ghfile;

    /* First we need to find all UUID targets because UUIDs can link to things we have not seen yet in a sequential
     * scan. We do not use the rest of gwyhdf5_scan_file() much.
     *
     * NB: We hijack ghfile.meta, clear it and use it for some more localised scanning. */
    if ((status = H5Literate(file_id, H5_INDEX_NAME, H5_ITER_NATIVE, NULL, gwyhdf5_scan_file, &ghfile)) < 0) {
        err_HDF5(&nhfile.error, "H5Literate", status);
        goto end;
    }

    nhfile.file = gwy_file_new_in_construction();
    nhfile.group = nhfile.subgroup = nhfile.dataset = -1;
    if ((status = H5Literate(file_id, H5_INDEX_NAME, H5_ITER_NATIVE, NULL, handle_group, &nhfile)) < 0) {
        set_gerror_hdf_internal(&nhfile, "H5Literate", status);
        goto end;
    }

    if (!(nhfile.image_id + nhfile.cmap_id + nhfile.volume_id + nhfile.graph_id-1))
        err_NO_DATA(&nhfile.error);

end:
    if (nhfile.error) {
        g_clear_object(&nhfile.file);
        g_propagate_error(error, nhfile.error);
        nhfile.error = NULL;
    }

    if (file_id > 0) {
        status = gwyhdf5_fclose(file_id);
        gwy_debug("status %d", status);
    }
    gwyhdf5_free(&ghfile);
    g_string_free(nhfile.path, TRUE);
    g_hash_table_destroy(nhfile.targets);

    return nhfile.file;
}

/* Returns -1 iff it sets @nhfile->error.
 * Returns zero on success/skip (any ≥ 0 is considered success by H5Literate). */
static herr_t
handle_image_dataset(hid_t loc_id,
                     const char *name,
                     G_GNUC_UNUSED const H5L_info_t *info,
                     void *op_data)
{
    NHFFile *nhfile = (NHFFile*)op_data;
    GString *path = nhfile->path;
    hid_t dataset_id = -1;
    NHFStatus status;
    guint pfxlen;
    gint size, ndata = nhfile->xres * nhfile->yres;

    if (dataset_is_placeholder(loc_id, name, &size)) {
        gwy_debug("dataset is a placeholder");
        return NHF_NEUTRAL;
    }

    if ((status = enter_grouping(loc_id, name, H5O_TYPE_DATASET, "dataset_%04u", 12,
                                 &nhfile->dataset, path, &pfxlen, &nhfile->error)) != NHF_SUCCESS)
        return status;

    status = NHF_FAIL;
    /* If size == ndata we could also read dataset_block_size_max and check that it is indeed 1 (for volume data we
     * need to read it because it is not 1). But it does not seem necessary if we already can read it as an image. */
    if (size != ndata) {
        /* Looks like associated volume data. Skip it silently here. */
        if (nhfile->group_type == NHF_GROUP_WAVEMODE_NMA && size % ndata == 0) {
            gwy_debug("skipping associated volume data");
            status = NHF_NEUTRAL;
            goto end;
        }
        g_warning("Attribute dataset_size (%d) != xres*yres (%d). Ignoring and checking physical data size.",
                  size, ndata);
    }
    if ((dataset_id = gwyhdf5_open_and_check_dataset(loc_id, name, 1, &ndata, &nhfile->error)) < 0)
        goto end;

    if (create_image_data(nhfile, dataset_id))
        status = NHF_NEUTRAL;

end:
    leave_grouping(dataset_id, path, pfxlen);

    return status;
}

/* Returns -1 iff it sets @nhfile->error.
 * Returns zero on success/skip (any ≥ 0 is considered success by H5Literate). */
static herr_t
handle_volume_dataset(hid_t loc_id,
                      const char *name,
                      G_GNUC_UNUSED const H5L_info_t *info,
                      void *op_data)
{
    NHFFile *nhfile = (NHFFile*)op_data;
    GString *path = nhfile->path;
    hid_t dataset_id = -1;
    NHFStatus status;
    guint pfxlen;
    gint xres = nhfile->xres, yres = nhfile->yres, size, ndata = xres * yres;
    GwyLine *freewave = nhfile->freewave;

    /* Just skip the data if there is no z freewave line. */
    if (!freewave)
        return NHF_NEUTRAL;

    if (dataset_is_placeholder(loc_id, name, &size)) {
        gwy_debug("dataset is a placeholder");
        return NHF_NEUTRAL;
    }
    if (size == ndata) {
        /* Looks like the dataset is actually an image. We handle it elsewhere, so skip it silently here. */
        gwy_debug("skipping single-plane volume data; imported as an image");
        return NHF_NEUTRAL;
    }

    if ((status = enter_grouping(loc_id, name, H5O_TYPE_DATASET, "dataset_%04u", 12,
                                 &nhfile->dataset, path, &pfxlen, &nhfile->error)) != NHF_SUCCESS)
        return status;

    status = NHF_FAIL;
    ndata *= gwy_line_get_res(freewave);

    if (ndata != size) {
        g_warning("Attribute dataset_size (%d) != xres*yres (%d). Ignoring and checking physical data size.",
                  size, ndata);
    }

    if ((dataset_id = gwyhdf5_open_and_check_dataset(loc_id, name, 1, &ndata, &nhfile->error)) < 0)
        goto end;

    if (create_volume_data(nhfile, dataset_id))
        status = NHF_NEUTRAL;

end:
    leave_grouping(dataset_id, path, pfxlen);

    return status;
}

static herr_t
handle_wnma_wave_dataset(hid_t loc_id,
                         const char *name,
                         G_GNUC_UNUSED const H5L_info_t *info,
                         void *op_data)
{
    NHFFile *nhfile = (NHFFile*)op_data;
    GString *path = nhfile->path;
    GwyLine *freewave = nhfile->freewave;
    guint pfxlen;
    hid_t dataset_id = -1;
    gdouble q, v0;
    gint size;
    gdouble *d;
    NHFStatus status;

    if (dataset_is_placeholder(loc_id, name, &size)) {
        gwy_debug("dataset is a placeholder");
        return NHF_NEUTRAL;
    }
    if (size == 1) {
        gwy_debug("dataset has size 1 and we are never going to use it for z index");
        return NHF_NEUTRAL;
    }

    if ((status = enter_grouping(loc_id, name, H5O_TYPE_DATASET, "dataset_%04u", 12,
                                 &nhfile->dataset, path, &pfxlen, &nhfile->error)) != NHF_SUCCESS)
        return status;

    status = NHF_FAIL;
    if (err_DIMENSION(&nhfile->error, size))
        goto end;
    if ((dataset_id = gwyhdf5_open_and_check_dataset(loc_id, name, 1, &size, &nhfile->error)) < 0)
        goto end;

    nhfile->freewave = freewave = gwy_line_new(size, size, TRUE);
    d = gwy_line_get_data(freewave);
    if ((status = H5Dread(dataset_id, H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, H5P_DEFAULT, d)) < 0) {
        err_HDF5(&nhfile->error, "H5Dread", status);
        g_clear_object(&nhfile->freewave);
        goto end;
    }

    gwyhdf5_get_str_attr(dataset_id, ".", "signal_name", &nhfile->freewave_name, NULL);

    prepare_calibration(dataset_id, gwy_line_get_unit_y(freewave), &q, &v0);
    gwy_line_multiply(freewave, q);
    gwy_line_add(freewave, v0);

    gwy_debug("found volume data free wave with %d points", size);
    /* NB: Once we return anything than NEUTRAL, HDF5 will stop iterating. */
    status = NHF_SUCCESS;

end:
    leave_grouping(dataset_id, path, pfxlen);

    return status;
}

static herr_t
handle_spec_coord_dataset(hid_t loc_id,
                          const char *name,
                          G_GNUC_UNUSED const H5L_info_t *info,
                          void *op_data)
{
    NHFFile *nhfile = (NHFFile*)op_data;
    GString *path = nhfile->path;
    guint pfxlen;
    gchar *dataset_name = NULL;
    hid_t dataset_id = -1;
    gint i, size, npts;
    gdouble *d = NULL;
    NHFStatus status;
    gboolean is_y;

    if ((status = enter_grouping(loc_id, name, H5O_TYPE_DATASET, "dataset_%04u", 12,
                                 &nhfile->dataset, path, &pfxlen, &nhfile->error)) != NHF_SUCCESS)
        return status;

    status = NHF_NEUTRAL;
    if (dataset_is_placeholder(loc_id, name, &size)) {
        gwy_debug("dataset is a placeholder");
        goto end;
    }
    npts = nhfile->xres * nhfile->yres;
    if (size != npts) {
        gwy_debug("dataset has the wrong size %d (expecting %d coordinates)", size, npts);
        goto end;
    }
    gwyhdf5_get_str_attr(loc_id, name, "name", &dataset_name, NULL);
    if (dataset_name && gwy_strequal(dataset_name, "coordinates_x")) {
        is_y = FALSE;
    }
    else if (dataset_name && gwy_strequal(dataset_name, "coordinates_y")) {
        is_y = TRUE;
    }
    else {
        gwy_debug("dataset is neither x nor y");
        goto end;
    }

    status = NHF_FAIL;
    if ((dataset_id = gwyhdf5_open_and_check_dataset(loc_id, name, 1, &size, &nhfile->error)) < 0)
        goto end;

    d = g_new(gdouble, npts);
    if ((status = H5Dread(dataset_id, H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, H5P_DEFAULT, d)) < 0) {
        err_HDF5(&nhfile->error, "H5Dread", status);
        goto end;
    }

    if (is_y) {
        for (i = 0; i < npts; i++)
            nhfile->xy[i].y = d[i];
        nhfile->have_y = TRUE;
    }
    else {
        for (i = 0; i < npts; i++)
            nhfile->xy[i].x = d[i];
        nhfile->have_x = TRUE;
    }

    gwy_debug("found %s coordinates", is_y ? "y" : "x");
    status = (nhfile->have_x && nhfile->have_y) ? NHF_SUCCESS : NHF_NEUTRAL;

end:
    leave_grouping(dataset_id, path, pfxlen);
    if (dataset_name)
        H5free_memory(dataset_name);
    GWY_FREE(d);

    return status;
}

static herr_t
handle_curvelen_dataset(hid_t loc_id,
                        const char *name,
                        G_GNUC_UNUSED const H5L_info_t *info,
                        void *op_data)
{
    NHFFile *nhfile = (NHFFile*)op_data;
    GString *path = nhfile->path;
    guint pfxlen;
    gchar *blocksize_id = NULL;
    hid_t dataset_id = -1;
    gint i, size, npts;
    guint *cl;
    NHFStatus status;

    if (dataset_is_placeholder(loc_id, name, &size)) {
        gwy_debug("dataset is a placeholder");
        return NHF_NEUTRAL;
    }
    if (!gwyhdf5_get_int_attr(loc_id, name, "dataset_block_size_max", &nhfile->maxlen, NULL))
        return NHF_NEUTRAL;
    if (!gwyhdf5_get_str_attr(loc_id, name, "dataset_block_size_id", &blocksize_id, NULL))
        return NHF_NEUTRAL;
    H5free_memory(blocksize_id);

    if ((status = enter_grouping(loc_id, name, H5O_TYPE_DATASET, "dataset_%04u", 12,
                                 &nhfile->dataset, path, &pfxlen, &nhfile->error)) != NHF_SUCCESS)
        return status;

    status = NHF_NEUTRAL;
    npts = nhfile->xres * nhfile->yres;
    if (size != npts) {
        gwy_debug("dataset has the wrong size %d (expecting %d coordinates)", size, npts);
        goto end;
    }

    status = NHF_FAIL;
    if ((dataset_id = gwyhdf5_open_and_check_dataset(loc_id, name, 1, &size, &nhfile->error)) < 0)
        goto end;

    nhfile->curve_lengths = cl = g_renew(guint, nhfile->curve_lengths, npts+1);
    if ((status = H5Dread(dataset_id, H5T_NATIVE_UINT32, H5S_ALL, H5S_ALL, H5P_DEFAULT, cl)) < 0) {
        err_HDF5(&nhfile->error, "H5Dread", status);
        goto end;
    }

    cl[npts] = 0;
    for (i = 0; i < npts; i++) {
        gwy_debug("[%d] %u", i, cl[i]);
        if (cl[i] > nhfile->maxlen) {
            err_INVALID(&nhfile->error, "dataset_block_size_max");
            status = NHF_FAIL;
        }
    }
    gwy_accumulate_counts(cl, npts, TRUE);

    gwy_debug("found curve lengths (max %u)", nhfile->maxlen);
    status = NHF_SUCCESS;

end:
    leave_grouping(dataset_id, path, pfxlen);

    return status;
}

static herr_t
handle_cmap_dataset(hid_t loc_id,
                    const char *name,
                    G_GNUC_UNUSED const H5L_info_t *info,
                    void *op_data)
{
    NHFFile *nhfile = (NHFFile*)op_data;
    GString *path = nhfile->path;
    guint pfxlen;
    gchar *blocksize_id = NULL;
    hid_t dataset_id = -1;
    gint i, size, npts, ndata;
    gdouble q, v0;
    NHFCurveData cd;
    NHFStatus status;

    if (dataset_is_placeholder(loc_id, name, &size)) {
        gwy_debug("dataset is a placeholder");
        return NHF_NEUTRAL;
    }
    if (!gwyhdf5_get_str_attr(loc_id, name, "dataset_block_size_source", &blocksize_id, NULL)) {
        gwy_debug("dataset is curve lengths");
        return NHF_NEUTRAL;
    }
    H5free_memory(blocksize_id);

    status = NHF_NEUTRAL;
    gwy_clear1(cd);
    /* dataset_is_placeholder() reads "dataset_size" attribute which used to be the true total size (curve length
     * sum), but can apparently be either that or the allocated size (fullsize) */
    npts = nhfile->xres * nhfile->yres;
    ndata = npts*nhfile->maxlen;
    gwy_debug("xres %d, yres %d, size %d, maxlen %d", nhfile->xres, nhfile->yres, size, nhfile->maxlen);
    if (size != nhfile->curve_lengths[npts]) {
        gwy_debug("dataset has the wrong size %d (expecting %d values)", size, ndata);
        return NHF_NEUTRAL;
        gwy_debug("dataset has the wrong size %d (expecting true size %d values)", size, nhfile->curve_lengths[npts]);
        if (size != ndata) {
            gwy_debug("dataset has the wrong size also for maxlen (virtual %d values), giving up", ndata);
            return NHF_NEUTRAL;
        }
    }

    if ((status = enter_grouping(loc_id, name, H5O_TYPE_DATASET, "dataset_%04u", 12,
                                 &nhfile->dataset, path, &pfxlen, &nhfile->error)) != NHF_SUCCESS)
        return status;

    status = NHF_FAIL;
    /* The physical size is the allocated size, i.e. ndata. */
    if ((dataset_id = gwyhdf5_open_and_check_dataset(loc_id, name, 1, &ndata, &nhfile->error)) < 0)
        goto end;

    cd.data = g_new(gdouble, ndata);
    if ((status = H5Dread(dataset_id, H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, H5P_DEFAULT, cd.data)) < 0) {
        err_HDF5(&nhfile->error, "H5Dread", status);
        g_free(cd.data);
        goto end;
    }

    gwyhdf5_get_str_attr_g(dataset_id, ".", "signal_name", &cd.name, NULL);
    gwy_debug("signal name %s", cd.name);

    cd.unit = gwy_unit_new(NULL);
    prepare_calibration(dataset_id, cd.unit, &q, &v0);
    for (i = 0; i < ndata; i++)
        cd.data[i] = q*cd.data[i] + v0;

    g_array_append_val(nhfile->curve_data, cd);

    status = NHF_NEUTRAL;

end:
    leave_grouping(dataset_id, path, pfxlen);

    return status;
}

/* Returns -1 iff it sets @nhfile->error.
 * Returns zero on success/skip (any ≥ 0 is considered success by H5Literate). */
static herr_t
handle_image_or_wnma_subgroup(hid_t loc_id,
                              const char *name,
                              G_GNUC_UNUSED const H5L_info_t *info,
                              void *op_data)
{
    NHFFile *nhfile = (NHFFile*)op_data;
    GString *path = nhfile->path;
    H5L_iterate_t iterate_func = NULL;
    hid_t subgrp_id = -1;
    NHFStatus status;
    guint pfxlen;

    if ((status = enter_grouping(loc_id, name, H5O_TYPE_GROUP, "subgroup_%04u", 13,
                                 &nhfile->subgroup, path, &pfxlen, &nhfile->error)) != NHF_SUCCESS)
        return status;

    status = NHF_FAIL;
    if ((subgrp_id = H5Gopen2(loc_id, name, H5P_DEFAULT)) < 0) {
        err_HDF5(&nhfile->error, "H5Gopen2", subgrp_id);
        goto end;
    }

    if (!get_image_pixel_res(subgrp_id, nhfile))
        goto end;

    gwyhdf5_get_str_attr_g(subgrp_id, ".", "segment_name", &nhfile->segment_name, NULL);
    gwy_debug("segment_name %s", nhfile->segment_name ? nhfile->segment_name : "(unset)");

    if (nhfile->extracting == NHF_EXTRACTING_IMAGES)
        iterate_func = handle_image_dataset;
    else if (nhfile->extracting == NHF_EXTRACTING_VOLUMES)
        iterate_func = handle_volume_dataset;
    else {
        g_assert_not_reached();
    }

    if ((status = H5Literate(subgrp_id, H5_INDEX_NAME, H5_ITER_NATIVE, NULL, iterate_func, nhfile)) < 0) {
        set_gerror_hdf_internal(nhfile, "H5Literate", status);
        goto end;
    }

end:
    leave_grouping(subgrp_id, path, pfxlen);
    GWY_FREE(nhfile->segment_name);

    return status;
}

/* Returns -1 iff it sets @nhfile->error.
 * Returns zero on success/skip (any ≥ 0 is considered success by H5Literate). */
static herr_t
handle_cmap_subgroup(hid_t loc_id,
                     const char *name,
                     G_GNUC_UNUSED const H5L_info_t *info,
                     void *op_data)
{
    NHFFile *nhfile = (NHFFile*)op_data;
    GString *path = nhfile->path;
    hid_t subgrp_id = -1;
    NHFStatus status;
    gchar *segment_config;
    guint pfxlen, i, npts = nhfile->xres * nhfile->yres;

    if ((status = enter_grouping(loc_id, name, H5O_TYPE_GROUP, "subgroup_%04u", 13,
                                 &nhfile->subgroup, path, &pfxlen, &nhfile->error)) != NHF_SUCCESS)
        return status;

    status = NHF_FAIL;
    if ((subgrp_id = H5Gopen2(loc_id, name, H5P_DEFAULT)) < 0) {
        err_HDF5(&nhfile->error, "H5Gopen2", subgrp_id);
        goto end;
    }

    gwyhdf5_get_str_attr_g(subgrp_id, ".", "segment_name", &nhfile->segment_name, NULL);
    gwy_debug("segment_name %s", nhfile->segment_name ? nhfile->segment_name : "(unset)");

    /* Of course they organise data completely differently. We group all curves measured in a given pixel together.
     * They store them as independent datasets. So we start by finding the dataset with curve lengths which also gives
     * us the maximum length, so we can preallocate simple temporary buffers. Then read the data to them, noting down
     * the names, units and stuff. Only once we have read everything we can create a GwyLawn and interweave the curve
     * data into it. */
    if ((status = H5Literate(subgrp_id, H5_INDEX_NAME, H5_ITER_NATIVE, NULL, handle_curvelen_dataset, nhfile)) < 0) {
        set_gerror_hdf_internal(nhfile, "H5Literate", status);
        goto end;
    }

    nhfile->curve_data = g_array_new(FALSE, FALSE, sizeof(NHFCurveData));
    if ((status = H5Literate(subgrp_id, H5_INDEX_NAME, H5_ITER_NATIVE, NULL, handle_cmap_dataset, nhfile)) < 0) {
        set_gerror_hdf_internal(nhfile, "H5Literate", status);
        goto end;
    }

    if (gwyhdf5_get_str_attr_g(subgrp_id, ".", "segment_configuration", &segment_config, NULL))
        gwy_dict_pass_string_by_name(nhfile->ghfile->meta, "segment_configuration", segment_config);
    else
        gwy_dict_remove_by_name(nhfile->ghfile->meta, "segment_configuration");

    if (npts > 1)
        create_curve_map(nhfile);
    else
        create_cmap_1x1_graph(nhfile);

    status = NHF_NEUTRAL;

end:
    leave_grouping(subgrp_id, path, pfxlen);
    GWY_FREE(nhfile->segment_name);
    GWY_FREE(nhfile->curve_lengths);
    if (nhfile->curve_data) {
        for (i = 0; i < nhfile->curve_data->len; i++) {
            NHFCurveData *cd = &g_array_index(nhfile->curve_data, NHFCurveData, i);
            g_free(cd->name);
            g_clear_object(&cd->unit);
            g_free(cd->data);
        }
        g_array_free(nhfile->curve_data, TRUE);
        nhfile->curve_data = NULL;
    }

    return status;
}

/* Returns -1 iff it sets @nhfile->error.
 * Returns zero on success/skip (any ≥ 0 is considered success by H5Literate). */
static herr_t
handle_image_group(hid_t grp_id, NHFFile *nhfile)
{
    GwyDict *meta = nhfile->ghfile->meta;
    herr_t status;

    nhfile->extracting = NHF_EXTRACTING_IMAGES;
    if (!get_image_real_range(grp_id, nhfile))
        return NHF_FAIL;

    gwy_dict_remove_by_prefix(meta, NULL);
    H5Aiterate2(grp_id, H5_INDEX_NAME, H5_ITER_NATIVE, NULL, gwyhdf5_process_attribute, nhfile->ghfile);

    /* There should be two subgroups (segments) called subgroup_0000 and subgroup_0001. But assuming this does not
     * really simplify things much. So just enumerate them. */
    gwy_debug("scanning to extract data");
    if ((status = H5Literate(grp_id, H5_INDEX_NAME, H5_ITER_NATIVE, NULL, handle_image_or_wnma_subgroup, nhfile)) < 0) {
        set_gerror_hdf_internal(nhfile, "H5Literate", status);
        goto end;
    }

end:
    return status;
}

/* Returns -1 iff it sets @nhfile->error.
 * Returns zero on success/skip (any ≥ 0 is considered success by H5Literate). */
static herr_t
handle_wnma_group(hid_t grp_id, NHFFile *nhfile)
{
    GwyDict *meta = nhfile->ghfile->meta;
    herr_t status;

    nhfile->extracting = NHF_EXTRACTING_VOLUMES;
    if (!get_image_real_range(grp_id, nhfile))
        return NHF_FAIL;

    gwy_dict_remove_by_prefix(meta, NULL);
    H5Aiterate2(grp_id, H5_INDEX_NAME, H5_ITER_NATIVE, NULL, gwyhdf5_process_attribute, nhfile->ghfile);

    /* First iterate over datasets and find free wave. We are not guaranteed to get it in any particular order with
     * respect to the volume data. So if we want it first we must get it first. */
    gwy_debug("scanning to find free wave");
    if ((status = H5Literate(grp_id, H5_INDEX_NAME, H5_ITER_NATIVE, NULL, handle_wnma_wave_dataset, nhfile)) < 0) {
        set_gerror_hdf_internal(nhfile, "H5Literate", status);
        goto end;
    }

    /* There should be two subgroups (segments) called subgroup_0000 and subgroup_0001. But assuming this does not
     * really simplify things much. So just enumerate them. */
    gwy_debug("scanning to extract data");
    if ((status = H5Literate(grp_id, H5_INDEX_NAME, H5_ITER_NATIVE, NULL, handle_image_or_wnma_subgroup, nhfile)) < 0) {
        set_gerror_hdf_internal(nhfile, "H5Literate", status);
        goto end;
    }

    create_free_wave_graph(nhfile);

end:
    g_clear_object(&nhfile->freewave);
    GWY_FREE(nhfile->freewave_name);

    return status;
}

static herr_t
handle_cmap_group(hid_t grp_id, NHFFile *nhfile)
{
    herr_t status = NHF_NEUTRAL;
    GwyDict *meta = nhfile->ghfile->meta;
    guint npts = nhfile->xres * nhfile->yres;
    guint xres, yres;
    GwyXY xymin, xymax;

    if (npts > 1) {
        /* Only attempt grid detection for actual grid measurements. */
        if (!(nhfile->coord_index = gwy_check_regular_2d_grid((gdouble*)nhfile->xy, 2, npts, -1,
                                                              &xres, &yres, &xymin, &xymax))) {
            gwy_debug("points do not form a regular grid; cannot import");
            goto end;
        }
        if (xres != (guint)nhfile->xres || yres != (guint)nhfile->yres) {
            /* XXX: Is it possible we get xres and yres swapped with respect to the file? */
            g_warning("Detected grid resolution %ux%u differs from file's %dx%d.",
                      xres, yres, nhfile->xres, nhfile->yres);
            if (xres*yres != npts)
                goto end;
            nhfile->xres = xres;
            nhfile->yres = yres;
        }
        gwy_debug("got usable grid point index");
    }

    gwy_dict_remove_by_prefix(meta, NULL);
    H5Aiterate2(grp_id, H5_INDEX_NAME, H5_ITER_NATIVE, NULL, gwyhdf5_process_attribute, nhfile->ghfile);

    /* There are some number of subgroups (segments) called subgroup_0000 and subgroup_0001. Usually four. The way
     * data are structured, we do not actually know if we will be able to create one segmented curve map from them.
     * So, we start conservative and create a Lawn (or GraphModel) from each segment. */
    gwy_debug("scanning to extract data");
    if ((status = H5Literate(grp_id, H5_INDEX_NAME, H5_ITER_NATIVE, NULL, handle_cmap_subgroup, nhfile)) < 0) {
        set_gerror_hdf_internal(nhfile, "H5Literate", status);
        goto end;
    }

    /* TODO: If everything went well we may want to merge the lawns to a single segmented one (or merge graphs). */
    status = NHF_NEUTRAL;

end:
    GWY_FREE(nhfile->coord_index);

    return status;
}

/* Returns -1 iff it sets @nhfile->error.
 * Returns zero on success/skip (any ≥ 0 is considered success by H5Literate). */
static herr_t
handle_spectroscopy_group(hid_t grp_id, NHFFile *nhfile)
{
    herr_t status;
    gint npts;

    nhfile->extracting = NHF_EXTRACTING_SPECTRA;
    if (!get_image_pixel_res(grp_id, nhfile))
        return NHF_FAIL;

    npts = nhfile->xres * nhfile->yres;
    gwy_debug("spectroscopy is %s", npts == 1 ? "single-point" : "grid");
    if (npts > 1) {
        if (!get_image_real_range(grp_id, nhfile))
            return NHF_FAIL;

        /* First iterate over datasets and find free wave. We are not guaranteed to get it in any particular order with
         * respect to the volume data. So if we want it first we must get it first. */
        gwy_debug("scanning to find free wave");
        nhfile->have_x = nhfile->have_y = FALSE;
        nhfile->xy = g_new(GwyXY, npts);
        if ((status = H5Literate(grp_id, H5_INDEX_NAME, H5_ITER_NATIVE, NULL, handle_spec_coord_dataset, nhfile)) < 0) {
            set_gerror_hdf_internal(nhfile, "H5Literate", status);
            goto end;
        }

        status = NHF_FAIL;
        if (!nhfile->have_x) {
            err_MISSING_FIELD(&nhfile->error, "coordinates_x");
            goto end;
        }
        if (!nhfile->have_y) {
            err_MISSING_FIELD(&nhfile->error, "coordinates_y");
            goto end;
        }
    }
    else {
        /* That is what the file probably also says, but we do not consider zeros valid so do not call
         * get_image_real_range(). We also do not care about coordinates because we are going to create plain old
         * graphs. */
        nhfile->xreal = nhfile->yreal = 0.0;
        nhfile->xy = NULL;
    }

    status = handle_cmap_group(grp_id, nhfile);

end:
    GWY_FREE(nhfile->xy);

    return status;
}

/* Returns -1 iff it sets @nhfile->error.
 * Returns zero on success/skip (any ≥ 0 is considered success by H5Literate). */
static herr_t
handle_group(hid_t loc_id,
             const char *name,
             G_GNUC_UNUSED const H5L_info_t *info,
             void *op_data)
{
    NHFFile *nhfile = (NHFFile*)op_data;
    GString *path = nhfile->path;
    hid_t grp_id = -1;
    herr_t status;
    gchar *group_type_str = NULL;
    guint pfxlen;

    nhfile->group_type = NHF_GROUP_UNKNOWN;
    nhfile->extracting = NHF_EXTRACTING_NOTHING;
    if ((status = enter_grouping(loc_id, name, H5O_TYPE_GROUP, "group_%04u", 10,
                                 &nhfile->group, path, &pfxlen, &nhfile->error)) < 0)
        return status;

    status = NHF_FAIL;
    if ((grp_id = H5Gopen2(loc_id, name, H5P_DEFAULT)) < 0) {
        err_HDF5(&nhfile->error, "H5Gopen2", grp_id);
        goto end;
    }
    if (!gwyhdf5_get_str_attr_g(grp_id, ".", "group_type", &group_type_str, &nhfile->error))
        goto end;

    gwy_debug("group_type %s", group_type_str);
    nhfile->group_type = gwy_string_to_enum(group_type_str, group_names, G_N_ELEMENTS(group_names));
    g_free(group_type_str);

    gwyhdf5_get_str_attr_g(grp_id, ".", "measurement_mode", &nhfile->measurement_mode, NULL);
    gwy_debug("measurement_mode %s", nhfile->measurement_mode ? nhfile->measurement_mode : "(unset)");

    nhfile->subgroup = nhfile->dataset = -1;
    if (nhfile->group_type == NHF_GROUP_IMAGE_LINE_BASED)
        status = handle_image_group(grp_id, nhfile);
    else if (nhfile->group_type == NHF_GROUP_WAVEMODE_NMA) {
        /* First try to read images from the group because they are probably some. */
        if ((status = handle_image_group(grp_id, nhfile)) != NHF_FAIL)
            status = handle_wnma_group(grp_id, nhfile);
    }
    else if (nhfile->group_type == NHF_GROUP_SPECTROSCOPY)
        status = handle_spectroscopy_group(grp_id, nhfile);
    else {
        gwy_debug("skipping unimplemented group type");
        status = NHF_NEUTRAL;
    }

end:
    leave_grouping(grp_id, path, pfxlen);
    GWY_FREE(nhfile->measurement_mode);

    return status;
}

static void
nhf_attr_handler(GwyHDF5File *ghfile, hid_t loc_id, const char *attr_name)
{
    NHFFile *nhfile = (NHFFile*)ghfile->impl;
    guint plen = ghfile->path->len;
    const gchar *path = ghfile->path->str;
    gchar *uuid = NULL;
    NHFTarget target;

    target.group = target.subgroup = target.dataset = -1;

    /* /group_NNNN/group_uuid */
    if (plen == 22
        && gwy_strequal(attr_name, "group_uuid")
        && sscanf(path, "/group_%04u/group_uuid", &target.group) == 1) {
        target.type = NHF_TARGET_GROUP;
        goto finalise;
    }

    /* /group_NNNN/dataset_NNNN/signal_calibration_id */
    if (plen == 46
        && gwy_strequal(attr_name, "signal_calibration_id")
        && sscanf(path, "/group_%04u/dataset_%04u/signal_calibration_id", &target.group, &target.dataset) == 2) {
        target.type = NHF_TARGET_CALIBRATION;
        goto finalise;
    }

    /* /group_NNNN/dataset_NNNN/dataset_block_size_id */
    if (plen == 46
        && gwy_strequal(attr_name, "dataset_block_size_id")
        && sscanf(path, "/group_%04u/dataset_%04u/dataset_block_size_id", &target.group, &target.dataset) == 2) {
        target.type = NHF_TARGET_BLOCK_SIZE;
        goto finalise;
    }
    /* /group_NNNN/subgroup_NNNN/dataset_NNNN/dataset_block_size_id */
    if (plen == 60
        && gwy_strequal(attr_name, "dataset_block_size_id")
        && sscanf(path, "/group_%04u/subgroup_%04u/dataset_%04u/dataset_block_size_id",
                  &target.group, &target.subgroup, &target.dataset) == 3) {
        target.type = NHF_TARGET_BLOCK_SIZE;
        goto finalise;
    }

    return;

finalise:
    if (!gwyhdf5_get_str_attr_g(loc_id, ".", attr_name, &uuid, NULL)) {
        gwy_debug("failed to read target string of %s", path);
        return;
    }

    gwy_debug("target (type %d, group %d, subgroup %d, dataset %d) = %s",
              target.type, target.group, target.subgroup, target.dataset, uuid);
    g_hash_table_insert(nhfile->targets, uuid, g_memdup(&target, sizeof(target)));
}

/* Returns FAIL iff it sets @error.
 * Returns NEUTRAL for skip (but not considered error).
 * Returns SUCCESS for match. */
static NHFStatus
enter_grouping(hid_t loc_id, const gchar *name,
               H5O_type_t expected_type,
               const gchar *format, guint namelen, guint *idx,
               GString *path, guint *pfxlen, GError **error)
{
    H5O_info_t infobuf;
    herr_t status;

    status = H5Oget_info_by_name(loc_id, name, &infobuf, H5O_INFO_BASIC, H5P_DEFAULT);
    if (status < 0) {
        err_HDF5(error, "H5Oget_info_by_name", status);
        return NHF_FAIL;
    }

    if (strlen(name) != namelen || infobuf.type != expected_type)
        return NHF_NEUTRAL;
    if (sscanf(name, format, idx) != 1)
        return NHF_NEUTRAL;

    *pfxlen = path->len;
    g_string_append_c(path, '/');
    g_string_append(path, name);
    gwy_debug("group/dataset %u, path %s", *idx, path->str);

    return NHF_SUCCESS;
}

static void
leave_grouping(hid_t obj_id, GString *path, guint pfxlen)
{
    gwy_debug("leaving %s", path->str);
    g_string_truncate(path, pfxlen);

    if (obj_id >= 0)
        H5Oclose(obj_id);
}

static gboolean
create_image_data(NHFFile *nhfile, hid_t dataset_id)
{
    GwyFile *file = nhfile->file;
    GwyDict *meta = nhfile->ghfile->meta;
    GwyField *field = NULL;
    gchar *signal_name = NULL;
    gint id, xres = nhfile->xres, yres = nhfile->yres;
    herr_t status;
    gdouble *d;
    gdouble q, v0;
    gboolean ok = FALSE;

    field = gwy_field_new(xres, yres, nhfile->xreal, nhfile->yreal, FALSE);
    d = gwy_field_get_data(field);
    if ((status = H5Dread(dataset_id, H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, H5P_DEFAULT, d)) < 0) {
        err_HDF5(&nhfile->error, "H5Dread", status);
        goto end;
    }

    /* FIXME: signal_data_pattern??? */
    gwy_field_flip(field, FALSE, TRUE);

    gwyhdf5_get_str_attr(dataset_id, ".", "signal_name", &signal_name, NULL);

    gwy_unit_set_from_string(gwy_field_get_unit_xy(field), "m");
    prepare_calibration(dataset_id, gwy_field_get_unit_z(field), &q, &v0);
    gwy_field_multiply(field, q);
    gwy_field_add(field, v0);

    id = nhfile->image_id;
    gwy_file_set_image(file, id, field);
    gwy_log_add_import(file, GWY_FILE_IMAGE, id, NULL, nhfile->filename);

    if (signal_name && *signal_name)
        gwy_file_set_title(file, GWY_FILE_IMAGE, id, signal_name, FALSE);
    else
        gwy_image_title_fall_back(file, id);
    add_segment_name_to_title(nhfile, gwy_file_key_image_title(id));

    if (gwy_dict_get_n_items(meta)) {
        meta = gwy_dict_copy(meta);
        clean_up_json_meta(meta);
        gwy_file_pass_meta(file, GWY_FILE_IMAGE, id, meta);
    }

    nhfile->image_id++;
    ok = TRUE;

end:
    g_clear_object(&field);
    if (signal_name)
        H5free_memory(signal_name);

    return ok;
}

static gboolean
create_volume_data(NHFFile *nhfile, hid_t dataset_id)
{
    GwyFile *file = nhfile->file;
    GwyDict *meta = nhfile->ghfile->meta;
    GwyBrick *brick = NULL, *tmpbrick = NULL;
    gchar *signal_name = NULL;
    gint id, xres = nhfile->xres, yres = nhfile->yres, zres = gwy_line_get_res(nhfile->freewave);
    herr_t status;
    gdouble *d;
    gdouble q, v0;
    gboolean ok = FALSE;

    /* The data are organised by spectra (i.e. z is the innermost index). We have to transpose to storage by planes.
     * FIXME: There might be a snake pattern? But the data does not look like needing to flip every other row. */
    tmpbrick = gwy_brick_new(zres, yres, xres, zres, nhfile->yreal, nhfile->xreal, FALSE);
    d = gwy_brick_get_data(tmpbrick);
    if ((status = H5Dread(dataset_id, H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, H5P_DEFAULT, d)) < 0) {
        err_HDF5(&nhfile->error, "H5Dread", status);
        goto end;
    }

    brick = gwy_brick_new(xres, yres, zres, nhfile->xreal, nhfile->yreal, zres, FALSE);
    gwy_brick_transpose(tmpbrick, brick, GWY_BRICK_TRANSPOSE_ZXY, FALSE, FALSE, FALSE);
    g_clear_object(&tmpbrick);

    gwyhdf5_get_str_attr(dataset_id, ".", "signal_name", &signal_name, NULL);

    /* Transposition sets it weird. */
    gwy_brick_set_xreal(brick, nhfile->xreal);
    gwy_brick_set_yreal(brick, nhfile->yreal);
    gwy_brick_set_zreal(brick, zres);
    gwy_unit_set_from_string(gwy_brick_get_unit_x(brick), "m");
    gwy_unit_set_from_string(gwy_brick_get_unit_y(brick), "m");
    prepare_calibration(dataset_id, gwy_brick_get_unit_w(brick), &q, &v0);
    gwy_brick_multiply(brick, q);
    gwy_brick_add(brick, v0);

    id = nhfile->volume_id;
    gwy_file_set_volume(file, id, brick);
    gwy_log_add_import(file, GWY_FILE_VOLUME, id, NULL, nhfile->filename);

    if (signal_name && *signal_name)
        gwy_file_set_title(file, GWY_FILE_VOLUME, id, signal_name, FALSE);
    add_segment_name_to_title(nhfile, gwy_file_key_volume_title(id));

    if (gwy_dict_get_n_items(meta)) {
        meta = gwy_dict_copy(meta);
        clean_up_json_meta(meta);
        gwy_file_pass_meta(file, GWY_FILE_VOLUME, id, meta);
    }

    nhfile->volume_id++;
    ok = TRUE;

end:
    g_clear_object(&brick);
    g_clear_object(&tmpbrick);
    if (signal_name)
        H5free_memory(signal_name);

    return ok;
}

/* The free wave is some kind of independent variable (e.g. voltage). But it is really wave-like, not something we
 * could meaningfully use as Brick z-calibration. So we simply use integer indices in the brick z dimension.  */
static void
create_free_wave_graph(NHFFile *nhfile)
{
    GwyGraphModel *gmodel;
    GwyGraphCurveModel *gcmodel;

    if (!nhfile->freewave)
        return;

    gmodel = gwy_graph_model_new();
    gcmodel = gwy_graph_curve_model_new();
    gwy_graph_curve_model_set_data_from_line(gcmodel, nhfile->freewave, 0, 0);
    gwy_graph_model_add_curve(gmodel, gcmodel);
    g_object_unref(gcmodel);
    g_object_set(gmodel,
                 "unit-y", gwy_line_get_unit_y(nhfile->freewave),
                 "axis-label-bottom", _("Sample"),
                 NULL);
    if (nhfile->freewave_name) {
        g_object_set(gmodel,
                     "title", nhfile->freewave_name,
                     "axis-label-left", nhfile->freewave_name,
                     NULL);
    }
    g_object_set(gcmodel,
                 "mode", GWY_GRAPH_CURVE_LINE,
                 "description", _("Time series"),
                 NULL);
    gwy_file_pass_graph(nhfile->file, nhfile->graph_id, gmodel);
    gwy_log_add_import(nhfile->file, GWY_FILE_GRAPH, nhfile->graph_id, NULL, nhfile->filename);
    nhfile->graph_id++;
}

static void
create_curve_map(NHFFile *nhfile)
{
    GwyFile *file = nhfile->file;
    GwyDict *meta = nhfile->ghfile->meta;
    GwyLawn *lawn;
    gdouble *buf;
    guint m, start, len, ncurves, maxlen = nhfile->maxlen;
    gint id, i, k, xres = nhfile->xres, yres = nhfile->yres, npts = xres*yres;
    gchar *name = NULL;
    NHFCurveData *cd;

    ncurves = nhfile->curve_data->len;
    if (!ncurves)
        return;

    buf = g_new(gdouble, ncurves*maxlen);
    lawn = gwy_lawn_new(xres, yres, nhfile->xreal, nhfile->yreal, ncurves, 0);
    for (i = 0; i < npts; i++) {
        k = nhfile->coord_index[i];
        start = nhfile->curve_lengths[k];
        len = nhfile->curve_lengths[k+1] - start;
        for (m = 0; m < ncurves; m++) {
            cd = &g_array_index(nhfile->curve_data, NHFCurveData, m);
            gwy_assign(buf + m*len, cd->data + start, len);
        }
        gwy_lawn_set_curves(lawn, i % xres, i/xres, len, buf, NULL);
    }

    g_free(buf);

    gwy_unit_set_from_string(gwy_lawn_get_unit_xy(lawn), "m");
    for (m = 0; m < ncurves; m++) {
        cd = &g_array_index(nhfile->curve_data, NHFCurveData, m);
        gwy_lawn_set_curve_label(lawn, m, cd->name);
        gwy_unit_assign(gwy_lawn_get_unit_curve(lawn, m), cd->unit);
    }

    id = nhfile->cmap_id;
    gwy_file_pass_cmap(file, id, lawn);
    gwy_log_add_import(file, GWY_FILE_CMAP, id, NULL, nhfile->filename);

    if (gwyhdf5_get_str_attr(nhfile->file_id, nhfile->path->str, "segment_name", &name, NULL)) {
        gwy_file_set_title(file, GWY_FILE_CMAP, id, name, FALSE);
        H5free_memory(name);
    }

    if (gwy_dict_get_n_items(meta)) {
        meta = gwy_dict_copy(meta);
        clean_up_json_meta(meta);
        gwy_file_pass_meta(file, GWY_FILE_CMAP, id, meta);
    }

    nhfile->cmap_id++;
}

static void
create_cmap_1x1_graph(NHFFile *nhfile)
{
    GwyGraphModel *gmodel;
    GwyGraphCurveModel *gcmodel;
    GwyFile *file = nhfile->file;
    gdouble *buf;
    guint m, ncurves, clen;
    gchar *name = NULL;
    NHFCurveData *cd;

    g_return_if_fail(nhfile->xres * nhfile->yres == 1);
    ncurves = nhfile->curve_data->len;
    if (!ncurves)
        return;

    /* We have just one curve, but… */
    clen = nhfile->curve_lengths[1] - nhfile->curve_lengths[0];
    buf = g_new(gdouble, clen);
    gwy_math_linspace(buf, clen, 0, 1);
    for (m = 0; m < ncurves; m++) {
        cd = &g_array_index(nhfile->curve_data, NHFCurveData, m);
        gmodel = gwy_graph_model_new();
        g_object_set(gmodel,
                     "title", cd->name,
                     "axis-label-bottom", _("Sample"),
                     "axis-label-left", cd->name,
                     "unit-y", cd->unit,
                     NULL);
        gcmodel = gwy_graph_curve_model_new();
        g_object_set(gcmodel, "mode", GWY_GRAPH_CURVE_LINE, NULL);
        if (gwyhdf5_get_str_attr(nhfile->file_id, nhfile->path->str, "segment_name", &name, NULL))
            g_object_set(gcmodel, "description", name, NULL);
        gwy_graph_curve_model_set_data(gcmodel, buf, cd->data, clen);
        gwy_graph_model_add_curve(gmodel, gcmodel);
        g_object_unref(gcmodel);
        gwy_file_pass_graph(file, nhfile->graph_id, gmodel);
        gwy_log_add_import(file, GWY_FILE_GRAPH, nhfile->graph_id, NULL, nhfile->filename);
        nhfile->graph_id++;
    }

    g_free(buf);
}

static gboolean
get_image_pixel_res(hid_t loc_id, NHFFile *nhfile)
{
    gint yxres[2];
    gint nitems = 2;

    /* The sizes are technically 64bit. But when they actually need to be so huge we are not able to do anything
     * meaningful with the data anyway, so just let it fail. */
    if (!gwyhdf5_get_ints_attr(loc_id, ".", "rect_axis_size", 1, &nitems, yxres, &nhfile->error))
        return FALSE;
    gwy_debug("xres %d, yres %d", yxres[1], yxres[0]);
    nhfile->xres = yxres[1];
    nhfile->yres = yxres[0];
    if (err_DIMENSION(&nhfile->error, nhfile->xres) || err_DIMENSION(&nhfile->error, nhfile->yres))
        return FALSE;

    return TRUE;
}

static gboolean
get_image_real_range(hid_t loc_id, NHFFile *nhfile)
{
    gdouble yxreal[2];
    gint nitems = 2;

    if (!gwyhdf5_get_floats_attr(loc_id, ".", "rect_axis_range", 1, &nitems, yxreal, &nhfile->error))
        return FALSE;
    gwy_debug("xreal %g, yreal %g", yxreal[1], yxreal[0]);
    nhfile->xreal = yxreal[1];
    nhfile->yreal = yxreal[0];
    sanitise_real_size(&nhfile->xreal, "x size");
    sanitise_real_size(&nhfile->yreal, "y size");

    return TRUE;
}

static gboolean
prepare_calibration(hid_t dataset_id, GwyUnit *unit,
                    gdouble *q, gdouble *v0)
{
    gdouble calib_min, calib_max, signal_minmax[2], r, p;
    gchar *signal_unit;
    gboolean have_calibration = TRUE;
    gint power10, nitems = 2;

    *q = 1.0;
    *v0 = 0.0;
    /* FIXME: Some signals do not have calibrations and are directly in doubles (like "time"). Confusingly, they still
     * seem to have "signal_calibration_unit" so the condition below does not actually work. */
    if (!gwyhdf5_get_str_attr_g(dataset_id, ".", "signal_calibration_unit", &signal_unit, NULL)) {
        if (!gwyhdf5_get_str_attr_g(dataset_id, ".", "signal_unit", &signal_unit, NULL)) {
            gwy_unit_clear(unit);
            g_warning("Missing signal units.");
            return FALSE;
        }
        have_calibration = FALSE;
    }

    signal_unit = extract_unit_symbol(signal_unit);
    power10 = gwy_unit_set_from_string(unit, signal_unit);
    g_free(signal_unit);

    *q = p = gwy_exp10(power10);
    if (!have_calibration)
        return TRUE;

    if (!gwyhdf5_get_floats_attr(dataset_id, ".", "signal_minmax", 1, &nitems, signal_minmax, NULL)
        || !gwyhdf5_get_float_attr(dataset_id, ".", "signal_calibration_min", &calib_min, NULL)
        || !gwyhdf5_get_float_attr(dataset_id, ".", "signal_calibration_max", &calib_max, NULL)) {
        g_warning("Missing calibration attributes.");
        return FALSE;
    }

    r = (calib_max - calib_min)/(signal_minmax[1] - signal_minmax[0]);
    if (signal_minmax[1] == signal_minmax[0] || gwy_isinf(r) || gwy_isnan(r)) {
        g_warning("Invalid calibration attributes.");
        return FALSE;
    }

    *q = p*r;
    *v0 = p*(calib_min - signal_minmax[0]*r);
    return TRUE;
}

/* Extract unit symbol from unit of the form ‘name (symbol)’. */
static gchar*
extract_unit_symbol(gchar *signal_unit)
{
    gchar *opening, *closing;
    gchar *retval = NULL;

    if (!signal_unit)
        return NULL;

    /* Treat integers as unitless. */
    if (!g_str_has_prefix(signal_unit, "integer")) {
        opening = strchr(signal_unit, '(');
        closing = strrchr(signal_unit, ')');
        if (opening && closing && opening < closing)
            retval = g_strndup(opening+1, closing-opening - 1);
    }
    g_free(signal_unit);

    return retval;
}

static void
add_segment_name_to_title(NHFFile *nhfile, GQuark quark)
{
    const gchar *s;
    gchar *t;

    if (!nhfile->segment_name)
        return;

    s = gwy_dict_get_string(GWY_DICT(nhfile->file), quark);
    t = g_strconcat(s, " [", nhfile->segment_name, "]", NULL);
    gwy_dict_pass_string(GWY_DICT(nhfile->file), quark, t);
}

static gboolean
dataset_is_placeholder(hid_t parent_id, const gchar *dataset_name, gint *size)
{
    /* Just skip datasets which do not give dataset_size at all. */
    if (!gwyhdf5_get_int_attr(parent_id, dataset_name, "dataset_size", size, NULL))
        return TRUE;
    return *size <= 0;
}

/* Our functions set nhfile->error, HDF5 functions obviously do not. So if something like H5Literate() fails we do not
 * know if we reported the error or it is an HDF5 internal error. Distinguish them simply by checking if nhfile->error
 * is set – and set it when it is unset so callers can rely on it being set. */
static void
set_gerror_hdf_internal(NHFFile *nhfile, const gchar *funcname, herr_t status)
{
    if (!nhfile->error)
        err_HDF5(&nhfile->error, funcname, status);
}

#ifdef HAVE_JSON_GLIB
/* 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, "%g", 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_meta(GwyDict *meta, 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;

    JsonObject *root = json_node_get_object(root_node);
    g_string_append(path, "::");
    guint pfxlen = path->len;

    JsonObjectIter iter;
    json_object_iter_init(&iter, root);

    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)) {
            gwy_dict_set_string_by_name(meta, path->str, 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);
            gwy_dict_set_string_by_name(meta, path->str, buf->str);
        }
        else if (JSON_NODE_HOLDS_OBJECT(node) && gwy_strequal(name, "property")) {
            /* "property" is a nested dictionary with further nested members name:{value:x} or name{value:x,unit:y}.
             * Remove some of the silly nesting levels to make it look reasonable in the metadata. */
            JsonObject *property = json_node_get_object(node);
            JsonObjectIter property_iter;
            json_object_iter_init(&property_iter, property);

            const gchar *property_name;
            JsonNode *property_node;
            while (json_object_iter_next(&property_iter, &property_name, &property_node)) {
                if (!JSON_NODE_HOLDS_OBJECT(property_node))
                    continue;
                JsonObject *property_item = json_node_get_object(property_node);
                JsonNode *value = json_object_get_member(property_item, "value");
                JsonNode *unit = json_object_get_member(property_item, "unit");
                if (value) {
                    g_string_truncate(path, pfxlen);
                    g_string_truncate(buf, 0);
                    g_string_append(path, property_name);
                    format_atomic_json_meta(value, buf);
                    if (unit) {
                        g_string_append(buf, " ");
                        format_atomic_json_meta(unit, buf);
                    }
                    gwy_dict_set_string_by_name(meta, path->str, buf->str);
                }
            }
        }
        else {
            gwy_debug("unhandled meta %s of type %d", name, JSON_NODE_TYPE(member));
        }
        g_string_truncate(path, pfxlen);
    }
}
#endif

static void
clean_up_json_meta(GwyDict *meta)
{
    GQuark *keys = gwy_dict_keys(meta);
    guint i, n = gwy_dict_get_n_items(meta);
#ifdef HAVE_JSON_GLIB
    GString *path = NULL, *buf = NULL;
    JsonParser *parser = NULL;
#endif

    for (i = 0; i < n; i++) {
        const gchar *value = gwy_dict_get_string(meta, keys[i]);
        guint len, x = 0, jschars = 0;
        gchar c;

        for (len = 0; (c = value[len]); len++) {
            if (c == '{' || c == '}' || c == ':' || c == '[' || c == ']' || c == '"' || c == ',')
                jschars++;
        }
        if (len >= 10) {
            if (value[0] == '{' && value[1] == '"')
                x += len/10;
            if (value[len-1] == '}' && (value[len-2] == '}' || value[len-2] == '"' || value[len-2] == ']'))
                x += len/10;
            jschars += x;
        }

        if (jschars <= len/5)
            continue;

        gwy_debug("handling JSON meta %.16s...", value);
#ifdef HAVE_JSON_GLIB
        const gchar *key = g_quark_to_string(keys[i]);
        path = path ? g_string_assign(path, key) : g_string_new(key);
        if (!buf)
            buf = g_string_new(NULL);
        if (!parser)
            parser = json_parser_new_immutable();

        /* NB: It cannot not simply create a metadata item with the same name because we would remove it below.
         * We could to it in the opposite order if we duplicated the string… */
        expand_json_meta(meta, parser, value, path, buf);
#endif
        gwy_dict_remove(meta, keys[i]);
    }

    g_free(keys);
#ifdef HAVE_JSON_GLIB
    g_clear_object(&parser);
    if (path)
        g_string_free(path, TRUE);
    if (buf)
        g_string_free(buf, TRUE);
#endif
}

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