/*
 *  $Id: rhk-spm32.c 29611 2026-03-17 10:31:06Z yeti-dn $
 *  Copyright (C) 2005-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-FREEDESKTOP]
 * <mime-type type="application/x-rhk-sm2-spm">
 *   <comment>RHK SM2 SPM data</comment>
 *   <magic priority="80">
 *     <match type="string" offset="0" value="STiMage 3.1"/>
 *   </magic>
 *   <glob pattern="*.sm2"/>
 *   <glob pattern="*.SM2"/>
 * </mime-type>
 **/

/**
 * [FILE-MAGIC-FILEMAGIC]
 * # RHK SM2
 * 0 string STiMage\ 3.1 RHK Technology SM2 data
 **/

/**
 * [FILE-MAGIC-USERGUIDE]
 * RHK Instruments SM2
 * .sm2
 * Read SPS:Limited[1]
 * [1] Spectra curves are imported as graphs, positional information is lost.
 **/

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

#include "err.h"

#define HEADER_SIZE 512

#define MAGIC "STiMage 3.1"
#define MAGIC_SIZE (sizeof(MAGIC) - 1)
#define EXTENSION ".sm2"

/* FIXME GTK3: change to actual graph metadata once we some. */
#define GRAPH_PREFIX "/0/graph/graph"

typedef enum {
    RHK_TYPE_IMAGE =            0,
    RHK_TYPE_LINE =             1,
    RHK_TYPE_ANNOTATED_LINE =   3
} RHKType;

typedef enum {
    RHK_SCAN_RIGHT = 0,
    RHK_SCAN_LEFT  = 1,
    RHK_SCAN_UP    = 2,
    RHK_SCAN_DOWN  = 3
} RHKScanType;

typedef enum {
    RHK_DATA_SINGLE    = 0,
    RHK_DATA_INT16     = 1,
    RHK_DATA_INT32     = 2,
    RHK_DATA_INT8      = 3
} RHKDataType;

typedef enum {
    RHK_IMAGE_UNDEFINED                = 0,
    RHK_IMAGE_TOPOGRAPHIC              = 1,
    RHK_IMAGE_CURRENT                  = 2,
    RHK_IMAGE_AUX                      = 3,
    RHK_IMAGE_FORCE                    = 4,
    RHK_IMAGE_SIGNAL                   = 5,
    RHK_IMAGE_FFT                      = 6,
    RHK_IMAGE_NOISE_POWER_SPECTRUM     = 7,
    RHK_IMAGE_LINE_TEST                = 8,
    RHK_IMAGE_OSCILLOSCOPE             = 9,
    RHK_IMAGE_IV_SPECTRA               = 10,
    RHK_IMAGE_IV_4x4                   = 11,
    RHK_IMAGE_IV_8x8                   = 12,
    RHK_IMAGE_IV_16x16                 = 13,
    RHK_IMAGE_IV_32x32                 = 14,
    RHK_IMAGE_IV_CENTER                = 15,
    RHK_IMAGE_INTERACTIVE_SPECTRA      = 16,
    RHK_IMAGE_AUTOCORRELATION          = 17,
    RHK_IMAGE_IZ_SPECTRA               = 18,
    RHK_IMAGE_4_GAIN_TOPOGRAPHY        = 19,
    RHK_IMAGE_8_GAIN_TOPOGRAPHY        = 20,
    RHK_IMAGE_4_GAIN_CURRENT           = 21,
    RHK_IMAGE_8_GAIN_CURRENT           = 22,
    RHK_IMAGE_IV_64x64                 = 23,
    RHK_IMAGE_AUTOCORRELATION_SPECTRUM = 24,
    RHK_IMAGE_COUNTER                  = 25,
    RHK_IMAGE_MULTICHANNEL_ANALYSER    = 26,
    RHK_IMAGE_AFM_100                  = 27,
    RHK_IMAGE_LAST
} RHKPageType;

typedef struct {
    gdouble scale;
    gdouble offset;
    gchar *units;
} RHKRange;

typedef struct {
    gchar *date;
    guint xres;
    guint yres;
    RHKType type;
    RHKDataType data_type;
    GwyRawDataType rawtype;
    guint item_size;
    guint line_type;
    guint size;
    RHKPageType page_type;
    RHKRange x;
    RHKRange y;
    RHKRange z;
    gdouble xyskew;
    gdouble alpha;
    gboolean e_alpha;
    RHKRange iv;
    guint scan;
    gdouble period;
    guint id;
    guint data_offset;
    gchar *label;
    gchar *comment;

    const guchar *buffer;
} RHKPage;

