/*
 *  $Id: tiaser.c 29724 2026-04-02 16:59:53Z yeti-dn $
 *  Copyright (C) 2012-2025 David Necas (Yeti), Daniil Bratashov (dn2010).
 *  E-mail: yeti@gwyddion.net, dn2010@gmail.com.
 *
 *  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.
 */

/*
 *  Format description from here:
 *  http://www.microscopy.cen.dtu.dk/~cbb/info/TIAformat/index.html
 *  or more recent copy on:
 *  http://www.er-c.org/cbb/info/TIAformat/index.html
 */

/**
 * [FILE-MAGIC-FREEDESKTOP]
 * <mime-type type="application/x-tiaser-tem">
 *   <comment>FEI TIA (Emispec) data</comment>
 *   <magic priority="80">
 *     <match type="string" offset="0" word-size="2" value="\x49\x49\x01\x97"/>
 *   </magic>
 *   <glob pattern="*.ser"/>
 *   <glob pattern="*.SER"/>
 * </mime-type>
 **/

/**
 * [FILE-MAGIC-FILEMAGIC]
 * # FEI TIA/Emispec
 * 0 string \x49\x49\x97\x01 FEI Tecnai imaging and analysis (S)TEM data
 **/

/**
 * [FILE-MAGIC-USERGUIDE]
 * FEI Tecnai imaging and analysis (former Emispec) data
 * .ser
 * Read SPS Volume
 **/

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

#include "err.h"

#define MAGIC1 "\x49\x49\x97\x01"
#define MAGIC2 "\x49\x49\x01\x97"
#define MAGIC_SIZE (sizeof(MAGIC1)-1)

#define EXTENSION ".ser"

typedef enum {
    TIA_DATA_UINT8          = 1,
    TIA_DATA_UINT16         = 2,
    TIA_DATA_UINT32         = 3,
    TIA_DATA_INT8           = 4,
    TIA_DATA_INT16          = 5,
    TIA_DATA_INT32          = 6,
    TIA_DATA_FLOAT          = 7,
    TIA_DATA_DOUBLE         = 8,
    TIA_DATA_FLOAT_COMPLEX  = 9,
    TIA_DATA_DOUBLE_COMPLEX = 10
} TIADataType;

typedef enum {
    TIA_ES_LE        = 0x4949,
    TIA_ES_MAGIC     = 0x0197,
    TIA_ES_VERSION   = 0x0210,
    TIA_1D_DATA      = 0x4120,
    TIA_2D_DATA      = 0x4122,
    TIA_TAG_TIME     = 0x4152,
    TIA_TAG_TIMEPOS  = 0x4142,
    TIA_HEADER_SIZE  = 3 * 2 + 6 * 4,
    TIA_DIM_SIZE     = 4 * 4 + 2 * 8,
    TIA_2D_SIZE      = 50,
    TIA_1D_SIZE      = 26,
    TIA_TIMEPOS_SIZE = 22,
} TIAConsts;

typedef struct {
    gint16 byteorder;
    gint16 seriesid;
    gint16 seriesversion;
    gint32 datatypeid;
    gint32 tagtypeid;
    gint32 totalnumberelements;
    gint32 validnumberelements;
    gint32 offsetarrayoffset;
    gint32 numberdimensions;
} TIAHeader;

typedef struct {
    gint32  numelements;
    gdouble calibrationoffset;
    gdouble calibrationdelta;
    gint32  calibrationelement;
    gint32  descriptionlength;
    gchar  *description;
    gint32  unitslength;
    gchar  *units;
} TIADimension;

typedef struct {
    gint16  tagtypeid;
    gint32  time;
} TIATimeTag;

typedef struct {
    gint16  tagtypeid;
    gint32  time;
    gdouble positionx;
    gdouble positiony;
} TIATimePosTag;

typedef struct {
    gdouble       calibrationoffset;
    gdouble       calibrationdelta;
    gint32        calibrationelement;
    TIADataType   datatype;
    gint32        arraylength;
    const guchar *data;
} TIA1DData;

typedef struct {
    gdouble       calibrationoffsetx;
    gdouble       calibrationdeltax;
    gint32        calibrationelementx;
    gdouble       calibrationoffsety;
    gdouble       calibrationdeltay;
    gint32        calibrationelementy;
    TIADataType   datatype;
    guint32       arraylengthx;
    guint32       arraylengthy;
    const guchar *data;
} TIA2DData;

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 void           load_header             (const guchar *p,
                                               TIAHeader *header);
static gboolean       check_header            (TIAHeader *header,
                                               gsize size);
