/*
 *  $Id: bcrfile.c 29611 2026-03-17 10:31:06Z yeti-dn $
 *  Copyright (C) 2005-2025 David Necas (Yeti), Petr Klapetek.
 *  E-mail: yeti@gwyddion.net, klapetek@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-bcr-spm">
 *   <comment>BCR SPM data</comment>
 *   <magic priority="80">
 *     <match type="string" offset="0" value="fileformat = bcrstm\n"/>
 *     <match type="string" offset="0" value="f\0i\0l\0e\0f\0o\0r\0m\0a\0t\0 \0=\0 \0b\0c\0r\0s\0t\0m\0_\0u\0n\0i\0c\0o\0d\0e\0\n\0"/>
 *   </magic>
 *   <glob pattern="*.bcr"/>
 * </mime-type>
 **/

/**
 * [FILE-MAGIC-FREEDESKTOP]
 * <mime-type type="application/x-bcrf-spm">
 *   <comment>BCRF SPM data</comment>
 *   <magic priority="80">
 *     <match type="string" offset="0" value="fileformat = bcrf\n"/>
 *     <match type="string" offset="0" value="f\0i\0l\0e\0f\0o\0r\0m\0a\0t\0 \0=\0 \0b\0c\0r\0f\0_\0u\0n\0i\0c\0o\0d\0e\0\n\0"/>
 *   </magic>
 *   <glob pattern="*.bcrf"/>
 * </mime-type>
 **/

/**
 * [FILE-MAGIC-FILEMAGIC]
 * # Image Metrology BCR 3423/1/0/184/4/91-BCR-DK(30).
 * 0 string fileformat\ =\ bcrstm\x0a SPM data exchange format BCR, 16bit int data, ASCII
 * 0 string fileformat\ =\ bcrf\x0a SPM data exchange format BCR, 32bit float data, ASCII
 * 0 lestring16 fileformat\ =\ bcrstm_unicode\x0a SPM data exchange format BCR, 16bit int data, Unicode
 * 0 lestring16 fileformat\ =\ bcrf_unicode\x0a SPM data exchange format BCR, 32bit float data, Unicode
 **/

/**
 * [FILE-MAGIC-USERGUIDE]
 * Image Metrology BCR, BCRF
 * .bcr, .bcrf
 * Read
 **/

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

#include "err.h"

/* in characters, not bytes */
#define HEADER_SIZE 2048

/* 16bit integers */
#define MAGIC1 "fileformat = bcrstm\n"
#define MAGIC_SIZE1 (sizeof(MAGIC1) - 1)

/* floats */
#define MAGIC2 "fileformat = bcrf\n"
#define MAGIC_SIZE2 (sizeof(MAGIC2) - 1)

/* 16bit integers, text header is in Microsoft-style wide characters (utf16) */
#define MAGIC3 "f\0i\0l\0e\0f\0o\0r\0m\0a\0t\0 \0=\0 \0b\0c\0r\0s\0t\0m\0_\0u\0n\0i\0c\0o\0d\0e\0\n\0"
#define MAGIC_SIZE3 (sizeof(MAGIC3) - 1)

/* floats, text header is in Microsoft-style wide characters (utf16) */
#define MAGIC4 "f\0i\0l\0e\0f\0o\0r\0m\0a\0t\0 \0=\0 \0b\0c\0r\0f\0_\0u\0n\0i\0c\0o\0d\0e\0\n\0"
#define MAGIC_SIZE4 (sizeof(MAGIC4) - 1)

#define MAGIC5 "fileformat = bcrf\r\n"
#define MAGIC_SIZE5 (sizeof(MAGIC5) - 1)

#define MAGIC_SIZE \
    (MAX(MAX(MAX(MAGIC_SIZE1, MAGIC_SIZE2), MAX(MAGIC_SIZE3, MAGIC_SIZE4)), MAGIC_SIZE5))

#define HEADER_SIZE_MAGIC "h\000e\000a\000d\000e\000r\000s\000i\000z\000e\000"

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          find_header_size(const guchar *buffer,
                                      gsize *header_size);
