/*
 *  $Id: psppt.c 29611 2026-03-17 10:31:06Z yeti-dn $
 *  Copyright (C) 2021-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-FREEDESKTOP]
 * <mime-type type="application/x-park-ps-ppt-spm">
 *   <comment>Park Systems PS-PPT</comment>
 *   <magic priority="75">
 *     <match type="string" offset="0" value="PS-PPT/v1\n"/>
 *   </magic>
 *   <glob pattern="*.ps-ppt"/>
 *   <glob pattern="*.PS-PPT"/>
 * </mime-type>
 **/

/**
 * [FILE-MAGIC-FILEMAGIC]
 * # Park Systems PS-PPT
 * 0 string PS-PPT/v1\x0a Park Systems PS-PPT curve map data
 **/

/**
 * [FILE-MAGIC-USERGUIDE]
 * Park Systems PS-PPT
 * .ps-ppt
 * Read Curvemap
 **/

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

#include "get.h"
#include "err.h"

#define MAGIC "PS-PPT/v1\n"
#define MAGIC_SIZE (sizeof(MAGIC)-1)

enum {
    HEADER_SIZE = 16,
    FRAME_SIZE = 8
};

typedef enum {
    PSPPT_SCAN_START = 0,
    PSPPT_SCAN_STOP  = 1,
    PSPPT_PARAM      = 16,
    PSPPT_RTFD       = 17,
    PSPPT_UNUSED     = 255,
} PSPPTDataType;

typedef struct {
    gchar magic[MAGIC_SIZE];
    guint unused1;
    guint nframes;
    guint next_table_offset_unused;
    guint reserved1;
    guint reserved2;
} PSPPTHeader;

typedef struct {
    /* Frame table. */
    PSPPTDataType type;
    guint reserved;
    guint offset;
    /* JSON or precalculated. */
    guint size;
} PSPPTFrame;

typedef struct {
    gint xres;
    gint yres;
    gchar *direction;
    gdouble xreal;
    gdouble yreal;
    JsonObject *root;
} PSPPTScanStart;

typedef struct {
    JsonObject *root;
} PSPPTScanStop;

typedef struct {
    JsonObject *root;
} PSPPTParam;

typedef struct {
    PSPPTHeader header;
    PSPPTScanStart scanstart;
    PSPPTScanStop scanstop;
    PSPPTParam param;
    guint nframes;           /* Real count; can be smaller than header.frames. */
    PSPPTFrame *frames;
    gchar **ids;
    gchar **units;
    gdouble *power10;
    gint *reorder;
    GString *str;
    GArray *databuf;
    GwyLawn *lawn;
    GwyDict *meta;
} PSPPTFile;

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 check_string_list_item(JsonObject *root,
                                       const gchar *name,
                                       gchar **values,
                                       guint i,
                                       GError **error);

static GwyModuleInfo module_info = {
    GWY_MODULE_ABI_VERSION,
    module_register,
    N_("Imports Park Systems PS-PPT data files."),
    "Yeti <yeti@gwyddion.net>",
    "3.0",
    "David Nečas (Yeti)",
    "2021",
};

GWY_MODULE_QUERY2(module_info, psppt)

static gboolean
module_register(void)
{
    gwy_file_func_register("psppt",
                           N_("Park Systems PS-PPT data files (.ps-ppt)"),
                           detect_file, load_file, NULL, NULL);

    return TRUE;
}

static gint
detect_file(const GwyFileDetectInfo *fileinfo, gboolean only_name)
{
    if (only_name)
        return g_str_has_suffix(fileinfo->name_lowercase, ".ps-ppt") ? 20 : 0;

    if (fileinfo->buffer_len >= MAGIC_SIZE && !memcmp(fileinfo->head, MAGIC, MAGIC_SIZE))
        return 80;

    return 0;
}