static gboolean       load_dimarray           (const guchar *p,
                                               TIADimension *dimarray,
                                               gsize size);
static GwyField*      read_2d_image           (const guchar *p,
                                               gsize size);
static GwyLine*       read_1d_line            (const guchar *p,
                                               gsize size);
static GwyRawDataType tia_datatype_to_raw_type(TIADataType datatype,
                                               gdouble *q,
                                               gboolean *is_complex);

static GwyModuleInfo module_info = {
    GWY_MODULE_ABI_VERSION,
    &module_register,
    N_("Imports FEI Tecnai imaging and analysis (former Emispec) files."),
    "dn2010 <dn2010@gmail.com>",
    "0.6",
    "David Nečas (Yeti), Daniil Bratashov (dn2010)",
    "2012",
};

GWY_MODULE_QUERY2(module_info, tiaser)

static gboolean
module_register(void)
{
    gwy_file_func_register("tiaser",
                           N_("FEI TIA (Emispec) data"),
                           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, MAGIC1, MAGIC_SIZE) == 0)
        score = 100;
    if (fileinfo->buffer_len > MAGIC_SIZE && memcmp(fileinfo->head, MAGIC2, MAGIC_SIZE) == 0)
        score = 100;

    return score;
}

static void load_header(const guchar *p, TIAHeader *header)
{
    header->byteorder           = gwy_get_guint16_le(&p);
    header->seriesid            = gwy_get_guint16_le(&p);
    header->seriesversion       = gwy_get_guint16_le(&p);
    header->datatypeid          = gwy_get_guint32_le(&p);
    header->tagtypeid           = gwy_get_guint32_le(&p);
    header->totalnumberelements = gwy_get_guint32_le(&p);
    header->validnumberelements = gwy_get_guint32_le(&p);
    header->offsetarrayoffset   = gwy_get_guint32_le(&p);
    header->numberdimensions    = gwy_get_guint32_le(&p);

    gwy_debug("bo=%X si=%X sv=%X dtid=%X ttid=%X",
              header->byteorder,
              header->seriesid,
              header->seriesversion,
              header->datatypeid,
              header->tagtypeid);
    gwy_debug("elemtot=%i elemvalid=%i offset=%i ndim=%i",
              header->totalnumberelements,
              header->validnumberelements,
              header->offsetarrayoffset,
              header->numberdimensions);
}

static gboolean
check_header(TIAHeader *header, gsize size)
{
    if ((header->byteorder != TIA_ES_LE)
        || (header->seriesid != TIA_ES_MAGIC)
        || (header->seriesversion != TIA_ES_VERSION)
        || ((header->datatypeid != TIA_1D_DATA)
            && (header->datatypeid != TIA_2D_DATA))
        || ((header->tagtypeid != TIA_TAG_TIME)
            && (header->tagtypeid != TIA_TAG_TIMEPOS))
        || (header->totalnumberelements < header->validnumberelements)
        || (header->offsetarrayoffset >= size)
        || (size-header->offsetarrayoffset < 8 * header->totalnumberelements))
        return FALSE;

    return TRUE;
}

static gboolean
load_dimarray(const guchar *p, TIADimension *dimarray, gsize size)
{
    dimarray->numelements        = gwy_get_guint32_le(&p);
    dimarray->calibrationoffset  = gwy_get_gdouble_le(&p);
    dimarray->calibrationdelta   = gwy_get_gdouble_le(&p);
    dimarray->calibrationelement = gwy_get_guint32_le(&p);
    gwy_debug("numelem=%i caloffset=%G caldelta=%G calelem=%i",
              dimarray->numelements,
              dimarray->calibrationoffset,
              dimarray->calibrationdelta,
              dimarray->calibrationelement);

    dimarray->descriptionlength  = gwy_get_guint32_le(&p);

    if (dimarray->descriptionlength >= size - TIA_HEADER_SIZE - TIA_DIM_SIZE) {
        return FALSE;
    }

    dimarray->description = g_strndup(p, dimarray->descriptionlength);
    p += dimarray->descriptionlength;
    dimarray->unitslength  = gwy_get_guint32_le(&p);

    if (dimarray->unitslength + dimarray->descriptionlength >= size - TIA_HEADER_SIZE - TIA_DIM_SIZE) {
        return FALSE;
    }

    dimarray->units = g_strndup(p, dimarray->unitslength);
    p += dimarray->unitslength;
    gwy_debug("descr = \"%s\" units=\"%s\"",
              dimarray->description,
              dimarray->units);

    return TRUE;
}