static GwyField*     file_load_real  (const guchar *buffer,
                                      gsize size,
                                      gsize header_size,
                                      GHashTable *meta,
                                      GwyNield **voidmask,
                                      GError **error);
static GwyField*     read_field      (const guchar *buffer,
                                      gint xres,
                                      gint yres,
                                      GwyRawDataType rawtype,
                                      GwyByteOrder byteorder,
                                      GwyNield **voidmask);
static GwyDict* get_metadata    (GHashTable *bcrmeta);

static GwyModuleInfo module_info = {
    GWY_MODULE_ABI_VERSION,
    &module_register,
    N_("Imports Image Metrology BCR data files."),
    "Yeti <yeti@gwyddion.net>",
    "0.20",
    "David Nečas (Yeti) & Petr Klapetek",
    "2005",
};

GWY_MODULE_QUERY2(module_info, bcrfile)

static gboolean
module_register(void)
{
    gwy_file_func_register("bcrfile",
                           N_("BCR files (.bcr, .bcrf)"),
                           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, ".bcr")
                || g_str_has_suffix(fileinfo->name_lowercase, ".bcrf"))
                ? 20 : 0;
    }

    if (fileinfo->buffer_len > MAGIC_SIZE
        && (memcmp(fileinfo->head, MAGIC1, MAGIC_SIZE1) == 0
            || memcmp(fileinfo->head, MAGIC2, MAGIC_SIZE2) == 0
            || memcmp(fileinfo->head, MAGIC3, MAGIC_SIZE3) == 0
            || memcmp(fileinfo->head, MAGIC4, MAGIC_SIZE4) == 0
            || memcmp(fileinfo->head, MAGIC5, MAGIC_SIZE5) == 0))
        score = 100;

    return score;
}

static GwyFile*
load_file(const gchar *filename,
          G_GNUC_UNUSED GwyRunModeFlags mode,
          GError **error)
{
    GwyFile *file = NULL;
    GwyDict *meta;
    const guchar *buffer = NULL;
    gchar *header;
    gsize header_size, size = 0;
    GError *err = NULL;
    GwyField *field = NULL;
    GwyNield *voidmask = NULL;
    GwyTextHeaderParser parser;
    GHashTable *bcrmeta = NULL;
    gboolean utf16 = FALSE;

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

    if (size > 2*HEADER_SIZE && (memcmp(buffer, MAGIC3, MAGIC_SIZE3) == 0 || memcmp(buffer, MAGIC4, MAGIC_SIZE4) == 0))
        utf16 = TRUE;
    else if (size < HEADER_SIZE) {
        err_TOO_SHORT(error);
        gwy_unmap_file_contents(buffer);
        return NULL;
    }
    gwy_debug("utf16: %d", utf16);

    if (utf16) {
        header_size = 2*HEADER_SIZE;
        find_header_size(buffer, &header_size);
        header = gwy_utf16_to_utf8((const gunichar2*)buffer, header_size/2, GWY_BYTE_ORDER_LITTLE_ENDIAN);
        if (!header) {
            g_set_error(error, GWY_MODULE_FILE_ERROR, GWY_MODULE_FILE_ERROR_DATA,
                        _("Cannot convert string from UTF-16."));
            gwy_unmap_file_contents(buffer);
            return NULL;
        }
    }
    else {
        header_size = HEADER_SIZE;
        header = g_memdup(buffer, header_size);
        header[header_size-1] = '\0';
    }

    gwy_clear1(parser);
    parser.comment_prefix = "#\n%";
    parser.key_value_separator = "=";
    bcrmeta = gwy_text_header_parse(header, &parser, NULL, NULL);

    field = file_load_real(buffer + header_size, size, header_size, bcrmeta, &voidmask, error);
    gwy_unmap_file_contents(buffer);
    if (field) {
        file = gwy_file_new_in_construction();
        gwy_file_pass_image(file, 0, field);

        //gwy_app_set_field_title(file, 0, g_strdup(g_strrstr(filename, "/") + 1));
        gwy_image_title_fall_back(file, 0);
        if (voidmask)
            gwy_file_pass_image_mask(file, 0, voidmask);

        meta = get_metadata(bcrmeta);
        if (meta)
            gwy_file_pass_meta(file, GWY_FILE_IMAGE, 0, meta);

        gwy_log_add_import(file, GWY_FILE_IMAGE, 0, NULL, filename);
    }
    g_hash_table_destroy(bcrmeta);
    g_free(header);

    return file;
}