static gboolean
psppt_read_header(PSPPTHeader *header, const guchar *buf, gsize size, gsize *pos, GError **error)
{
    const guchar *p = buf + *pos;

    if ((size - *pos) < HEADER_SIZE + MAGIC_SIZE) {
        err_FILE_TYPE(error, "PS-PPT/v1");
        return FALSE;
    }
    get_CHARARRAY(header->magic, &p);
    if (memcmp(header->magic, MAGIC, MAGIC_SIZE)) {
        err_FILE_TYPE(error, "PS-PPT/v1");
        return FALSE;
    }
    /* They have a one-byte field and three-byte field there.  Read it as one number. */
    header->nframes = gwy_get_guint32_be(&p);
    header->unused1 = (header->nframes >> 24);
    header->nframes &= 0xffffff;
    gwy_debug("unused %u, nframes %u", header->unused1, header->nframes);
    header->next_table_offset_unused = gwy_get_guint32_be(&p);
    header->reserved1 = gwy_get_guint32_be(&p);
    header->reserved2 = gwy_get_guint32_be(&p);
    gwy_debug("next_offset %u, reserved1 %u, reserved2 %u",
              header->next_table_offset_unused, header->reserved1, header->reserved2);

    *pos += p - buf;
    return TRUE;
}

static gsize
psppt_read_frame_table(PSPPTFile *pfile, const guchar *buf, gsize size, gsize *pos, GError **error)
{
    const guchar *p = buf + *pos;
    gsize framepos;
    guint i, j, ndata = 0, nframes = pfile->header.nframes;
    PSPPTFrame *frames;

    if ((size - *pos)/FRAME_SIZE < nframes) {
        err_TRUNCATED_PART(error, "Frame Table");
        return FALSE;
    }

    framepos = *pos + nframes*FRAME_SIZE;
    pfile->frames = frames = g_new(PSPPTFrame, nframes);
    /* Compactify frames (get rid of empty ones) while reading. */
    for (i = j = 0; i < nframes; i++) {
        PSPPTFrame *frame = frames + j;

        /* They have a one-byte field and three-byte field there.  Read it as one number. */
        frame->reserved = gwy_get_guint32_be(&p);
        frame->type = (frame->reserved >> 24);
        frame->reserved &= 0xffffff;
        frame->offset = gwy_get_guint32_be(&p);
        if (frame->type == PSPPT_UNUSED)
            continue;

        gwy_debug("[%u] type %u, offset %u (reserved %u)", i, frame->type, frame->offset, frame->reserved);
        if (frame->offset >= size) {
            err_TRUNCATED_PART(error, "Frame");
            return FALSE;
        }
        if (frame->offset <= framepos) {
            g_set_error(error, GWY_MODULE_FILE_ERROR, GWY_MODULE_FILE_ERROR_DATA,
                        _("Frame offsets do not increase monotonically."));
            return FALSE;
        }
        framepos = frame->offset;
        j++;
    }
    pfile->nframes = nframes = j;

    /* Verify frame types. */
    ndata = 0;
    for (i = 0; i < nframes; i++) {
        PSPPTDataType type = frames[i].type;
        gboolean ok = FALSE;

        if (i == 0)
            ok = (type == PSPPT_SCAN_START);
        else if (i == 1)
            ok = (type == PSPPT_PARAM);
        else if (i == nframes-1)
            ok = (type == PSPPT_SCAN_STOP);
        else {
            ok = (type == PSPPT_RTFD || type == PSPPT_PARAM);
            if (type == PSPPT_RTFD)
                ndata++;
        }

        if (!ok) {
            g_set_error(error, GWY_MODULE_FILE_ERROR, GWY_MODULE_FILE_ERROR_DATA,
                        _("Unexpected frame with data type %d."), type);
            return FALSE;
        }
    }
    if (!ndata) {
        err_NO_DATA(error);
        return FALSE;
    }

    /* Calculate frame sizes. */
    framepos = size;
    for (i = nframes; i; i--) {
        frames[i-1].size = framepos - frames[i-1].offset;
        framepos = frames[i-1].offset;
    }
    *pos += p - buf;
    return TRUE;
}

static gboolean
err_JSON_STRUCTURE(GError **error, const gchar *what, GType type, const gchar *typename)
{
    if (!typename && type) {
        if (type == G_TYPE_INT64)
            typename = "integer";
        else if (type == G_TYPE_DOUBLE)
            typename = "number";
        else if (type == G_TYPE_STRING)
            typename = "string";
        else if (type == G_TYPE_BOOLEAN)
            typename = "boolean";
        else if (type == JSON_TYPE_ARRAY)
            typename = "array";
        else if (type == JSON_TYPE_OBJECT)
            typename = "object";
        else
            typename = "???";
    }

    g_set_error(error, GWY_MODULE_FILE_ERROR, GWY_MODULE_FILE_ERROR_DATA,
                _("Unexpected JSON structure: %s should be %s."), what, typename);
    return FALSE;
}