static gboolean       module_register (void);
static gint           detect_file     (const GwyFileDetectInfo *fileinfo,
                                       gboolean only_name);
static GwyFile*       load_file       (const gchar *filename,
                                       GwyRunModeFlags mode,
                                       GError **error);
static gboolean       read_header     (RHKPage *rhkpage,
                                       GError **error);
static gboolean       read_range      (const gchar *buffer,
                                       const gchar *name,
                                       RHKRange *range);
static void           free_rhkpage    (RHKPage *rhkpage);
static GwyDict*  get_metadata    (RHKPage *rhkpage);
static GwyField*      read_data       (RHKPage *rhkpage);
static GwySpectra*    read_spectra    (RHKPage *rhkpage);
static GwyGraphModel* spectra_to_graph(GwySpectra *spectra);

static GwyModuleInfo module_info = {
    GWY_MODULE_ABI_VERSION,
    &module_register,
    N_("Imports RHK Technology SPM32 data files."),
    "Yeti <yeti@gwyddion.net>",
    "0.13",
    "David Nečas (Yeti) & Petr Klapetek, mod by Niv Levy",
    "2007",
};

static const GwyEnum scan_directions[] = {
    { "Right", RHK_SCAN_RIGHT, },
    { "Left",  RHK_SCAN_LEFT,  },
    { "Up",    RHK_SCAN_UP,    },
    { "Down",  RHK_SCAN_DOWN,  },
};

GWY_MODULE_QUERY2(module_info, rhk_spm32)

static gboolean
module_register(void)
{
    gwy_file_func_register("rhk-spm32",
                           N_("RHK SPM32 files (.sm2)"),
                           detect_file, load_file, NULL, NULL);

    return TRUE;
}

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

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

    if (fileinfo->buffer_len > MAGIC_SIZE
        && memcmp(fileinfo->head, MAGIC, MAGIC_SIZE) == 0)
        score = 100;

    return score;
}

