/*
 *  $Id: qnami.c 29617 2026-03-18 09:10:50Z yeti-dn $
 *  Copyright (C) 2025-2026 David Necas (Yeti).
 *
 *  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-qnami-proteusq-spm">
 *   <comment>Qnami ProteusQ SPM data</comment>
 *   <magic priority="50">
 *     <match type="string" offset="0" value="# {'"/>
 *   </magic>
 *   <glob pattern="*.dat"/>
 *   <glob pattern="*.DAT"/>
 * </mime-type>
 **/

/**
 * [FILE-MAGIC-USERGUIDE]
 * Qnami ProteusQ
 * .dat
 * Read
 **/

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

#include "err.h"

#define MAGIC "# {'"
#define EXTENSION ".dat"

static gboolean    module_register(void);
static gint        detect_file    (const GwyFileDetectInfo *fileinfo,
                                   gboolean only_name);
static GwyFile*    import_file    (const gchar *filename,
                                   GwyRunModeFlags mode,
                                   GError **error);
static GHashTable* read_header    (const gchar *str,
                                   gsize len,
                                   GError **error);

static GwyModuleInfo module_info = {
    GWY_MODULE_ABI_VERSION,
    &module_register,
    N_("Imports Qnami ProteusQ SPM files."),
    "Yeti <yeti@gwyddion.net>",
    "0.2",
    "David Nečas (Yeti)",
    "2025",
};

GWY_MODULE_QUERY2(module_info, qnami)

static gboolean
module_register(void)
{
    gwy_file_func_register("qnami",
                           N_("Qnami ProteusQ files (.dat)"),
                           detect_file,
                           import_file,
                           NULL,
                           NULL);

    return TRUE;
}

static gint
detect_file(const GwyFileDetectInfo *fileinfo,
            gboolean only_name)
{
    static const gchar *keys[] = {
        "'xstart':", "'xstop':", "'ystart':", "'ystop':", "'signal_name':", "'xnum':", "'ynum':"
    };
    const gchar *head, *eol;
    guint i, len;
    gint antiscore = 90;

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

    /* There is no real magic header. Try to weed out files which are clearly not in the format. */
    if (fileinfo->buffer_len < 120 || strncmp(fileinfo->head, MAGIC, sizeof(MAGIC)-1))
        return 0;

    head = fileinfo->head + 3;
    len = fileinfo->buffer_len - 3;
    /* The line can be longer than our buffer. */
    if (!(eol = strchr(fileinfo->head, '\n')))
        len = eol - head;

    for (i = 0; i < G_N_ELEMENTS(keys); i++) {
        if (gwy_memmem(head, len, keys[i], strlen(keys[i]))) {
            gwy_debug("found %s", keys[i]);
            antiscore = 2*antiscore/3;
        }
    }

    return 100 - antiscore;
}

static GwyFile*
import_file(const gchar *filename,
            G_GNUC_UNUSED GwyRunModeFlags mode,
            GError **error)
{
    GwyFile *file = NULL;
    GwyField *field = NULL;
    const guchar *p, *buffer = NULL;
    const gchar *s;
    GHashTable *hash = NULL;
    const guchar *eol;
    gsize size = 0;
    gint xres, yres;
    gdouble xoff, xreal, yoff, yreal;
    GError *err = NULL;

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

    if (strncmp(buffer, MAGIC, MIN(size, sizeof(MAGIC)-1))) {
        err_FILE_TYPE(error, "Qnami ProteusQ");
        goto fail;
    }

    if (!(eol = strchr(buffer, '\n'))) {
        err_FILE_TYPE(error, "Qnami ProteusQ");
        goto fail;
    }

    p = buffer + 2;
    if (!(hash = read_header(p, eol - buffer - 2, error)))
        goto fail;
    if (!require_keys(hash, error, "xnum", "ynum", NULL))
        goto fail;

    xres = atoi(g_hash_table_lookup(hash, "xnum"));
    yres = atoi(g_hash_table_lookup(hash, "ynum"));
    if (err_DIMENSION(error, xres) || err_DIMENSION(error, yres))
        goto fail;

    xoff = g_ascii_strtod(g_hash_table_lookup(hash, "xstart"), NULL);
    xreal = g_ascii_strtod(g_hash_table_lookup(hash, "xstop"), NULL);
    GWY_ORDER(gdouble, xoff, xreal);
    xreal -= xoff;
    sanitise_real_size(&xreal, "x size");

    yoff = g_ascii_strtod(g_hash_table_lookup(hash, "ystart"), NULL);
    yreal = g_ascii_strtod(g_hash_table_lookup(hash, "ystop"), NULL);
    GWY_ORDER(gdouble, yoff, yreal);
    yreal -= yoff;
    sanitise_real_size(&yreal, "y size");

    field = gwy_field_new(xres, yres, xreal, yreal, FALSE);
    gwy_unit_set_from_string(gwy_field_get_unit_xy(field), "m");
    gwy_unit_set_from_string(gwy_field_get_unit_z(field), "m");

    p = eol+1;
    if (!gwy_parse_doubles(p, gwy_field_get_data(field), 0, &yres, &xres, NULL, &err)) {
        err_PARSE_DOUBLES(error, &err);
        g_object_unref(field);
        goto fail;
    }

    file = gwy_file_new_in_construction();
    gwy_file_pass_image(file, 0, field);

    if ((s = g_hash_table_lookup(hash, "signal_name")))
        gwy_file_set_title(file, GWY_FILE_IMAGE, 0, s, FALSE);
    else
        gwy_image_title_fall_back(file, 0);

    gwy_log_add_import(file, GWY_FILE_IMAGE, 0, NULL, filename);

fail:
    gwy_unmap_file_contents(buffer);
    if (hash)
        g_hash_table_destroy(hash);

    return file;
}