static gboolean
get_json_with_type(JsonObject *object, JsonNode **member, const gchar *key, GType type, GError **error)
{
    *member = json_object_get_member(object, key);
    if (*member) {
        GType nodetype = json_node_get_value_type(*member);
        if (nodetype == type)
            return TRUE;
        if (type == G_TYPE_DOUBLE && nodetype == G_TYPE_INT64)
            return TRUE;
    }

    return err_JSON_STRUCTURE(error, key, type, NULL);
}

static JsonObject*
psppt_read_frame(JsonParser *parser, PSPPTFrame *frame, const guchar *buf, GError **error)
{
    const gchar *start = buf + frame->offset;
    GError *err = NULL;

    if (!json_parser_load_from_data(parser, start, frame->size, &err)) {
        g_set_error(error, GWY_MODULE_FILE_ERROR, GWY_MODULE_FILE_ERROR_DATA,
                    _("JSON parsing error: %s"), err->message);
        g_clear_error(&err);
        return NULL;
    }

    JsonNode *root_node = json_parser_get_root(parser);
    if (!JSON_NODE_HOLDS_OBJECT(root_node)) {
        err_JSON_STRUCTURE(error, "root", JSON_TYPE_OBJECT, NULL);
        return NULL;
    }

    return json_node_get_object(root_node);
}

static gboolean
handle_scan_start(PSPPTScanStart *scanstart, JsonObject *root, GError **error)
{
    JsonNode *type, *geometry_node, *direction, *pixel_width, *pixel_height, *width, *height;

    if (!get_json_with_type(root, &type, "type", G_TYPE_STRING, error)
        || !get_json_with_type(root, &geometry_node, "geometry", JSON_TYPE_OBJECT, error))
        return FALSE;

    JsonObject *geometry = json_node_get_object(geometry_node);
    if (!get_json_with_type(geometry, &direction, "direction", G_TYPE_STRING, error)
        || !get_json_with_type(geometry, &pixel_height, "pixelHeight", G_TYPE_INT64, error)
        || !get_json_with_type(geometry, &pixel_width, "pixelWidth", G_TYPE_INT64, error)
        || !get_json_with_type(geometry, &width, "width", G_TYPE_DOUBLE, error)
        || !get_json_with_type(geometry, &height, "height", G_TYPE_DOUBLE, error))
        return FALSE;

    if (!gwy_strequal(json_node_get_string(type), "scan.start"))
        return err_JSON_STRUCTURE(error, "scan.start.type", 0, "scan.start");

    g_assert(!scanstart->root);
    scanstart->root = json_object_ref(root);
    scanstart->xres = json_node_get_int(pixel_width);
    scanstart->yres = json_node_get_int(pixel_height);
    scanstart->xreal = json_node_get_double(width) * 1e-6;
    scanstart->yreal = json_node_get_double(height) * 1e-6;
    scanstart->direction = g_strdup(json_node_get_string(direction));
    gwy_debug("xres = %u, yres = %u", scanstart->xres, scanstart->yres);
    gwy_debug("xreal = %g, yreal = %g", scanstart->xreal, scanstart->yreal);
    gwy_debug("direction = %s", scanstart->direction);

    return TRUE;
}

static gboolean
handle_scan_stop(PSPPTScanStop *scanstop, JsonObject *root, GError **error)
{
    JsonNode *type;

    if (!get_json_with_type(root, &type, "type", G_TYPE_STRING, error))
        return FALSE;
    if (!gwy_strequal(json_node_get_string(type), "scan.stop"))
        return err_JSON_STRUCTURE(error, "scan.stop.type", 0, "scan.stop");

    g_assert(!scanstop->root);
    scanstop->root = json_object_ref(root);

    return TRUE;
}