static GwyFile*
load_file(const gchar *filename,
          G_GNUC_UNUSED GwyRunModeFlags mode,
          GError **error)
{
    GArray *rhkfile;
    RHKPage *rhkpage;
    GwyFile *file = NULL;
    GwyDict *meta;
    const guchar *buffer = NULL;
    gsize size = 0;
    GError *err = NULL;
    GwyField *dfield = NULL;
    gsize totalpos, pagesize;
    GString *key;
    guint i;

    if (!gwy_map_file_contents(filename, &buffer, &size, &err)) {
        err_GET_FILE_CONTENTS(error, &err);
        return NULL;
    }
    if (size < HEADER_SIZE) {
        err_TOO_SHORT(error);
        gwy_unmap_file_contents(buffer);
        return NULL;
    }

    // niv - rhkfile is an array of rhkpage's, but buffer is where the actual raw file data is stored
    rhkfile = g_array_new(FALSE, TRUE, sizeof(RHKPage));
    totalpos = 0;

    while (totalpos < size) {
        g_array_set_size(rhkfile, rhkfile->len + 1);
        rhkpage = &g_array_index(rhkfile, RHKPage, rhkfile->len - 1);
        rhkpage->buffer = buffer + totalpos;
        // niv - if the header seems invalid, skip all the next ones as well (and cancel the element addition to the
        // g_array)
        if (!read_header(rhkpage, &err)) {
            g_warning("failed to read rhk header after %u", rhkfile->len);
            g_free(rhkpage->date);
            g_array_set_size(rhkfile, rhkfile->len - 1);
            break;
        }

        pagesize = rhkpage->data_offset + rhkpage->item_size*rhkpage->xres*rhkpage->yres;
        if (size < totalpos + pagesize) {
            free_rhkpage(rhkpage);
            g_array_set_size(rhkfile, rhkfile->len - 1);
            break;
        }

        totalpos += pagesize;
    }

    /* Be tolerant and don't fail when we were able to import at least something */
    if (!rhkfile->len) {
        if (err)
            g_propagate_error(error, err);
        else
            err_NO_DATA(error);
        gwy_unmap_file_contents(buffer);
        g_array_free(rhkfile, TRUE);
        return NULL;
    }
    g_clear_error(&err);

    file = gwy_file_new_in_construction();
    key = g_string_new(NULL);
    for (i = 0; i < rhkfile->len; i++) {
        const gchar *cs;
        gchar *s;

        gwy_debug("rhk-spm32: processing page %d of %d\n", i+1, rhkfile->len);
        rhkpage = &g_array_index(rhkfile, RHKPage, i);
        if (rhkpage->type == RHK_TYPE_IMAGE) { // niv - just leaving this alone
            dfield = read_data(rhkpage);
            gwy_file_pass_image(file, i, dfield);
            cs = gwy_enum_to_string(rhkpage->scan, scan_directions, G_N_ELEMENTS(scan_directions));
            if (rhkpage->label) {
                s = (cs ? g_strdup_printf("%s [%s]", rhkpage->label, cs) : g_strdup(rhkpage->label));
                gwy_file_pass_title(file, GWY_FILE_IMAGE, i, s);
            }
            else
                gwy_image_title_fall_back(file, i);

            gwy_log_add_import(file, GWY_FILE_IMAGE, i, NULL, filename);
        }
        else if (rhkpage->type == RHK_TYPE_LINE) { // niv - after omicron.c
            GwySpectra* spectra = read_spectra(rhkpage);
            GwyGraphModel *gmodel;

            /* converting to graphs, as there is no point in leaving these as sps - no xy coordinates, so the spectro
             * tool is kinda clueless */
            gwy_debug("processing graph in page %d\n", i);
            if ((gmodel = spectra_to_graph(spectra))) {
                gwy_file_pass_graph(file, i, gmodel);
                gwy_log_add_import(file, GWY_FILE_GRAPH, i, NULL, filename);
            }
            g_object_unref(spectra);
        }
        else
            continue;

        gwy_debug("rhk-spm32: finished parsing page %d \n", i);
        meta = get_metadata(rhkpage);
        if (rhkpage->type == RHK_TYPE_IMAGE)
            gwy_file_pass_meta(file, GWY_FILE_IMAGE, i, meta);
        else if (rhkpage->type == RHK_TYPE_LINE)
            gwy_file_pass_meta(file, GWY_FILE_GRAPH, i, meta);
    }
    g_string_free(key, TRUE);

    gwy_unmap_file_contents(buffer);
    for (i = 0; i < rhkfile->len; i++)
        free_rhkpage(&g_array_index(rhkfile, RHKPage, i));
    g_array_free(rhkfile, TRUE);

    return file;
}

