/*
 *  $Id: hitachi-afm.c 29571 2026-03-05 15:49:32Z yeti-dn $
 *  Copyright (C) 2005-2025 David Necas (Yeti), Petr Klapetek, Markus Pristovsek
 *  E-mail: yeti@gwyddion.net, klapetek@gwyddion.net.
 *  prissi@gift.physik.tu-berlin.de.
 *
 *  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.
 */

/* Note read_field() and read_field_old() read two completely different file formats, even if they were
 * probably both invented at Hitachi. */

/**
 * [FILE-MAGIC-FREEDESKTOP]
 * <mime-type type="application/x-hitachi-spm">
 *   <comment>Hitachi SPM data</comment>
 *     <magic priority="80">
 *     <match type="string" offset="0" value="AFM/Ver. "/>
 *   </magic>
 * </mime-type>
 **/

/**
 * [FILE-MAGIC-FILEMAGIC]
 * # Hitachi AFM data.
 * # Look also at the date field before deciding.
 * 0 string AFM/Ver.\x20
 * >32 regex [0-9]{4}/[0-9]{2}/[0-9]{2}
 * >>9 regex [0-9.]+ Hitachi AFM data version %s
 **/

/**
 * [FILE-MAGIC-USERGUIDE]
 * Hitachi AFM
 * .afm
 * Read
 **/

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

#include "err.h"

#define MAGIC "AFM/Ver. "
#define MAGIC_SIZE (sizeof(MAGIC)-1)

#define EXTENSION ".afm"

#define Nanometer (1e-9)

enum {
    HEADER_SIZE   = 0x280,
    XREAL_OFFSET  = 0x16c,
    YREAL_OFFSET  = 0x176,
    ZSCALE_OFFSET = 0x184,
    RES_OFFSET    = 0x1dc,
};

enum {
    HEADER_SIZE_OLD  = 0x100,
    RES_OFFSET_OLD   = 0xc2,
    SCALE_OFFSET_OLD = 0x42,
    UNIT_OFFSET_OLD  = 0x62,
    SPEED_OFFSET_OLD = 0x82,
    NS_OFFSET_OLD    = 0xc8,
};

typedef GwyField* (*ReadImageFunc)(const guchar *buffer, gsize size, GError **error);

static gboolean  module_register(void);
static gint      detect_file    (const GwyFileDetectInfo *fileinfo,
                                 gboolean only_name);
static gint      file_detect_old(const GwyFileDetectInfo *fileinfo,
                                 gboolean only_name);
static GwyFile*  load_file      (const gchar *filename,
                                 GwyRunModeFlags mode,
                                 GError **error);
static GwyFile*  file_load_old  (const gchar *filename,
                                 GwyRunModeFlags mode,
                                 GError **error);
static GwyFile*  hitachi_load   (const gchar *filename,
                                 ReadImageFunc read_image,
                                 gsize header_size,
                                 GError **error);
static GwyField* read_field     (const guchar *buffer,
                                 gsize size,
                                 GError **error);
static GwyField* read_field_old (const guchar *buffer,
                                 gsize size,
                                 GError **error);

static GwyModuleInfo module_info = {
    GWY_MODULE_ABI_VERSION,
    &module_register,
    N_("Imports Hitachi AFM files."),
    "Yeti <yeti@gwyddion.net>",
    "0.8",
    "David Nečas (Yeti) & Petr Klapetek & Markus Pristovsek",
    "2005",
};

GWY_MODULE_QUERY2(module_info, hitachi_afm)

static gboolean
module_register(void)
{
    /* Register two functions to keep the disctinction in the app although the load function physically the same */
    gwy_file_func_register("hitachi-afm",
                           N_("Hitachi AFM files (.afm)"),
                           detect_file, load_file, NULL, NULL);
    gwy_file_func_register("hitachi-afm-old",
                           N_("Hitachi AFM files, old (.afm)"),
                           file_detect_old, file_load_old, 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) ? 10 : 0;

    if (fileinfo->buffer_len > MAGIC_SIZE
        && fileinfo->file_size >= HEADER_SIZE + 2
        && !memcmp(fileinfo->head, MAGIC, MAGIC_SIZE))
        score = 100;

    return score;
}

static gint
file_detect_old(const GwyFileDetectInfo *fileinfo,
                gboolean only_name)
{
    if (only_name)
        return g_str_has_suffix(fileinfo->name_lowercase, EXTENSION) ? 10 : 0;

    if (fileinfo->buffer_len < HEADER_SIZE_OLD
        || fileinfo->file_size < HEADER_SIZE_OLD + 2
        /* This is actually header size (0x100), just weed out non-AFM files */
        || fileinfo->head[0] != 0 || fileinfo->head[1] != 1)
        return 0;

    const guchar *p = fileinfo->head + RES_OFFSET_OLD;
    guint xres = gwy_get_guint16_le(&p);
    guint yres = gwy_get_guint16_le(&p);

    if (fileinfo->file_size == 2*xres*yres + HEADER_SIZE_OLD)
        return 100;

    return 0;
}

static GwyFile*
load_file(const gchar *filename,
          G_GNUC_UNUSED GwyRunModeFlags mode,
          GError **error)
{
    return hitachi_load(filename, read_field, HEADER_SIZE, error);
}

static GwyFile*
file_load_old(const gchar *filename,
              G_GNUC_UNUSED GwyRunModeFlags mode,
              GError **error)
{
    return hitachi_load(filename, read_field_old, HEADER_SIZE_OLD, error);
}