static gboolean
handle_param(PSPPTParam *param, JsonObject *root, GError **error)
{
    JsonNode *type;

    if (!get_json_with_type(root, &type, "type", G_TYPE_STRING, error))
        return FALSE;
    if (!gwy_strequal(json_node_get_string(type), "ppt.param"))
        return err_JSON_STRUCTURE(error, "ppt.param.type", 0, "ppt.param");

    /* We can keep the first or the last or any of them if parameter change…  Keeping the first is simplest. */
    if (!param->root) {
        param->root = json_object_ref(root);
    }

    return TRUE;
}

static gboolean
try_to_reorder(gchar **names, gint *order, gint n,
               const gchar *name, gint movewhere)
{
    gint i;

    if (movewhere < 0 || movewhere >= n)
        return FALSE;
    for (i = 0; i < n; i++) {
        if (gwy_strequal(names[order[i]], name))
            break;
    }
    if (i == n)
        return FALSE;
    if (i == movewhere)
        return TRUE;

    GWY_SWAP(gint, order[i], order[movewhere]);
    return TRUE;
}

static gboolean
handle_rtfd(PSPPTFile *pfile, JsonObject *root, GError **error)
{
    JsonNode *type, *info_node, *numbers_node, *channels_node, *indices_node, *fast, *slow, *padding;
    gint power10;
    GString *str = pfile->str;
    GwyLawn *lawn;
    gboolean lawn_is_new = FALSE;

    if (!get_json_with_type(root, &type, "type", G_TYPE_STRING, error)
        || !get_json_with_type(root, &info_node, "info", JSON_TYPE_OBJECT, error)
        || !get_json_with_type(root, &numbers_node, "numbers", JSON_TYPE_ARRAY, error))
        return FALSE;

    JsonObject *info = json_node_get_object(info_node);
    if (!get_json_with_type(info, &channels_node, "channels", JSON_TYPE_ARRAY, error)
        || !get_json_with_type(info, &padding, "padding", G_TYPE_BOOLEAN, error)
        || !get_json_with_type(info, &indices_node, "index", JSON_TYPE_OBJECT, error))
        return FALSE;

    JsonObject *indices = json_node_get_object(indices_node);
    if (!get_json_with_type(indices, &fast, "fast", G_TYPE_INT64, error)
        || !get_json_with_type(indices, &slow, "slow", G_TYPE_INT64, error))
        return FALSE;
    if (!gwy_strequal(json_node_get_string(type), "ppt.rtfd"))
        return err_JSON_STRUCTURE(error, "ppt.rtfd.type", 0, "ppt.rtfd");

    JsonArray *channels = json_node_get_array(channels_node);
    JsonArray *numbers = json_node_get_array(numbers_node);
    guint n = json_array_get_length(channels);
    guint nnum = json_array_get_length(numbers);
    guint col = json_node_get_int(fast);
    guint row = json_node_get_int(slow);
    gboolean G_GNUC_UNUSED pad = json_node_get_boolean(padding);
    gwy_debug("(%u,%u) nchannels = %u, nnumbers = %u, padding = %d", col, row, n, nnum, pad);

    /* The first time we encounter a spectrum set use it as a template.  All other sets must follow the same
     * structure. */
    lawn = pfile->lawn;
    if (!n || n != nnum || (lawn && n != gwy_lawn_get_n_curves(lawn)))
        return err_INCONSISTENT_SPECTRA(error);

    if (!pfile->lawn) {
        const PSPPTScanStart *ss = &pfile->scanstart;

        lawn_is_new = TRUE;
        gwy_debug("creating lawn xres=%d, yres=%d", ss->xres, ss->yres);
        lawn = pfile->lawn = gwy_lawn_new(ss->xres, ss->yres, ss->xreal, ss->yreal, n, 0);
        gwy_unit_set_from_string(gwy_lawn_get_unit_xy(lawn), "m");
        /* These duplicates lawn's properties, but can be used with check_string_list_item(). */
        pfile->units = g_new0(gchar*, n+1);
        pfile->ids = g_new0(gchar*, n+1);
        pfile->power10 = g_new0(gdouble, n);
        pfile->reorder = g_new(gint, n);
    }

    guint base64len = 0;
    for (guint i = 0; i < n; i++) {
        JsonNode *item = json_array_get_element(channels, i);
        if (!JSON_NODE_HOLDS_OBJECT(item))
            return err_JSON_STRUCTURE(error, "channels.item", JSON_TYPE_OBJECT, NULL);

        JsonObject *object = json_node_get_object(item);
        if (!check_string_list_item(object, "id", pfile->ids, i, error))
            return FALSE;
        if (!check_string_list_item(object, "unit", pfile->units, i, error))
            return FALSE;

        item = json_array_get_element(numbers, i);
        const gchar *base64data = json_node_get_string(item);
        if (!base64data)
            return err_JSON_STRUCTURE(error, "numbers.item", G_TYPE_STRING, NULL);

        if (!i)
            base64len = strlen(base64data);
        else if (strlen(base64data) != base64len)
            return err_INCONSISTENT_SPECTRA(error);
    }

    if (lawn_is_new) {
        for (guint i = 0; i < n; i++)
            pfile->reorder[i] = i;
        try_to_reorder(pfile->ids, pfile->reorder, n, "Force", 0);
        try_to_reorder(pfile->ids, pfile->reorder, n, "ZHeight", 0);
        try_to_reorder(pfile->ids, pfile->reorder, n, "Lfm", n-1);
        for (guint i = 0; i < n; i++) {
            guint ri = pfile->reorder[i];
            gwy_lawn_set_curve_label(lawn, ri, pfile->ids[i]);
            power10 = gwy_unit_set_from_string(gwy_lawn_get_unit_curve(lawn, ri), pfile->units[i]);
            pfile->power10[i] = gwy_exp10(power10);
        }
    }

    g_array_set_size(pfile->databuf, 0);
    for (guint i = 0; i < n; i++) {
        guint ri = pfile->reorder[i];
        JsonNode *item = json_array_get_element(numbers, i);
        g_string_assign(str, json_node_get_string(item));
        g_base64_decode_inplace(str->str, &str->len);
        guint npts = str->len/sizeof(gfloat);
        if (i) {
            if (npts != pfile->databuf->len/n)
                return err_INCONSISTENT_SPECTRA(error);
        }
        else
            g_array_set_size(pfile->databuf, n*npts);

        gdouble *d = &g_array_index(pfile->databuf, gdouble, npts*ri);
        gwy_convert_raw_data(str->str, npts, 1, GWY_RAW_DATA_FLOAT, GWY_BYTE_ORDER_LITTLE_ENDIAN, d,
                             pfile->power10[i], 0.0);
    }
    guint npts = pfile->databuf->len/n;
    gwy_debug("items per curve %u (%u items total)", npts, pfile->databuf->len);
    gwy_lawn_set_curves(lawn, col, row, npts, &g_array_index(pfile->databuf, gdouble, 0), NULL);

    return TRUE;
}