static gboolean
read_header(RHKPage *rhkpage,
            GError **error)
{
    const gchar *buffer;
    gchar *end;
    guint pos;

    buffer = rhkpage->buffer;

    rhkpage->date = g_strstrip(g_strndup(buffer + MAGIC_SIZE, 0x20 - MAGIC_SIZE));
    if (sscanf(buffer + 0x20, "%d %d %d %d %d %d %d",
               (gint*)&rhkpage->type, (gint*)&rhkpage->data_type, &rhkpage->line_type,
               &rhkpage->xres, &rhkpage->yres, &rhkpage->size, (gint*)&rhkpage->page_type) != 7) {
        g_set_error(error, GWY_MODULE_FILE_ERROR, GWY_MODULE_FILE_ERROR_DATA,
                    _("Invalid file header."));
        g_free(rhkpage->date);
        return FALSE;
    }
    gwy_debug("type = %u, data = %u, line = %u, image = %u",
              rhkpage->type, rhkpage->data_type, rhkpage->line_type, rhkpage->page_type);
    gwy_debug("xres = %d, yres = %d", rhkpage->xres, rhkpage->yres);
    if (err_DIMENSION(error, rhkpage->xres) || err_DIMENSION(error, rhkpage->yres))
        return FALSE;
    if (!((rhkpage->type == RHK_TYPE_IMAGE) || (rhkpage->type == RHK_TYPE_LINE))) {
        g_set_error(error, GWY_MODULE_FILE_ERROR, GWY_MODULE_FILE_ERROR_DATA,
                    _("Only image and line files are supported."));
        g_free(rhkpage->date);
        return FALSE;
    }

    if (rhkpage->data_type == RHK_DATA_INT8)
        rhkpage->rawtype = GWY_RAW_DATA_SINT8;
    else if ((rhkpage->data_type) == RHK_DATA_INT16)
        rhkpage->rawtype = GWY_RAW_DATA_SINT16;
    else if ((rhkpage->data_type) == RHK_DATA_INT32)
        rhkpage->rawtype = GWY_RAW_DATA_SINT32;
    else if ((rhkpage->data_type) == RHK_DATA_SINGLE)
        rhkpage->rawtype = GWY_RAW_DATA_FLOAT;
    else {
        err_INVALID(error, _("data type"));
        g_free(rhkpage->date);
        return FALSE;
    }
    rhkpage->item_size = gwy_raw_data_size(rhkpage->rawtype);

    if (!read_range(buffer + 0x40, "X", &rhkpage->x)
        || !read_range(buffer + 0x60, "Y", &rhkpage->y)
        || !read_range(buffer + 0x80, "Z", &rhkpage->z)) {
        err_INVALID(error, _("data ranges"));
        g_free(rhkpage->date);
        return FALSE;
    }

    /* Use negated positive conditions to catch NaNs */
    sanitise_real_size(&rhkpage->x.scale, "x scale");
    /* The y scale seem unused for non-image data */
    if (rhkpage->type == RHK_TYPE_IMAGE)
        sanitise_real_size(&rhkpage->y.scale, "y scale");

    if (!g_str_has_prefix(buffer + 0xa0, "XY ")) {
        err_MISSING_FIELD(error, "XY");
        g_free(rhkpage->date);
        return FALSE;
    }
    pos = 0xa0 + sizeof("XY");
    rhkpage->xyskew = g_ascii_strtod(buffer + pos, &end);
    if (end == buffer + pos) {
        err_INVALID(error, "XY");
        g_free(rhkpage->date);
        return FALSE;
    }
    pos = (end - buffer) + 2;
    /* Don't check failure, it seems the value is optional */
    rhkpage->alpha = g_ascii_strtod(buffer + pos, &end);
    /* not failing, but setting an existance flag, this happens for spectra, but otherwise i want to add this to the
     * metadata. */
    if (end == buffer + pos)
        rhkpage->e_alpha = FALSE;
    else
        rhkpage->e_alpha = TRUE;

    if (!read_range(buffer + 0xc0, "IV", &rhkpage->iv)) {
        err_INVALID(error, "IV");
        g_free(rhkpage->date);
        return FALSE;
    }

    if (g_str_has_prefix(buffer + 0xe0, "scan "))
        pos = 0xe0 + sizeof("scan");
    rhkpage->scan = strtol(buffer + pos, &end, 10);
    if (end == buffer + pos) {
        err_INVALID(error, "scan");
        g_free(rhkpage->date);
        return FALSE;
    }
    pos = (end - buffer);
    rhkpage->period = g_ascii_strtod(buffer + pos, &end);
    if (end == buffer + pos) {
        err_INVALID(error, "period");
        g_free(rhkpage->date);
        return FALSE;
    }

    if (sscanf(buffer + 0x100, "id %u %u", &rhkpage->id, &rhkpage->data_offset) != 2) {
        /* XXX: Some braindamaged files encountered in practice do not contain the data offset.  Cross fingers and
         * substitute HEADER_SIZE.  */
        g_warning("Data offset is missing, just guessing from now...");
        rhkpage->id = 0;
        rhkpage->data_offset = HEADER_SIZE;
    }
    gwy_debug("data_offset = %u", rhkpage->data_offset);
    if (rhkpage->data_offset < HEADER_SIZE) {
        err_INVALID(error, _("data offset"));
        g_free(rhkpage->date);
        return FALSE;
    }

    /* XXX: The same braindamaged files overwrite the label and comment part with some XML mumbo jumbo.  Sigh and
     * ignore it.  */
    if (strncmp(buffer + 0x140, "\x0d\x0a<?", 4) != 0) {
        rhkpage->label = g_strstrip(g_strndup(buffer + 0x140, 0x20));
        rhkpage->comment = g_strstrip(g_strndup(buffer + 0x160, HEADER_SIZE - 0x160));
    }

    return TRUE;
}

static gboolean
read_range(const gchar *buffer,
           const gchar *name,
           RHKRange *range)
{
    gchar *end;
    guint pos;

    if (!g_str_has_prefix(buffer, name))
        return FALSE;
    pos = strlen(name) + 1;

    range->scale = g_ascii_strtod(buffer + pos, &end);
    if (end == buffer + pos || pos > 0x20)
        return FALSE;
    pos = end - buffer;

    range->offset = g_ascii_strtod(buffer + pos, &end);
    if (end == buffer + pos || pos > 0x20)
        return FALSE;
    pos = end - buffer;

    range->units = g_strstrip(g_strndup(buffer + pos, 0x20 - pos));
    gwy_debug("<%s> %g %g <%s>", name, range->scale, range->offset, range->units);

    return TRUE;
}

