/*
 *  $Id: talos.c 29827 2026-04-16 18:08:35Z yeti-dn $
 *  Copyright (C) 2025 David Necas (Yeti).
 *  E-mail: yeti@gwyddion.net.
 *
 *  This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public
 *  License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any
 *  later version.
 *
 *  This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
 *  warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
 *  details.
 *
 *  You should have received a copy of the GNU General Public License along with this program; if not, write to the
 *  Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */

/**
 * [FILE-MAGIC-USERGUIDE]
 * ThermoFisher Talos TEM images
 * .tif
 * Read
 **/

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

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

#define MAGIC_COMMENT "<Root><Data><Label>Microscope</Label><Value>"

enum {
    TALOS_TIFFTAG_XML             = 34682,
    TALOS_TIFFTAG_X_RESOLUTION    = 65450,
    TALOS_TIFFTAG_Y_RESOLUTION    = 65451,
    TALOS_TIFFTAG_RESOLUTION_UNIT = 65452,
};

typedef struct {
    GString *path;
    GString *label;
    GString *value;
    GString *unit;
    GwyDict *meta;
} MetaParserData;

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 GwyFile*      load_file_tiff (const GwyTIFF *tiff,
                                     const gchar *filename,
                                     GError **error);
static GwyDict* get_meta       (const gchar *comment);

static GwyModuleInfo module_info = {
    GWY_MODULE_ABI_VERSION,
    module_register,
    N_("Imports ThermoFisher Talos TEM images."),
    "Yeti <yeti@gwyddion.net>",
    "1.1",
    "David Nečas (Yeti)",
    "2025",
};

GWY_MODULE_QUERY2(module_info, talos)

static gboolean
module_register(void)
{
    gwy_file_func_register("talos",
                           N_("ThermoFisher Talos TEM image (.tif)"),
                           &detect_file, &load_file, NULL, NULL);

    return TRUE;
}

static gint
detect_file(const GwyFileDetectInfo *fileinfo, gboolean only_name)
{
    GwyTIFF *tiff;
    gint score = 0;
    gchar *comment = NULL;
    GwyTIFFVersion version = GWY_TIFF_CLASSIC;
    guint byteorder = G_LITTLE_ENDIAN;

    if (only_name)
        return score;

    /* Weed out non-TIFFs */
    if (!gwy_tiff_detect(fileinfo->head, fileinfo->buffer_len, &version, &byteorder))
        return 0;

    /* Use GwyTIFF for detection to avoid problems with fragile libtiff. */
    if ((tiff = gwy_tiff_load(fileinfo->name, NULL))
        && gwy_tiff_get_string0(tiff, TALOS_TIFFTAG_XML, &comment)
        && !memcmp(comment, MAGIC_COMMENT, sizeof(MAGIC_COMMENT)-1)) {
        guint resunit;
        gdouble resx, resy;

        score = 20;
        /* The detection is only really good for anything if we can read the physical dimensions. */
        if (gwy_tiff_get_uint(tiff, 0, TALOS_TIFFTAG_RESOLUTION_UNIT, &resunit)
            && gwy_tiff_get_float(tiff, 0, TALOS_TIFFTAG_X_RESOLUTION, &resx)
            && gwy_tiff_get_float(tiff, 0, TALOS_TIFFTAG_Y_RESOLUTION, &resy)) {
            score = 100;
        }
    }

    g_free(comment);
    if (tiff)
        gwy_tiff_free(tiff);

    return score;
}

static GwyFile*
load_file(const gchar *filename,
          G_GNUC_UNUSED GwyRunModeFlags mode,
          GError **error)
{
    GwyTIFF *tiff = gwy_tiff_load(filename, error);
    if (!tiff)
        return NULL;

    GwyFile *file = load_file_tiff(tiff, filename, error);
    gwy_tiff_free(tiff);

    return file;
}