static gboolean
check_string_list_item(JsonObject *root, const gchar *name, gchar **values, guint i, GError **error)
{
    JsonNode *item;

    if (!get_json_with_type(root, &item, name, G_TYPE_STRING, error))
        return FALSE;
    if (!values[i])
        values[i] = g_strdup(json_node_get_string(item));
    else if (!gwy_strequal(json_node_get_string(item), values[i]))
        return err_INCONSISTENT_SPECTRA(error);
    return TRUE;
}

static gboolean
format_atomic_json_meta(JsonNode *node, GwyDict *meta, const gchar *path)
{
    GType type = json_node_get_value_type(node);

    if (type == G_TYPE_STRING)
        gwy_dict_set_string_by_name(meta, path, json_node_get_string(node));
    else if (type == G_TYPE_INT64)
        gwy_dict_pass_string_by_name(meta, path, g_strdup_printf("%" G_GINT64_FORMAT, json_node_get_int(node)));
    else if (type == G_TYPE_DOUBLE)
        gwy_dict_pass_string_by_name(meta, path, g_strdup_printf("%g", json_node_get_double(node)));
    else if (type == G_TYPE_BOOLEAN)
        gwy_dict_set_string_by_name(meta, path, json_node_get_boolean(node) ? "True" : "False");
    else
        return FALSE;

    return TRUE;
}