static GwyFile*
hitachi_load(const gchar *filename,
             ReadImageFunc read_image,
             gsize header_size,
             GError **error)
{
    const guchar *buffer = NULL;
    gsize size = 0;
    GError *err = NULL;

    if (!gwy_map_file_contents(filename, &buffer, &size, &err)) {
        err_GET_FILE_CONTENTS(error, &err);
        return NULL;
    }
    if (size < header_size + 2) {
        err_TOO_SHORT(error);
        gwy_unmap_file_contents(buffer);
        return NULL;
    }

    GwyField *dfield = read_image(buffer, size, error);
    gwy_unmap_file_contents(buffer);
    if (!dfield)
        return NULL;

    GwyFile *file = gwy_file_new_in_construction();
    gwy_file_pass_image(file, 0, dfield);
    gwy_file_set_title(file, GWY_FILE_IMAGE, 0, "Topography", FALSE);

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

    return file;
}

static GwyField*
read_field(const guchar *buffer,
           gsize size,
           GError **error)
{
    gint xres, yres, n;
    gdouble xreal, yreal, q;
    GwyField *dfield;
    const guchar *p;

    p = buffer + RES_OFFSET;
    xres = gwy_get_guint32_le(&p);
    yres = gwy_get_guint32_le(&p);
    gwy_debug("xres: %d, yres: %d", xres, yres);
    if (err_DIMENSION(error, xres) || err_DIMENSION(error, yres))
        return NULL;

    n = xres*yres;
    if (err_SIZE_MISMATCH(error, 2*n + HEADER_SIZE, size, TRUE))
        return NULL;

    p = buffer + XREAL_OFFSET;
    xreal = gwy_get_gdouble_le(&p) * Nanometer;
    p = buffer + YREAL_OFFSET;
    yreal = gwy_get_gdouble_le(&p) * Nanometer;
    p = buffer + ZSCALE_OFFSET;
    q = gwy_get_gdouble_le(&p) * Nanometer;
    gwy_debug("xreal: %g, yreal: %g, zreal: %g", xreal/Nanometer, yreal/Nanometer, q/Nanometer);
    sanitise_real_size(&xreal, "x size");
    sanitise_real_size(&yreal, "y size");
    /* XXX: I don't know where the factor of 0.5 comes from.  But it makes the imported data match the original
     * software. */
    q /= 2.0;
    q /= 65536.0;

    dfield = gwy_field_new(xres, yres, xreal, yreal, FALSE);
    gwy_convert_raw_data(buffer + HEADER_SIZE, xres*yres, 1, GWY_RAW_DATA_UINT16, GWY_BYTE_ORDER_LITTLE_ENDIAN,
                         gwy_field_get_data(dfield), q, 0.0);
    gwy_field_flip(dfield, FALSE, TRUE);

    gwy_unit_set_from_string(gwy_field_get_unit_xy(dfield), "m");
    gwy_unit_set_from_string(gwy_field_get_unit_z(dfield), "m");

    return dfield;
}

static GwyField*
read_field_old(const guchar *buffer,
               gsize size,
               GError **error)
{
    gint xres, yres, n, vx, vy;
    G_GNUC_UNUSED gint vz;
    gdouble xscale, yscale, zscale, xreal, yreal, q;
    G_GNUC_UNUSED gdouble xunit, yunit, zunit;
    GwyField *dfield;
    const guchar *p;

    p = buffer + RES_OFFSET_OLD;
    xres = gwy_get_guint16_le(&p);
    yres = gwy_get_guint16_le(&p);
    gwy_debug("xres: %d, yres: %d", xres, yres);
    if (err_DIMENSION(error, xres) || err_DIMENSION(error, yres))
        return NULL;

    n = xres*yres;
    if (err_SIZE_MISMATCH(error, 2*n + HEADER_SIZE_OLD, size, TRUE))
        return NULL;

    p = buffer + SCALE_OFFSET_OLD;
    xscale = gwy_get_gdouble_le(&p);
    yscale = gwy_get_gdouble_le(&p);
    zscale = gwy_get_gdouble_le(&p);
    p = buffer + UNIT_OFFSET_OLD;
    xunit = gwy_get_gdouble_le(&p);
    yunit = gwy_get_gdouble_le(&p);
    zunit = gwy_get_gdouble_le(&p);
    p = buffer + SPEED_OFFSET_OLD;
    vx = gwy_get_guint32_le(&p);
    vy = gwy_get_guint32_le(&p);
    vz = gwy_get_guint32_le(&p);

    xreal = xscale * vx;
    yreal = yscale * vy;
    q = zscale;
    gwy_debug("xreal: %g, yreal: %g, zscale: %g", xreal, yreal, q);
    sanitise_real_size(&xreal, "x size");
    sanitise_real_size(&yreal, "y size");

    dfield = gwy_field_new(xres, yres, xreal, yreal, FALSE);
    gwy_convert_raw_data(buffer + HEADER_SIZE_OLD, xres*yres, 1, GWY_RAW_DATA_UINT16, GWY_BYTE_ORDER_LITTLE_ENDIAN,
                         gwy_field_get_data(dfield), q, 0.0);

    gwy_unit_set_from_string(gwy_field_get_unit_xy(dfield), "m");
    gwy_unit_set_from_string(gwy_field_get_unit_z(dfield), "m");

    return dfield;
}

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