static GwyFile*
load_file(const gchar *filename,
          G_GNUC_UNUSED GwyRunModeFlags mode,
          GError **error)
{
    GwyFile *file = NULL;
    const guchar *p, *buffer = NULL;
    gsize size;
    GError *err = NULL;
    TIAHeader *header;
    TIADimension *dimension, dim;
    GArray *dimarray, *dataoffsets, *tagoffsets;
    GwyField *dfield;
    gint i, offset, dimarraylength, dimarraysize = 0;

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

    if (size < TIA_HEADER_SIZE) {
        err_TOO_SHORT(error);
        goto fail;
    }

    header = g_new0(TIAHeader, 1);
    p = buffer;
    load_header(p, header);
    if (!check_header(header, size)) {
        err_FILE_TYPE(error, "FEI TIA");
        goto fail2;
    }
    p += TIA_HEADER_SIZE;

    dimarraylength = header->numberdimensions;
    dimarray = g_array_sized_new(FALSE, TRUE, sizeof(TIADimension), dimarraylength);
    for (i = 0; i < dimarraylength; i++) {
        p += dimarraysize;
        gwy_clear1(dim);
        if (!load_dimarray(p, &dim, size - TIA_HEADER_SIZE - dimarraysize)) {
            err_FILE_TYPE(error, "FEI TIA");
            goto fail3;
        }
        dimarraysize += TIA_DIM_SIZE + dim.descriptionlength + dim.unitslength;
        g_array_append_val(dimarray, dim);
    }

    p = buffer+header->offsetarrayoffset;
    dataoffsets = g_array_new(FALSE, TRUE, sizeof(gint32));
    for (i = 0; i < header->totalnumberelements; i++) {
        offset = gwy_get_guint32_le(&p);
        g_array_append_val(dataoffsets, offset);
        if (offset > size) {
            goto dataoffsets_fail;
        }
    }
    tagoffsets = g_array_new(FALSE, TRUE, sizeof(gint32));
    for (i = 0; i < header->totalnumberelements; i++) {
        offset = gwy_get_guint32_le(&p);
        g_array_append_val(tagoffsets, offset);
        if (offset > size) {
            goto tagoffsets_fail;
        }
    }

    file = gwy_file_new_in_construction();
    if (header->datatypeid == TIA_2D_DATA)
        for (i = 0; i < header->validnumberelements; i++) {
            offset = g_array_index(dataoffsets, gint32, i);
            if ((offset > size) || (size-offset < TIA_2D_SIZE)) {
                gwy_debug("Attempt to read after EOF");
            }
            else {
                dfield = read_2d_image(buffer + offset, size - offset);
                if (dfield) {
                    gwy_file_pass_image(file, i, dfield);
                    gwy_file_set_title(file, GWY_FILE_IMAGE, i, "TEM", FALSE);
                    gwy_log_add_import(file, GWY_FILE_IMAGE, i, NULL, filename);
                }
            }
        }
    else if (header->datatypeid == TIA_1D_DATA) {
        /* XXX XXX XXX: What if the sizes of individual lines differ from the brick zres? This is just one big fucking
         * buffer overflow waiting to happen! If each has an independent length we should be probably creating a Lawn
         * anyway.
         *
         * Disabled for now. */
#if 0
        GwyBrick *brick;
        GwyLine *dline;
        GwyUnit *siunit;
        gdouble *value, *data;
        gint xres, yres, zres, xpos, ypos;
        gdouble xreal, xoffset, yreal, yoffset, zreal, zoffset;

        if (dimarray->len != 2) {
            gwy_debug("Wrong dimensions number");
            goto tagoffsets_fail;
        }
        dimension = &g_array_index(dimarray, TIADimension, 0);
        xres = dimension->numelements;
        xreal = dimension->numelements * dimension->calibrationdelta;
        sanitise_real_size(&xreal, "x size");
        xoffset = dimension->calibrationoffset - dimension->calibrationdelta * dimension->calibrationelement;
        dimension = &g_array_index(dimarray, TIADimension, 1);
        yres = dimension->numelements;
        yreal = dimension->numelements * dimension->calibrationdelta;
        sanitise_real_size(&yreal, "y size");
        yoffset = dimension->calibrationoffset - dimension->calibrationdelta * dimension->calibrationelement;

        offset = g_array_index(dataoffsets, gint32, 0);
        if ((offset > size) || (size-offset < TIA_1D_SIZE)) {
            gwy_debug("Attempt to read after EOF");
            goto tagoffsets_fail;
        }
        dline = read_1d_line(buffer + offset, size - offset);
        if (!dline) {
            goto tagoffsets_fail;
        }
        zres = gwy_line_get_res(dline);
        zreal = gwy_line_get_real(dline);
        zoffset = gwy_line_get_offset(dline);
        g_object_unref(dline);

        brick = gwy_brick_new(xres, yres, zres, xreal, yreal, zreal, TRUE);
        if (!brick) {
            goto tagoffsets_fail;
        }
        gwy_brick_set_xoffset(brick, xoffset);
        gwy_brick_set_yoffset(brick, yoffset);
        gwy_brick_set_zoffset(brick, zoffset);

        siunit = gwy_unit_new("m");
        gwy_unit_assign(gwy_brick_get_unit_x(brick), siunit);
        gwy_unit_assign(gwy_brick_get_unit_y(brick), siunit);
        g_object_unref(siunit);
        siunit = gwy_unit_new(NULL);
        gwy_unit_assign(gwy_brick_get_unit_z(brick), siunit);
        gwy_unit_assign(gwy_brick_get_unit_w(brick), siunit);
        g_object_unref(siunit);
        data = gwy_brick_get_data(brick);

        for (i = 0; i < header->validnumberelements; i++) {
            offset = g_array_index(dataoffsets, gint32, i);
            if ((offset > size) || (size-offset < TIA_1D_SIZE)) {
                gwy_debug("Attempt to read after EOF");
                goto tagoffsets_fail;
            }
            dline = read_1d_line(buffer + offset, size - offset);
            if (dline) {
                xpos = i % xres;
                ypos = (gint)(i / xres);
                zres = gwy_line_get_res(dline);
                value = (gdouble*)gwy_line_get_data_const(dline);
                for (k = 0; k < zres; k++)
                    *(data + k * xres * yres + ypos * xres + xpos) = *(value++);
            }
            else
                break;
            g_object_unref(dline);
        }
        if (brick) {
            gwy_file_pass_volume(file, 0, brick);
            gwy_file_set_title(file, GWY_FILE_VOLUME, 0, "TEM Spectroscopy", FALSE);
            gwy_log_add_import(file, GWY_FILE_VOLUME, 0, NULL, filename);
        }
#endif
    }

tagoffsets_fail:
    g_array_free(tagoffsets, TRUE);
dataoffsets_fail:
    g_array_free(dataoffsets, TRUE);
fail3:
    for (i = 0; i < dimarray->len; i++) {
        dimension = &g_array_index(dimarray, TIADimension, i);
        g_free(dimension->description);
        g_free(dimension->units);
    }
    g_array_free(dimarray, TRUE);
fail2:
    g_free(header);
fail:
    gwy_unmap_file_contents(buffer);
    if (file && !gwy_dict_get_n_items(GWY_DICT(file))) {
        err_NO_DATA(error);
        g_clear_object(&file);
    }

    return file;
}

