/*
 *  $Id: titan.c 29938 2026-05-05 16:33:51Z yeti-dn $
 *  Copyright (C) 2026 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 Titan 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 XML_MAGIC_COMMENT "<?xml version=\"1.0\" encoding=\"utf-8\"?>"

enum {
    TITAN_TIFFTAG_INFO            = 34682,
    TITAN_TIFFTAG_XML             = 34683,
    TITAN_TIFFTAG_X_RESOLUTION    = 65450,
    TITAN_TIFFTAG_Y_RESOLUTION    = 65451,
    TITAN_TIFFTAG_RESOLUTION_UNIT = 65452,
};

typedef struct {
    GString *path;
    GString *cpname;
    GString *value;
    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 GwyDict* get_meta       (const gchar *comment);

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

GWY_MODULE_QUERY2(module_info, titan)

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

    return TRUE;
}

static gint
detect_file(const GwyFileDetectInfo *fileinfo, gboolean only_name)
{
    static const gchar *expected_tags[] = {
        "<Medatata ",
        "<Core>",
        "<Guid>",
        "<ApplicationSoftware>",
        "<ApplicationSoftwareVersion>",
        "<Instrument>",
        "<ControlSoftwareVersion>",
        "<Manufacturer>",
        "<InstrumentClass>",
        "<InstrumentModel>",
        "<InstrumentID>",
        "<ComputerName>",
        "<Acquisition>",
        "<Optics>",
    };
    gint score = 0;

    if (only_name)
        return score;

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

    /* Use GwyTIFF for detection to avoid problems with fragile libtiff. */
    GwyTIFF *tiff;
    if ((tiff = gwy_tiff_load(fileinfo->name, NULL))) {
        gchar *comment = NULL;
        if (gwy_tiff_get_string0(tiff, TITAN_TIFFTAG_INFO, &comment)) {
            score += 20;
            GWY_FREE(comment);
        }

        if (gwy_tiff_get_string0(tiff, TITAN_TIFFTAG_XML, &comment)
            && !memcmp(comment, XML_MAGIC_COMMENT, sizeof(XML_MAGIC_COMMENT)-1)) {
            score += 20;
            // The XML can be pretty long and we are looking for things that should be at the beginning. Truncate it,
            // either for quicker success or less desperate failure.
            if (strlen(comment) > 1024)
                comment[1024] = '\0';
            for (guint i = 0; i < G_N_ELEMENTS(expected_tags) && score < 100; i++) {
                if (strstr(comment, expected_tags[i]))
                    score = 100 - 4*(100 - score)/5;
            }
        }
        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 = NULL;
    GwyDict *meta = NULL;
    GwyTIFFImageReader *reader = NULL;
    gchar *comment = NULL;

    if (!gwy_tiff_get_string0(tiff, TITAN_TIFFTAG_XML, &comment) || !strstr(comment, XML_MAGIC_COMMENT)) {
        err_FILE_TYPE(error, "ThermoFisher Titan");
        goto fail;
    }

    guint resunit;
    gdouble resx, resy;
    gboolean tags_ok = (gwy_tiff_get_uint(tiff, 0, TITAN_TIFFTAG_RESOLUTION_UNIT, &resunit)
                        && gwy_tiff_get_float(tiff, 0, TITAN_TIFFTAG_X_RESOLUTION, &resx)
                        && gwy_tiff_get_float(tiff, 0, TITAN_TIFFTAG_Y_RESOLUTION, &resy));
    gwy_debug("from tags: resx = %g, resy = %g, unit = %d", resx, resy, resunit);

    meta = get_meta(comment);

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

    // Prefer values from XML. The same info is also in the INFO tag, in yet again different form. Maybe too much?
    gboolean xml_dxy_ok = FALSE;
    gdouble dx, dy;
    if (meta) {
        const gchar *value;

        xml_dxy_ok = TRUE;
        if (gwy_dict_gis_string_by_name(meta, "BinaryResult::PixelSize::X", &value)) {
            dx = g_ascii_strtod(value, NULL);
            gwy_debug("PixelSize::X = %g", dx);
        }
        else
            xml_dxy_ok = FALSE;

        if (gwy_dict_gis_string_by_name(meta, "BinaryResult::PixelSize::Y", &value)) {
            dy = g_ascii_strtod(value, NULL);
            gwy_debug("PixelSize::Y = %g", dy);
        }
        else
            xml_dxy_ok = FALSE;
    }

    if (!xml_dxy_ok && tags_ok) {
        gdouble 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);
        }
        dx = step/resx;
        dy = step/resy;
    }
    else if (!tags_ok) {
        g_warning("Missing expected TIFF tags.");
        dx = dy = 0.0254;
    }

    gdouble xreal = reader->width * dx;
    gdouble yreal = reader->height * dy;
    sanitise_real_size(&xreal, "x scale");
    sanitise_real_size(&yreal, "y scale");

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

    gdouble *data = gwy_field_get_data(field);
    gdouble q = 1.0/((1 << reader->bits_per_sample) - 1);
    for (guint 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, field);
    gwy_file_set_title(file, GWY_FILE_IMAGE, 0, "TEM", FALSE);
    if (meta)
        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);
    gwy_tiff_free(tiff);
    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,
                   const gchar **attribute_names,
                   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, "CustomProperty") && strstr(mpd->path->str, "::CustomPropertyGroup::")) {
        const gchar *name = NULL, *value = NULL;

        for (guint i = 0; (!name || !value) && attribute_names[i]; i++) {
            if (gwy_strequal(attribute_names[i], "name"))
                name = attribute_values[i];
            else if (gwy_strequal(attribute_names[i], "value"))
                value = attribute_values[i];
        }
        if (name && value) {
            guint i = mpd->cpname->len;
            g_string_append(mpd->cpname, name);
            g_string_assign(mpd->value, value);
            strip_gstring(mpd->value);
            if (mpd->value->len)
                gwy_dict_set_string_by_name(mpd->meta, mpd->cpname->str, mpd->value->str);
            g_string_truncate(mpd->cpname, i);
        }
    }
}

static void
end_meta_element(G_GNUC_UNUSED GMarkupParseContext *context,
                 G_GNUC_UNUSED 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);
}

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 (!strstr(mpd->path->str, "::CustomPropertyGroup::")) {
        g_string_assign(mpd->value, text);
        strip_gstring(mpd->value);
        if (mpd->value->len) {
            const gchar *key = mpd->path->str;
            if (g_str_has_prefix(key, "Metadata::"))
                key += strlen("Metadata::");
            gwy_dict_set_string_by_name(mpd->meta, key, mpd->value->str);
        }
    }
}

static GwyDict*
get_meta(const gchar *comment)
{
    GMarkupParser parser = {
        start_meta_element, end_meta_element, meta_text, NULL, NULL,
    };
    MetaParserData mpd = {
        .path = g_string_new(NULL),
        .cpname = g_string_new("CustomProperty::"),
        .value = g_string_new(NULL),
        .meta = gwy_dict_new(),
    };

    GMarkupParseContext *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.cpname, TRUE);
    g_string_free(mpd.value, 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 : */