static GwyFile*
load_file_tiff(const GwyTIFF *tiff, const gchar *filename, GError **error)
{
    GwyFile *file = NULL;
    GwyDict *meta;
    GwyField *dfield;
    GwyTIFFImageReader *reader = NULL;
    gint i;
    gchar *comment = NULL;
    guint resunit;
    gdouble *data;
    gdouble resx, resy, step, q, xreal, yreal;
    gboolean tags_ok;

    if (!gwy_tiff_get_string0(tiff, TALOS_TIFFTAG_XML, &comment) || !strstr(comment, MAGIC_COMMENT)) {
        err_FILE_TYPE(error, "ThermoFisher Talos");
        goto fail;
    }

    tags_ok = (gwy_tiff_get_uint(tiff, 0, TALOS_TIFFTAG_RESOLUTION_UNIT, &resunit)
               && gwy_tiff_get_float(tiff, 0, TALOS_TIFFTAG_X_RESOLUTION, &resx)
               && gwy_tiff_get_float(tiff, 0, TALOS_TIFFTAG_Y_RESOLUTION, &resy));

    gwy_debug("resx = %g, resy = %g, unit = %d", resx, resy, resunit);

    if (!(reader = gwy_tiff_get_image_reader(tiff, 0, 3, error)))
        goto fail;

    if (!tags_ok) {
        g_warning("Missing expected TIFF tags.");
        resx = resy = 1.0;
        resunit = 2;
    }

    step = 0.0254; // The default resolution unit is inch.
    if (resunit == 2)
        step = 0.0254;
    else if (resunit == 3)
        step = 0.01;
    else {
        g_warning("Cannot interpret resolution unit %u.", resunit);
    }

    xreal = reader->width * step/resx;
    yreal = reader->height * step/resy;
    sanitise_real_size(&xreal, "x scale");
    sanitise_real_size(&yreal, "y sscale");

    dfield = gwy_field_new(reader->width, reader->height, xreal, yreal, FALSE);
    gwy_unit_set_from_string(gwy_field_get_unit_xy(dfield), "m");

    data = gwy_field_get_data(dfield);
    q = 1.0/((1 << reader->bits_per_sample) - 1);
    for (i = 0; i < reader->height; i++)
        gwy_tiff_read_image_row_averaged(tiff, reader, i, q, 0.0, data + i*reader->width);

    file = gwy_file_new_in_construction();
    gwy_file_pass_image(file, 0, dfield);
    gwy_file_set_title(file, GWY_FILE_IMAGE, 0, "TEM", FALSE);
    if ((meta = get_meta(comment)))
        gwy_file_pass_meta(file, GWY_FILE_IMAGE, 0, meta);
    gwy_log_add_import(file, GWY_FILE_IMAGE, 0, NULL, filename);

fail:
    g_free(comment);
    if (reader) {
        gwy_tiff_image_reader_free(reader);
        reader = NULL;
    }

    return file;
}

static void
strip_gstring(GString *str)
{
    g_strstrip(str->str);
    g_string_set_size(str, strlen(str->str));
}

static void
start_meta_element(G_GNUC_UNUSED GMarkupParseContext *context,
                   const gchar *element_name,
                   G_GNUC_UNUSED const gchar **attribute_names,
                   G_GNUC_UNUSED const gchar **attribute_values,
                   gpointer user_data,
                   G_GNUC_UNUSED GError **error)
{
    MetaParserData *mpd = (MetaParserData*)user_data;

    if (mpd->path->len)
        g_string_append(mpd->path, "::");
    g_string_append(mpd->path, element_name);

    if (gwy_strequal(element_name, "Data")) {
        g_string_truncate(mpd->label, 0);
        g_string_truncate(mpd->value, 0);
        g_string_truncate(mpd->unit, 0);
    }
}

static void
end_meta_element(G_GNUC_UNUSED GMarkupParseContext *context,
                 const gchar *element_name,
                 gpointer user_data,
                 G_GNUC_UNUSED GError **error)
{
    MetaParserData *mpd = (MetaParserData*)user_data;
    const gchar *p;

    if ((p = g_strrstr(mpd->path->str, "::")))
        g_string_truncate(mpd->path, p - mpd->path->str);
    else
        g_string_truncate(mpd->path, 0);

    if (gwy_strequal(element_name, "Data")) {
        strip_gstring(mpd->label);
        strip_gstring(mpd->value);
        strip_gstring(mpd->unit);
        if (mpd->label->len) {
            if (mpd->unit->len) {
                if (mpd->value->len)
                    g_string_append_c(mpd->value, ' ');
                g_string_append(mpd->value, mpd->unit->str);
            }
            gwy_debug("<%s> = <%s>", mpd->label->str, mpd->value->str);
            if (mpd->value->len)
                gwy_dict_set_string_by_name(mpd->meta, mpd->label->str, mpd->value->str);
        }
    }
}

static void
meta_text(G_GNUC_UNUSED GMarkupParseContext *context,
          const gchar *text,
          G_GNUC_UNUSED gsize text_len,
          gpointer user_data,
          G_GNUC_UNUSED GError **error)
{
    MetaParserData *mpd = (MetaParserData*)user_data;

    if (g_str_has_suffix(mpd->path->str, "::Data::Label"))
        g_string_assign(mpd->label, text);
    else if (g_str_has_suffix(mpd->path->str, "::Data::Value"))
        g_string_assign(mpd->value, text);
    else if (g_str_has_suffix(mpd->path->str, "::Data::Unit"))
        g_string_assign(mpd->unit, text);
}

static GwyDict*
get_meta(const gchar *comment)
{
    GMarkupParser parser = {
        start_meta_element, end_meta_element, meta_text, NULL, NULL,
    };
    MetaParserData mpd;
    GMarkupParseContext *context;

    mpd.path = g_string_new(NULL);
    mpd.label = g_string_new(NULL);
    mpd.value = g_string_new(NULL);
    mpd.unit = g_string_new(NULL);
    mpd.meta = gwy_dict_new_in_construction();

    context = g_markup_parse_context_new(&parser, G_MARKUP_TREAT_CDATA_AS_TEXT, &mpd, NULL);
    g_markup_parse_context_parse(context, comment, -1, NULL);
    g_markup_parse_context_free(context);

    g_string_free(mpd.path, TRUE);
    g_string_free(mpd.label, TRUE);
    g_string_free(mpd.value, TRUE);
    g_string_free(mpd.unit, TRUE);

    if (!gwy_dict_get_n_items(mpd.meta)) {
        g_object_unref(mpd.meta);
        return NULL;
    }

    return mpd.meta;
}

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