static GwyField*
read_2d_image(const guchar *p, gsize size)
{
    TIA2DData info;
    info.calibrationoffsetx  = gwy_get_gdouble_le(&p);
    info.calibrationdeltax   = gwy_get_gdouble_le(&p);
    info.calibrationelementx = gwy_get_guint32_le(&p);
    info.calibrationoffsety  = gwy_get_gdouble_le(&p);
    info.calibrationdeltay   = gwy_get_gdouble_le(&p);
    info.calibrationelementy = gwy_get_guint32_le(&p);
    info.datatype            = (TIADataType)gwy_get_guint16_le(&p);
    info.arraylengthx        = gwy_get_guint32_le(&p);
    info.arraylengthy        = gwy_get_guint32_le(&p);
    info.data                = p;

    gboolean is_complex;
    gdouble q;
    GwyRawDataType rawtype = tia_datatype_to_raw_type(info.datatype, &q, &is_complex);

    if (rawtype == (GwyRawDataType)G_MAXUINT || is_complex) {
        gwy_debug("Unsupported datatype");
        return NULL;
    }

    gsize itemsize = gwy_raw_data_size(rawtype) * (is_complex ? 2 : 1);
    if (size < 50 || (size - 50)/info.arraylengthx/info.arraylengthy < itemsize) {
        gwy_debug("data size mismatch");
        return NULL;
    }

    gwy_debug("X: caloffset=%G caldelta=%G calelem=%i",
              info.calibrationoffsetx, info.calibrationdeltax, info.calibrationelementx);
    gwy_debug("Y: caloffset=%G caldelta=%G calelem=%i",
              info.calibrationoffsety, info.calibrationdeltay, info.calibrationelementy);
    gwy_debug("nx=%i ny=%i type=%i",
              info.arraylengthx, info.arraylengthy, info.datatype);

    GwyField *dfield = gwy_field_new(info.arraylengthx,
                                     info.arraylengthy,
                                     info.arraylengthx*info.calibrationdeltax,
                                     info.arraylengthy*info.calibrationdeltay,
                                     TRUE);

    gwy_field_set_xoffset(dfield, info.calibrationoffsetx - info.calibrationdeltax * info.calibrationelementx);
    gwy_field_set_yoffset(dfield, info.calibrationoffsety - info.calibrationdeltay * info.calibrationelementy);
    gwy_unit_set_from_string(gwy_field_get_unit_xy(dfield), "m");
    gsize n = info.arraylengthx * info.arraylengthy;
    gwy_convert_raw_data(info.data, n, 1, rawtype, GWY_BYTE_ORDER_LITTLE_ENDIAN,
                         gwy_field_get_data(dfield), q, 0.0);

    return dfield;
}