/* Newer files contain header size embedded in the header.  Change header_size if we find it. */
static void
find_header_size(const guchar *buffer,
                 gsize *header_size)
{
    gsize size = 0;
    const guchar *s;
    guint16 c;

    s = gwy_memmem(buffer, *header_size, HEADER_SIZE_MAGIC, sizeof(HEADER_SIZE_MAGIC)-1);
    if (!s)
        return;

    s += sizeof(HEADER_SIZE_MAGIC)-1;
    while (s - buffer < *header_size && (c = gwy_get_guint16_le(&s)) == ' ')
        ;
    s -= sizeof(guint16);
    while (s - buffer < *header_size && (c = gwy_get_guint16_le(&s)) == '=')
        ;
    s -= sizeof(guint16);
    while (s - buffer < *header_size && (c = gwy_get_guint16_le(&s)) == ' ')
        ;
    s -= sizeof(guint16);
    while (s - buffer < *header_size && (c = gwy_get_guint16_le(&s)) && g_ascii_isdigit(c)) {
        size *= 10;
        size += c - '0';
    }
    if (size > 0) {
        gwy_debug("header size found in the file: %lu", (gulong)size);
        *header_size = 2*size;
    }
}

static GwyField*
file_load_real(const guchar *buffer,
               gsize size,
               gsize header_size,
               GHashTable *meta,
               GwyNield **voidmask,
               GError **error)
{
    GwyField *field;
    GwyUnit *siunit2 = NULL;
    GwyByteOrder byteorder = GWY_BYTE_ORDER_LITTLE_ENDIAN;
    GwyRawDataType rawtype;
    gdouble q, qq;
    gint xres, yres, power10;
    guchar *s;

    if (!(s = g_hash_table_lookup(meta, "fileformat"))) {
        err_FILE_TYPE(error, "BCR/BCFR");
        return NULL;
    }

    if (gwy_strequal(s, "bcrstm") || gwy_strequal(s, "bcrstm_unicode"))
        rawtype = GWY_RAW_DATA_SINT16;
    else if (gwy_strequal(s, "bcrf") || gwy_strequal(s, "bcrf_unicode"))
        rawtype = GWY_RAW_DATA_FLOAT;
    else {
        g_set_error(error, GWY_MODULE_FILE_ERROR, GWY_MODULE_FILE_ERROR_DATA,
                    _("Unknown file type header: `%s'."), s);
        return NULL;
    }
    gwy_debug("File type: %u", type);

    if (!(s = g_hash_table_lookup(meta, "xpixels"))) {
        err_MISSING_FIELD(error, "xpixels");
        return NULL;
    }
    xres = atol(s);

    if (!(s = g_hash_table_lookup(meta, "ypixels"))) {
        err_MISSING_FIELD(error, "ypixels");
        return NULL;
    }
    yres = atol(s);

    if (err_DIMENSION(error, xres) || err_DIMENSION(error, yres))
        return NULL;

    if ((s = g_hash_table_lookup(meta, "intelmode")))
        byteorder = (atol(s) ? GWY_BYTE_ORDER_LITTLE_ENDIAN : GWY_BYTE_ORDER_BIG_ENDIAN);

    if (err_SIZE_MISMATCH(error, xres*yres*gwy_raw_data_size(rawtype), (guint)(size - header_size), FALSE))
        return NULL;

    field = read_field(buffer, xres, yres, rawtype, byteorder, voidmask);

    if ((s = g_hash_table_lookup(meta, "xlength")) && (q = g_ascii_strtod(s, NULL)) > 0) {
        if (!(s = g_hash_table_lookup(meta, "xunit")))
            s = "nm";

        power10 = gwy_unit_set_from_string(gwy_field_get_unit_xy(field), s);
        q *= gwy_exp10(power10);
        gwy_field_set_xreal(field, q);
    }

    if ((s = g_hash_table_lookup(meta, "ylength")) && (q = g_ascii_strtod(s, NULL)) > 0) {
        if (!(s = g_hash_table_lookup(meta, "yunit")))
            s = "nm";

        siunit2 = gwy_unit_new_parse(s, &power10);
        q *= gwy_exp10(power10);
        if (!gwy_unit_equal(gwy_field_get_unit_xy(field), siunit2))
            g_warning("Incompatible x and y units");
        g_object_unref(siunit2);
        gwy_field_set_yreal(field, q);
    }

    if (!(s = g_hash_table_lookup(meta, "zunit")))
        s = "nm";
    power10 = gwy_unit_set_from_string(gwy_field_get_unit_z(field), s);
    q = gwy_exp10(power10);

    if (rawtype == GWY_RAW_DATA_SINT16) {
        if ((s = g_hash_table_lookup(meta, "bit2nm")) && (qq = g_ascii_strtod(s, NULL)) > 0)
            gwy_field_multiply(field, q*qq);
    }
    else if (rawtype == GWY_RAW_DATA_FLOAT)
        gwy_field_multiply(field, q);

    if ((s = g_hash_table_lookup(meta, "zmin")) && (qq = g_ascii_strtod(s, NULL)) > 0)
        gwy_field_add(field, q*qq - gwy_field_min(field));

    return field;
}