static void
free_rhkpage(RHKPage *rhkpage)
{
    g_free(rhkpage->date);
    g_free(rhkpage->x.units);
    g_free(rhkpage->y.units);
    g_free(rhkpage->z.units);
    g_free(rhkpage->iv.units);
    g_free(rhkpage->label);
    g_free(rhkpage->comment);
}

static GwyDict*
get_metadata(RHKPage *rhkpage)
{
    GwyDict *meta = gwy_dict_new_in_construction();
    const gchar *s;

    gwy_dict_pass_string_by_name(meta, "Tunneling voltage", g_strdup_printf("%g mV", 1e3*rhkpage->iv.offset));
    gwy_dict_pass_string_by_name(meta, "Current", g_strdup_printf("%g nA", 1e9*rhkpage->iv.scale));
    if (rhkpage->id)
        gwy_dict_pass_string_by_name(meta, "Id", g_strdup_printf("%u", rhkpage->id));
    if (rhkpage->date && *rhkpage->date)
        gwy_dict_set_string_by_name(meta, "Date", rhkpage->date);
    if (rhkpage->comment && *rhkpage->comment)
        gwy_dict_set_string_by_name(meta, "Comment", rhkpage->comment);
    if (rhkpage->label && *rhkpage->label)
        gwy_dict_set_string_by_name(meta, "Label", rhkpage->label);

    s = gwy_enum_to_string(rhkpage->page_type, scan_directions, G_N_ELEMENTS(scan_directions));
    if (s && *s)
        gwy_dict_set_string_by_name(meta, "Image type", s);

    // FIXME - seems one can read 0 for spectra as well, but maybe it's not that important - it should be clear that
    // this is a nonsense value
    if (rhkpage->e_alpha)
        gwy_dict_pass_string_by_name(meta, "Angle", g_strdup_printf("%g deg", rhkpage->alpha));

    return meta;
}

static GwyField*
read_data(RHKPage *rhkpage)
{
    GwyField *dfield;
    const gchar *s;
    gdouble q;
    gint power10;
    guint xres, yres;

    xres = rhkpage->xres;
    yres = rhkpage->yres;
    /* the scales are no longer gurunteed to be positive, so they must be "fixed" here (to enable spectra) */
    dfield = gwy_field_new(xres, yres, xres*fabs(rhkpage->x.scale), yres*fabs(rhkpage->y.scale), FALSE);

    power10 = gwy_unit_set_from_string(gwy_field_get_unit_xy(dfield), rhkpage->x.units);
    if (power10) {
        q = gwy_exp10(power10);
        gwy_field_set_xreal(dfield, q*gwy_field_get_xreal(dfield));
        gwy_field_set_yreal(dfield, q*gwy_field_get_yreal(dfield));
    }

    s = rhkpage->z.units;
    /* Fix some silly units */
    if (gwy_strequal(s, "N/sec"))
        s = "s^-1";
    power10 = gwy_unit_set_from_string(gwy_field_get_unit_z(dfield), s);
    q = gwy_exp10(power10);

    gwy_convert_raw_data(rhkpage->buffer + rhkpage->data_offset, xres*yres, 1,
                         rhkpage->rawtype, GWY_BYTE_ORDER_LITTLE_ENDIAN, gwy_field_get_data(dfield),
                         q*fabs(rhkpage->z.scale), q*rhkpage->z.offset);
    gwy_field_flip(dfield, TRUE, FALSE);

    return dfield;
}