static GwyLine*
read_1d_line(const guchar *p, gsize size)
{
    TIA1DData info;
    info.calibrationoffset  = gwy_get_gdouble_le(&p);
    info.calibrationdelta   = gwy_get_gdouble_le(&p);
    info.calibrationelement = gwy_get_guint32_le(&p);
    info.datatype           = (TIADataType)gwy_get_guint16_le(&p);
    info.arraylength        = gwy_get_guint32_le(&p);
    info.data               = p;

    gboolean is_complex;
    gdouble q;
    GwyRawDataType rawtype = tia_datatype_to_raw_type(info.datatype, &q, &is_complex);

    if (rawtype == (GwyRawDataType)G_MAXUINT || is_complex) {
        gwy_debug("Unsupported datatype");
        return NULL;
    }

    gsize itemsize = gwy_raw_data_size(rawtype) * (is_complex ? 2 : 1);
    if (size < 26 || (size - 26)/info.arraylength < itemsize) {
        gwy_debug("data size mismatch");
        return NULL;
    }

    GwyLine *dline = gwy_line_new(info.arraylength,
                                  info.arraylength * info.calibrationdelta,
                                  TRUE);

    gwy_line_set_offset(dline, info.calibrationoffset - info.calibrationdelta * info.calibrationelement);
    gsize n = info.arraylength;
    gwy_convert_raw_data(info.data, n, 1, rawtype, GWY_BYTE_ORDER_LITTLE_ENDIAN,
                         gwy_line_get_data(dline), q, 0.0);

    return dline;
}

static GwyRawDataType
tia_datatype_to_raw_type(TIADataType datatype, gdouble *q, gboolean *is_complex)
{
    static const struct {
        TIADataType tiatype;
        GwyRawDataType rawtype;
        gdouble q;
        gboolean is_complex;
    } conversion_table[] = {
        { TIA_DATA_UINT8,          GWY_RAW_DATA_UINT8,  1.0/G_MAXUINT8,  FALSE, },
        { TIA_DATA_UINT16,         GWY_RAW_DATA_UINT16, 1.0/G_MAXUINT16, FALSE, },
        { TIA_DATA_UINT32,         GWY_RAW_DATA_UINT32, 1.0/G_MAXUINT32, FALSE, },
        { TIA_DATA_INT8,           GWY_RAW_DATA_SINT8,  1.0/G_MAXINT8,   FALSE, },
        { TIA_DATA_INT16,          GWY_RAW_DATA_SINT16, 1.0/G_MAXINT16,  FALSE, },
        { TIA_DATA_INT32,          GWY_RAW_DATA_SINT32, 1.0/G_MAXINT32,  FALSE, },
        { TIA_DATA_FLOAT,          GWY_RAW_DATA_FLOAT,  1.0,             FALSE, },
        { TIA_DATA_DOUBLE,         GWY_RAW_DATA_DOUBLE, 1.0,             FALSE, },
        { TIA_DATA_FLOAT_COMPLEX,  GWY_RAW_DATA_FLOAT,  1.0,             TRUE,  },
        { TIA_DATA_DOUBLE_COMPLEX, GWY_RAW_DATA_DOUBLE, 1.0,             TRUE,  },
    };

    for (guint i = 0; i < G_N_ELEMENTS(conversion_table); i++) {
        if (conversion_table[i].tiatype == datatype) {
            *is_complex = conversion_table[i].is_complex;
            *q = conversion_table[i].q;
            return conversion_table[i].rawtype;
        }
    }
    return (GwyRawDataType)G_MAXUINT;
}

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