static GwyDict*
get_metadata(GHashTable *bcrmeta)
{
    const struct {
        const gchar *id;
        const gchar *unit;
        const gchar *key;
    }
    metakeys[] = {
        { "scanspeed",   "nm/s",    "Scan speed"        },
        { "xoffset",     "nm",      "X offset"          },
        { "yoffset",     "nm",      "Y offset"          },
        { "bias",        "V",       "Bias voltage"      },
        { "current",     "nA",      "Tunneling current" },
        { "starttime",   NULL,      "Scan time"         },
        /* FIXME: I've seen other stuff, but don't know interpretation */
    };

    GwyDict *meta = gwy_dict_new_in_construction();

    for (guint i = 0; i < G_N_ELEMENTS(metakeys); i++) {
        gchar *value;
        if (!(value = g_hash_table_lookup(bcrmeta, metakeys[i].id)))
            continue;

        if (metakeys[i].unit)
            gwy_dict_pass_string_by_name(meta, metakeys[i].key, g_strdup_printf("%s %s", value, metakeys[i].unit));
        else
            gwy_dict_set_string_by_name(meta, metakeys[i].key, value);
    }

    if (!gwy_dict_get_n_items(meta))
        g_clear_object(&meta);

    return meta;
}

static GwyField*
read_field(const guchar *buffer,
           gint xres,
           gint yres,
           GwyRawDataType rawtype,
           GwyByteOrder byteorder,
           GwyNield **voidmask)
{
    guint i, n = xres*yres;

    GwyField *field = gwy_field_new(xres, yres, 1e-6, 1e-6, FALSE);
    *voidmask = gwy_field_new_nield_alike(field);

    gdouble *data = gwy_field_get_data(field);
    gint *voids = gwy_nield_get_data(*voidmask);
    gwy_convert_raw_data(buffer, n, 1, rawtype, byteorder, data, 1.0, 0.0);
    if (rawtype == GWY_RAW_DATA_SINT16) {
        for (i = 0; i < n; i++) {
            if (data[i] == 32767.0)
                voids[i] = 1;
        }
    }
    else if (rawtype == GWY_RAW_DATA_FLOAT) {
        for (i = 0; i < n; i++) {
            if (data[i] > 1.7e38)
                voids[i] = 1;
        }
    }

    *voidmask = gwy_remove_bad_image_data(field, *voidmask);

    return field;
}

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