static void
add_one_meta(GwyDict *meta, JsonObject *object, GString *path)
{
    guint len = path->len;

    g_string_append(path, "::");

    JsonObjectIter iter;
    json_object_iter_init(&iter, object);

    const gchar *name;
    JsonNode *node;
    while (json_object_iter_next(&iter, &name, &node)) {
        if (gwy_strequal(name, "type"))
            continue;

        g_string_append(path, name);

        if (format_atomic_json_meta(node, meta, path->str)) {
            /* pass */
        }
        else if (JSON_NODE_HOLDS_OBJECT(node))
            add_one_meta(meta, json_node_get_object(node), path);
        else {
            g_warning("Unhandled metadata of type %s.", g_type_name(json_node_get_value_type(node)));
        }
        g_string_truncate(path, len+2);
    }
    g_string_truncate(path, len);
}

static GwyFile*
load_file(const gchar *filename,
          G_GNUC_UNUSED GwyRunModeFlags mode,
          GError **error)
{
    GwyFile *file = NULL;
    PSPPTFile pfile;
    JsonParser *parser = NULL;
    const guchar *buffer = NULL;
    GError *err = NULL;
    gsize pos, size = 0;
    gboolean waiting = FALSE;
    guint i;

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

    gwy_clear1(pfile);
    pfile.str = g_string_new(NULL);
    pfile.databuf = g_array_new(FALSE, FALSE, sizeof(gdouble));

    pos = 0;
    if (!psppt_read_header(&pfile.header, buffer, size, &pos, error))
        goto fail;

    if (mode == GWY_RUN_INTERACTIVE) {
        gwy_app_wait_start(NULL, _("Reading frame table..."));
        waiting = TRUE;
    }

    if (!psppt_read_frame_table(&pfile, buffer, size, &pos, error))
        goto fail;

    if (waiting && !gwy_app_wait_set_message(_("Reading curve data..."))) {
        err_CANCELLED(error);
        goto fail;
    }

    parser = json_parser_new_immutable();
    for (i = 0; i < pfile.nframes; i++) {
        PSPPTFrame *frame = pfile.frames + i;
        gboolean ok;

        if (waiting && i % 100 == 0 && !gwy_app_wait_set_fraction((i + 0.5)/pfile.nframes)) {
            err_CANCELLED(error);
            goto fail;
        }
        JsonObject *root;
        if (!(root = psppt_read_frame(parser, frame, buffer, error)))
            goto fail;

        /* The types have been checked so we can be a bit less paranoid here. */
        if (i == 0)
            ok = handle_scan_start(&pfile.scanstart, root, error);
        else if (i == 1 || frame->type == PSPPT_PARAM)
            ok = handle_param(&pfile.param, root, error);
        else if (i == pfile.nframes-1)
            ok = handle_scan_stop(&pfile.scanstop, root, error);
        else
            ok = handle_rtfd(&pfile, root, error);
        if (!ok)
            goto fail;
    }

    pfile.meta = gwy_dict_new_in_construction();
    g_string_assign(pfile.str, "Param");
    add_one_meta(pfile.meta, pfile.param.root, pfile.str);
    g_string_assign(pfile.str, "Scan");
    add_one_meta(pfile.meta, pfile.scanstart.root, pfile.str);

    file = gwy_file_new_in_construction();
    gwy_file_set_cmap(file, 0, pfile.lawn);
    gwy_file_set_title(file, GWY_FILE_CMAP, 0, pfile.scanstart.direction, FALSE);
    gwy_file_pass_meta(file, GWY_FILE_CMAP, 0, pfile.meta);
    pfile.meta = NULL;
    gwy_log_add_import(file, GWY_FILE_CMAP, 0, NULL, filename);

fail:
    if (waiting)
        gwy_app_wait_finish();
    if (pfile.scanstart.root)
        json_object_unref(pfile.scanstart.root);
    if (pfile.scanstop.root)
        json_object_unref(pfile.scanstop.root);
    if (pfile.param.root)
        json_object_unref(pfile.param.root);
    g_clear_object(&parser);
    g_free(pfile.scanstart.direction);
    g_free(pfile.power10);
    g_free(pfile.reorder);
    g_strfreev(pfile.ids);
    g_strfreev(pfile.units);
    g_free(pfile.frames);
    g_clear_object(&pfile.lawn);
    g_clear_object(&pfile.meta);
    g_array_free(pfile.databuf, TRUE);
    g_string_free(pfile.str, TRUE);
    gwy_unmap_file_contents(buffer);

    return file;
}

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