static GwySpectra*
read_spectra(RHKPage *rhkpage)
{
    guint i, rowstride;
    GwyUnit *siunit = NULL;
    GwyLine *dline;
    GwySpectra *spectra = NULL;
    GPtrArray *spectrum = NULL;
    // i'm leaving this alone, though it probably doesn't make sense, and i should just create graphs straight away
    // - but in case of future use, i'll just convert the data later to graphs

    // xres stores number of data points per spectra,
    // yres stores the number of spectra

    // reading data
    gwy_debug("rhk-spm32: %d spectra in this page\n", rhkpage->yres);
    rowstride = rhkpage->item_size * rhkpage->xres;
    for (i = 0; i < rhkpage->yres; i++) {
        dline = gwy_line_new(rhkpage->xres, rhkpage->x.scale, FALSE);
        gwy_line_set_offset(dline, rhkpage->x.offset);
        gwy_convert_raw_data(rhkpage->buffer + rhkpage->data_offset + i*rowstride, rhkpage->xres, 1,
                             rhkpage->rawtype, GWY_BYTE_ORDER_LITTLE_ENDIAN, gwy_line_get_data(dline),
                             rhkpage->z.scale, rhkpage->z.offset);
        gwy_unit_set_from_string(gwy_line_get_unit_x(dline), rhkpage->x.units);

        // the y units (and data) for a 1D graph are stored in Z in the rhk spm32 format!
        /* Fix "/\xfbHz" to "/Hz".
         * XXX: It might be still wrong as the strange character might mean
         * sqrt. */
        if (g_str_has_suffix(rhkpage->z.units, "/\xfbHz")) {
            gchar *s = gwy_strkill(g_strdup(rhkpage->z.units), "\xfb");
            siunit = gwy_unit_new(s);
            g_free(s);
        }
        else
            siunit = gwy_unit_new(rhkpage->z.units);
        gwy_unit_assign(gwy_line_get_unit_y(dline), siunit);
        g_object_unref(siunit);

        if (!spectrum)
            spectrum = g_ptr_array_sized_new(rhkpage->yres);
        g_ptr_array_add(spectrum, dline);
    }
    gwy_debug("rhk-spm32: finished parsing sps data\n");
    spectra = gwy_spectra_new();

    for (i = 0; i < rhkpage->yres; i++) {
        /* Static code analysis: sod off.  We can only get here when we allocated the spectrum above. */
        dline = g_ptr_array_index(spectrum, i);
        // since RHK spm32 does not record where it took the spectra, i'm setting these to zero
        gwy_spectra_add_spectrum(spectra, dline, 0, 0);
        g_object_unref(dline);
    }
    gwy_spectra_set_title(spectra, rhkpage->label);

    if (spectrum)
        g_ptr_array_free(spectrum, TRUE);

    return spectra;
}

static GwyGraphModel*
spectra_to_graph(GwySpectra *spectra)
{
    GwyGraphModel *gmodel;
    const gchar* graph_title;
    GwyGraphCurveModel *cmodel;
    gchar *curve_title = NULL;
    guint j, k, n_spectra, n_points;
    GwyLine *dline;
    gdouble *data, *xdata, *ydata, x_offset, x_realsize;
    GwyUnit *x_unit, *y_unit;

    if (!(n_spectra = gwy_spectra_get_n_spectra(spectra))) {
        gwy_debug("rhk-spm32: no spectra in rhkpage - something is odd\n");
        return NULL;
    }
    dline = gwy_spectra_get_spectrum(spectra, 0);
    n_points = gwy_line_get_res(dline);
    x_unit = gwy_line_get_unit_x(dline);
    y_unit = gwy_line_get_unit_y(dline);
    x_offset = gwy_line_get_offset(dline);
    x_realsize = gwy_line_get_real(dline);
    xdata = gwy_math_linspace(NULL, n_points, x_offset, x_realsize);
    ydata = g_new0(gdouble, n_points);
    gmodel = gwy_graph_model_new();
    g_object_set(gmodel, "unit-x", x_unit, "unit-y", y_unit, NULL);
    graph_title = gwy_spectra_get_title(spectra);
    g_object_set(gmodel, "title", graph_title, NULL);
    for (k = 1; k <= n_spectra; k++) {
        dline = gwy_spectra_get_spectrum(spectra, k-1);
        data = gwy_line_get_data(dline);
        for (j = 0; j < n_points; j++)
            ydata[j] = data[j];
        cmodel = gwy_graph_curve_model_new();
        gwy_graph_model_add_curve(gmodel, cmodel);
        g_object_unref(cmodel);
        curve_title = g_strdup_printf("%s %d", graph_title, k);
        g_object_set(cmodel, "description", curve_title,
                     "mode", GWY_GRAPH_CURVE_LINE,
                     "color", gwy_graph_get_preset_color(k),
                     NULL);
        g_free(curve_title);
        gwy_graph_curve_model_set_data(cmodel, xdata, ydata, n_points);
        gwy_graph_curve_model_enforce_order(cmodel);
    }
    g_free(ydata);
    g_free(xdata);

    return gmodel;
}

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