static void
err_HEADER_STRUCTURE(GError **error)
{
    g_set_error(error, GWY_MODULE_FILE_ERROR, GWY_MODULE_FILE_ERROR_DATA,
                _("Invalid file header structure."));
}

static const gchar*
find_string_end(const gchar *s)
{
    while (*s) {
        if (*s == '\'')
            return s;
        if (*s == '\\') {
            if (!s[1])
                return NULL;
            s++;
        }
        s++;
    }
    return NULL;
}

static gboolean
skip_space(const gchar **p)
{
    const gchar *q = *p;
    while (g_ascii_isspace(*q))
        q++;
    if (!*q)
        return FALSE;
    *p = q;
    return TRUE;
}

/* XXX: They use a weird JSON-like format, but with single quotes. Why? Is it just to make it harder for people to
 * parse the header because a standard JSON parse cannot be used? */
static GHashTable*
read_header(const gchar *str, gsize len, GError **error)
{
    GHashTable *hash = NULL;
    const gchar *p, *end;
    gchar *key = NULL, *value = NULL, *header = g_strstrip(g_strndup(str, len));
    gboolean ok = FALSE;

    len = strlen(header);

    if (len < 2 || header[0] != '{' || header[len-1] != '}') {
        gwy_debug("bad {}");
        err_HEADER_STRUCTURE(error);
        goto fail;
    }
    header[0] = ' ';
    header[len-1] = ',';

    hash = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
    p = header;
    /* Parse a simple single-level JSON-like thingy. */
    while (TRUE) {
        if (!skip_space(&p))
            break;

        if (*p != '\'') {
            gwy_debug("opening quote");
            err_HEADER_STRUCTURE(error);
            goto fail;
        }

        p++;
        if (!(end = find_string_end(p))) {
            gwy_debug("closing quote");
            err_HEADER_STRUCTURE(error);
            goto fail;
        }
        key = g_strndup(p, end-p);
        gwy_debug("key <%s>", key);

        p = end+1;
        if (!skip_space(&p) || *p != ':') {
            gwy_debug("colon");
            err_HEADER_STRUCTURE(error);
            goto fail;
        }
        p++;

        if (!skip_space(&p)) {
            err_HEADER_STRUCTURE(error);
            goto fail;
        }

        if (*p == '\'') {
            p++;
            if (!(end = find_string_end(p))) {
                gwy_debug("string end");
                err_HEADER_STRUCTURE(error);
                goto fail;
            }
            value = g_strndup(p, end-p);
            if (strchr(value, '\\')) {
                gchar *q = g_strcompress(value);
                GWY_SWAP(gchar*, value, q);
                g_free(q);
            }
            g_strstrip(value);
            gwy_debug("string value <%s>", value);
            p = end+1;

            if (!skip_space(&p) || *p != ',') {
                gwy_debug("comma");
                err_HEADER_STRUCTURE(error);
                goto fail;
            }
            p++;
        }
        else {
            if (!(end = strchr(p, ','))) {
                gwy_debug("comma");
                err_HEADER_STRUCTURE(error);
                goto fail;
            }
            value = g_strndup(p, end-p);
            gwy_debug("other value <%s>", value);
            p = end+1;
        }
        g_hash_table_insert(hash, key, value);
        key = value = NULL;
    }

    ok = TRUE;

fail:
    g_free(header);
    g_free(value);
    g_free(key);
    if (!ok && hash) {
        g_hash_table_destroy(hash);
        hash = NULL;
    }
    return hash;
}

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